From 7b9e30914fdd97fa3894c09e04be74aa11ff37af Mon Sep 17 00:00:00 2001 From: pranav nachnekar Date: Wed, 28 Aug 2019 16:57:37 +0530 Subject: [PATCH 001/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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/679] 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 a656151ee970fae142c03bcce04dd5b3dde7578e Mon Sep 17 00:00:00 2001 From: Rohan Date: Thu, 12 Sep 2019 16:00:25 +0530 Subject: [PATCH 037/679] fix: operating cost calculation in JS --- erpnext/manufacturing/doctype/work_order/work_order.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index ce7b4f9425b..d82158af33a 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -385,6 +385,11 @@ frappe.ui.form.on("Work Order", { } }); } + }, + + additional_operating_cost: function(frm) { + erpnext.work_order.calculate_cost(frm.doc); + erpnext.work_order.calculate_total_cost(frm); } }); @@ -524,9 +529,8 @@ erpnext.work_order = { }, calculate_total_cost: function(frm) { - var variable_cost = frm.doc.actual_operating_cost ? - flt(frm.doc.actual_operating_cost) : flt(frm.doc.planned_operating_cost); - frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost)); + let variable_cost = flt(frm.doc.actual_operating_cost) || flt(frm.doc.planned_operating_cost); + frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost)) }, set_default_warehouse: function(frm) { From cf045d86b07dae9dcd23ab53327574038b5e84fa Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 13 Sep 2019 15:55:54 +0530 Subject: [PATCH 038/679] 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 039/679] 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 040/679] 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 041/679] 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 042/679] 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 043/679] 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 044/679] 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 045/679] 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 046/679] 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 047/679] 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 048/679] 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 049/679] 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 050/679] 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 051/679] 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 052/679] 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 053/679] 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 054/679] 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 055/679] 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 056/679] 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 057/679] 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 058/679] 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 059/679] 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 060/679] 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 061/679] 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 062/679] 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 063/679] 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 064/679] 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 065/679] 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 066/679] 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 067/679] 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 068/679] 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 069/679] 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 070/679] 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 071/679] 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 072/679] 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 073/679] 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 074/679] 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 075/679] 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 076/679] 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 077/679] 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 078/679] 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 079/679] 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 080/679] 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 081/679] 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 082/679] 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 083/679] 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 084/679] 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 085/679] 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 086/679] 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 087/679] 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 088/679] 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 089/679] 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 090/679] 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 091/679] 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 092/679] 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 093/679] 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 094/679] 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 095/679] 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 096/679] 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 097/679] 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 098/679] 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 099/679] 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 100/679] 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 101/679] 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 102/679] 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 103/679] 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 104/679] 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 105/679] 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 106/679] 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 107/679] 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 108/679] 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 109/679] 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 110/679] 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 111/679] 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 112/679] 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 113/679] 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 114/679] 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 07ae3abf34f504e9766b7a34f9bc974ac4d13843 Mon Sep 17 00:00:00 2001 From: EconCode <56735547+EconCode@users.noreply.github.com> Date: Sat, 26 Oct 2019 16:31:29 +0200 Subject: [PATCH 115/679] fix: unregistering url wrong formated string Fixed the formated string for unregistering webhooks. String should look like: "/admin/api/2019-04/webhooks/#{webhook_id}.json" Taken from shopify api documentation - deleting webhooks: https://help.shopify.com/en/api/reference/events/webhook?api[version]=2019-04#destroy-2019-04 --- .../doctype/shopify_settings/shopify_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py index a4332b199e0..46422f54ed7 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.py @@ -50,7 +50,7 @@ class ShopifySettings(Document): deleted_webhooks = [] for d in self.webhooks: - url = get_shopify_url('admin/api/2019-04/webhooks.json'.format(d.webhook_id), self) + url = get_shopify_url('admin/api/2019-04/webhooks/{0}.json'.format(d.webhook_id), self) try: res = session.delete(url, headers=get_header(self)) res.raise_for_status() From 07b908c0c560f6fa59710ff798ad90fa3077a838 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 28 Oct 2019 16:00:24 +0530 Subject: [PATCH 116/679] patch: Missing Patch for item Tax template --- .../accounts/doctype/accounts_settings/accounts_settings.py | 4 ++-- erpnext/patches.txt | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py index 3222aeb0855..2473d715d0d 100644 --- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py +++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py @@ -15,8 +15,8 @@ class AccountsSettings(Document): frappe.clear_cache() def validate(self): - for f in ["add_taxes_from_item_tax_template"]: - frappe.db.set_default(f, self.get(f, "")) + frappe.db.set_default("add_taxes_from_item_tax_template", + self.get("add_taxes_from_item_tax_template", 0)) self.validate_stale_days() self.enable_payment_schedule_in_print() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index ee6bdff6612..94736e08e36 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -641,3 +641,4 @@ erpnext.patches.v12_0.set_produced_qty_field_in_sales_order_for_work_order erpnext.patches.v12_0.generate_leave_ledger_entries erpnext.patches.v12_0.set_default_shopify_app_type erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings +execute:frappe.db.set_value("Accounts Settings", None, "add_taxes_from_item_tax_template", 1) #setting default tax template enable \ No newline at end of file From d1c530c564e639bc8bac58f170c54e72a73381aa Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 31 Oct 2019 15:36:33 +0530 Subject: [PATCH 117/679] 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 118/679] 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 119/679] 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 120/679] 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 121/679] 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 122/679] 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 123/679] 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 124/679] 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 125/679] 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 126/679] 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 127/679] 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 075f6ea0d56fe6a41fb5c5e50679390994796903 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 4 Nov 2019 19:41:04 +0530 Subject: [PATCH 128/679] feat: Provision to fetch items from BOM in Stock Entry Items are populated in child table and amounts are calculated Warehouse Fields in popup toggle based on Stock Entry Type --- erpnext/manufacturing/doctype/bom/bom.py | 1 + .../stock/doctype/stock_entry/stock_entry.js | 73 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 225ae29429e..d02dd59d13c 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -599,6 +599,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite item.image, bom.project, item.stock_uom, + item.item_group, item.allow_alternative_item, item_default.default_warehouse, item_default.expense_account as expense_account, diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 0b023024f9d..cee09e78c6e 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -181,6 +181,12 @@ frappe.ui.form.on('Stock Entry', { } } + if (frm.doc.docstatus === 0) { + frm.add_custom_button(__('Bill of Materials'), function(){ + frm.events.get_items_from_bom(frm); + }, __("Get items from")); + } + if (frm.doc.docstatus===0) { frm.add_custom_button(__('Purchase Invoice'), function() { erpnext.utils.map_current_doc({ @@ -387,6 +393,73 @@ frappe.ui.form.on('Stock Entry', { } }, + get_items_from_bom: function(frm) { + let filters = function(){ + return {filters: { docstatus:1 }}; + } + + let fields = [ + {"fieldname":"bom", "fieldtype":"Link", "label":__("BOM"), + options:"BOM", reqd: 1, get_query: filters()}, + {"fieldname":"source_warehouse", "fieldtype":"Link", "label":__("Source Warehouse"), + options:"Warehouse"}, + {"fieldname":"target_warehouse", "fieldtype":"Link", "label":__("Target Warehouse"), + options:"Warehouse"}, + {"fieldname":"qty", "fieldtype":"Float", "label":__("Quantity"), + reqd: 1, "default": 1}, + {"fieldname":"fetch_exploded", "fieldtype":"Check", + "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1}, + {"fieldname":"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"} + ] + if (frm.doc.stock_entry_type == 'Material Issue'){ + fields.splice(2,1); + } + else if(frm.doc.stock_entry_type == 'Material Receipt'){ + fields.splice(1,1); + } + + let d = new frappe.ui.Dialog({ + title: __("Get Items from BOM"), + fields: fields + }); + d.get_input("fetch").on("click", function() { + let values = d.get_values(); + if(!values) return; + values["company"] = frm.doc.company; + if(!frm.doc.company) frappe.throw(__("Company field is required")); + frappe.call({ + method: "erpnext.manufacturing.doctype.bom.bom.get_bom_items", + args: values, + callback: function(r) { + if (!r.message) { + frappe.throw(__("BOM does not contain any stock item")); + } else { + erpnext.utils.remove_empty_first_row(frm, "items"); + $.each(r.message, function(i, item) { + let d = frappe.model.add_child(cur_frm.doc, "Stock Entry Detail", "items"); + d.item_code = item.item_code; + d.item_name = item.item_name; + d.item_group = item.item_group; + d.s_warehouse = values.source_warehouse; + d.t_warehouse = values.target_warehouse; + d.uom = item.stock_uom; + d.stock_uom = item.stock_uom; + d.conversion_factor = 1; + d.qty = item.qty; + d.expense_account = item.expense_account; + d.project = item.project; + frm.events.set_basic_rate(frm, d.doctype, d.name); + }); + } + d.hide(); + refresh_field("items"); + } + }); + + }); + d.show(); + }, + calculate_basic_amount: function(frm, item) { item.basic_amount = flt(flt(item.transfer_qty) * flt(item.basic_rate), precision("basic_amount", item)); From 4d3dc87a1a2dcf4ad7c71c276c0fc4e8368944dc Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 5 Nov 2019 04:32:06 +0000 Subject: [PATCH 129/679] 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 130/679] 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 131/679] 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 5a31e228923d3ec2fe387e3e83eca3920cee6da4 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 6 Nov 2019 17:03:12 +0530 Subject: [PATCH 132/679] chore: frappe.rename_doc update --- .../clinical_procedure_template.py | 3 ++- .../healthcare_service_unit_type.py | 3 ++- .../doctype/lab_test_template/lab_test_template.py | 5 +++-- erpnext/patches/v9_2/rename_translated_domains_in_en.py | 5 +++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py index 141329b3db1..a8a9037e4ae 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe, json from frappe import _ from frappe.model.document import Document +from frappe.model.rename_doc import rename_doc from frappe.utils import nowdate class ClinicalProcedureTemplate(Document): @@ -115,7 +116,7 @@ def change_item_code_from_template(item_code, doc): "item_code": item_code})): frappe.throw(_("Code {0} already exist").format(item_code)) else: - frappe.rename_doc("Item", doc.item_code, item_code, ignore_permissions = True) + rename_doc("Item", doc.item_code, item_code, ignore_permissions=True) frappe.db.set_value("Clinical Procedure Template", doc.name, "item_code", item_code) return diff --git a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py index 650499454b9..43f01c86b94 100644 --- a/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py +++ b/erpnext/healthcare/doctype/healthcare_service_unit_type/healthcare_service_unit_type.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document +from frappe.model.rename_doc import rename_doc class HealthcareServiceUnitType(Document): def validate(self): @@ -107,7 +108,7 @@ def change_item_code(item, item_code, doc_name): if(item_exist): frappe.throw(_("Code {0} already exist").format(item_code)) else: - frappe.rename_doc("Item", item, item_code, ignore_permissions = True) + rename_doc("Item", item, item_code, ignore_permissions=True) frappe.db.set_value("Healthcare Service Unit Type", doc_name, "item_code", item_code) @frappe.whitelist() diff --git a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py index 101e143c123..91488e35333 100644 --- a/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py +++ b/erpnext/healthcare/doctype/lab_test_template/lab_test_template.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe, json from frappe.model.document import Document +from frappe.model.rename_doc import rename_doc from frappe import _ class LabTestTemplate(Document): @@ -112,9 +113,9 @@ def change_test_code_from_template(lab_test_code, doc): if(item_exist): frappe.throw(_("Code {0} already exist").format(lab_test_code)) else: - frappe.rename_doc("Item", doc.name, lab_test_code, ignore_permissions = True) + rename_doc("Item", doc.name, lab_test_code, ignore_permissions=True) frappe.db.set_value("Lab Test Template",doc.name,"lab_test_code",lab_test_code) - frappe.rename_doc("Lab Test Template", doc.name, lab_test_code, ignore_permissions = True) + rename_doc("Lab Test Template", doc.name, lab_test_code, ignore_permissions=True) return lab_test_code @frappe.whitelist() diff --git a/erpnext/patches/v9_2/rename_translated_domains_in_en.py b/erpnext/patches/v9_2/rename_translated_domains_in_en.py index aec5d438ec8..e5a9e2461fa 100644 --- a/erpnext/patches/v9_2/rename_translated_domains_in_en.py +++ b/erpnext/patches/v9_2/rename_translated_domains_in_en.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals import frappe from frappe import _ +from frappe.model.rename_doc import rename_doc def execute(): frappe.reload_doc('stock', 'doctype', 'item') @@ -20,11 +21,11 @@ def execute(): if frappe.db.exists("Domain", domain): merge=True - frappe.rename_doc("Domain", translated_domain, domain, ignore_permissions=True, merge=merge) + rename_doc("Domain", translated_domain, domain, ignore_permissions=True, merge=merge) domain_settings = frappe.get_single("Domain Settings") active_domains = [d.domain for d in domain_settings.active_domains] - + try: for domain in active_domains: domain = frappe.get_doc("Domain", domain) From fce8f36bb2ba3541895a67714ec508b45e48d487 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 12:37:28 +0530 Subject: [PATCH 133/679] 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 134/679] 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 135/679] 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 136/679] 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 137/679] 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 138/679] 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 139/679] 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 06c6f7cfd3862955229d95168b77b39c5cd56916 Mon Sep 17 00:00:00 2001 From: Tufan Kaynak <31142607+toofun666@users.noreply.github.com> Date: Mon, 11 Nov 2019 14:41:16 +0300 Subject: [PATCH 140/679] fix: Currency Exchange for_selling and for_buying on the same day (#19339) * fix: test data of Currency Exchange to incluse buying and selling test data of Currency Exchange to incluse buying and selling * fix: Currency Exchange Test corrected to include selling and buying exchange_rate Currency Exchange Test corrected to include selling and buying exchange_rate * fix: Currency Exchange for_selling and for_buying fields test and functionality restored In this fix: * You can now add a separate exchange_rate in date for_selling and for_buying. You could not before because the unique field name was only calculated to allow a single name for a date * tests did not account for for_selling and for_buying fields and thier uniqueness * Update test_currency_exchange.py * Update test_records.json * fix: update test_records.json * Update test_records.json * The basic package for turkey is defined. It is empty now but applications specific to Turkey will be created under that package. * fix: update code with scapes vs.tabs updated the code regarding spaces issue * Update currency_exchange.py --- erpnext/regional/turkey/__init__.py | 0 .../currency_exchange/currency_exchange.py | 11 ++- .../test_currency_exchange.py | 38 +++++--- .../currency_exchange/test_records.json | 96 +++++++++++-------- 4 files changed, 85 insertions(+), 60 deletions(-) create mode 100644 erpnext/regional/turkey/__init__.py diff --git a/erpnext/regional/turkey/__init__.py b/erpnext/regional/turkey/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.py b/erpnext/setup/doctype/currency_exchange/currency_exchange.py index 4effb5ab017..60d367a4bbc 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.py @@ -11,10 +11,15 @@ from frappe.utils import get_datetime_str, formatdate, nowdate, cint class CurrencyExchange(Document): def autoname(self): + purpose = "" if not self.date: self.date = nowdate() - self.name = '{0}-{1}-{2}'.format(formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), - self.from_currency, self.to_currency) + if cint(self.for_buying)==0 and cint(self.for_selling)==1: + purpose = "Selling" + if cint(self.for_buying)==1 and cint(self.for_selling)==0: + purpose = "Buying" + self.name = '{0}-{1}-{2}{3}'.format(formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), + self.from_currency, self.to_currency, ("-" + purpose) if purpose else "") def validate(self): self.validate_value("exchange_rate", ">", 0) @@ -23,4 +28,4 @@ class CurrencyExchange(Document): throw(_("From Currency and To Currency cannot be same")) if not cint(self.for_buying) and not cint(self.for_selling): - throw(_("Currency Exchange must be applicable for Buying or for Selling.")) \ No newline at end of file + throw(_("Currency Exchange must be applicable for Buying or for Selling.")) diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index c488b996ffa..857f666b2a6 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -4,15 +4,21 @@ from __future__ import unicode_literals import frappe, unittest from frappe.utils import flt from erpnext.setup.utils import get_exchange_rate +from frappe.utils import cint test_records = frappe.get_test_records('Currency Exchange') def save_new_records(test_records): for record in test_records: + purpose = str("") + if cint(record.get("for_buying"))==0 and cint(record.get("for_selling"))==1: + purpose = "Selling" + if cint(record.get("for_buying"))==1 and cint(record.get("for_selling"))==0: + purpose = "Buying" kwargs = dict( doctype=record.get("doctype"), - docname=record.get("date") + '-' + record.get("from_currency") + '-' + record.get("to_currency"), + docname=record.get("date") + '-' + record.get("from_currency") + '-' + record.get("to_currency") + '-' + purpose, fieldname="exchange_rate", value=record.get("exchange_rate"), ) @@ -25,6 +31,8 @@ def save_new_records(test_records): curr_exchange.from_currency = record["from_currency"] curr_exchange.to_currency = record["to_currency"] curr_exchange.exchange_rate = record["exchange_rate"] + curr_exchange.for_buying = record["for_buying"] + curr_exchange.for_selling = record["for_selling"] curr_exchange.insert() @@ -44,18 +52,18 @@ class TestCurrencyExchange(unittest.TestCase): frappe.db.set_value("Accounts Settings", None, "allow_stale", 1) # Start with allow_stale is True - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01") + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying") self.assertEqual(flt(exchange_rate, 3), 60.0) - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15") + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") self.assertEqual(exchange_rate, 65.1) - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30") + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io self.clear_cache() - exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15") + exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_selling") self.assertFalse(exchange_rate == 60) self.assertEqual(flt(exchange_rate, 3), 66.894) @@ -64,35 +72,35 @@ class TestCurrencyExchange(unittest.TestCase): frappe.db.set_value("Accounts Settings", None, "allow_stale", 0) frappe.db.set_value("Accounts Settings", None, "stale_days", 1) - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01") + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-01", "for_buying") self.assertEqual(exchange_rate, 60.0) # Will fetch from fixer.io self.clear_cache() - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15") + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") self.assertEqual(flt(exchange_rate, 3), 67.79) - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30") + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-30", "for_selling") self.assertEqual(exchange_rate, 62.9) # Exchange rate as on 15th Dec, 2015, should be fetched from fixer.io self.clear_cache() - exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15") + exchange_rate = get_exchange_rate("USD", "INR", "2015-12-15", "for_buying") self.assertEqual(flt(exchange_rate, 3), 66.894) - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10") + exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-10", "for_selling") self.assertEqual(exchange_rate, 65.1) # NGN is not available on fixer.io so these should return 0 - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09") + exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-09", "for_selling") self.assertEqual(exchange_rate, 0) - exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11") + exchange_rate = get_exchange_rate("INR", "NGN", "2016-01-11", "for_selling") self.assertEqual(exchange_rate, 0) def test_exchange_rate_strict_switched(self): # Start with allow_stale is True - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15") + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") self.assertEqual(exchange_rate, 65.1) frappe.db.set_value("Accounts Settings", None, "allow_stale", 0) @@ -100,5 +108,5 @@ class TestCurrencyExchange(unittest.TestCase): # Will fetch from fixer.io self.clear_cache() - exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15") - self.assertEqual(flt(exchange_rate, 3), 67.79) \ No newline at end of file + exchange_rate = get_exchange_rate("USD", "INR", "2016-01-15", "for_buying") + self.assertEqual(flt(exchange_rate, 3), 67.79) diff --git a/erpnext/setup/doctype/currency_exchange/test_records.json b/erpnext/setup/doctype/currency_exchange/test_records.json index 0c9cfbb67c0..152060edfc4 100644 --- a/erpnext/setup/doctype/currency_exchange/test_records.json +++ b/erpnext/setup/doctype/currency_exchange/test_records.json @@ -1,44 +1,56 @@ [ - { - "doctype": "Currency Exchange", - "date": "2016-01-01", - "exchange_rate": 60.0, - "from_currency": "USD", - "to_currency": "INR" - }, - { - "doctype": "Currency Exchange", - "date": "2016-01-01", - "exchange_rate": 0.773, - "from_currency": "USD", - "to_currency": "EUR" - }, - { - "doctype": "Currency Exchange", - "date": "2016-01-01", - "exchange_rate": 0.0167, - "from_currency": "INR", - "to_currency": "USD" - }, - { - "doctype": "Currency Exchange", - "date": "2016-01-10", - "exchange_rate": 65.1, - "from_currency": "USD", - "to_currency": "INR" - }, + { + "doctype": "Currency Exchange", + "date": "2016-01-01", + "exchange_rate": 60.0, + "from_currency": "USD", + "to_currency": "INR", + "for_buying": 1, + "for_selling": 0 + }, { - "doctype": "Currency Exchange", - "date": "2016-01-30", - "exchange_rate": 62.9, - "from_currency": "USD", - "to_currency": "INR" - }, - { - "doctype": "Currency Exchange", - "date": "2016-01-10", - "exchange_rate": 65.1, - "from_currency": "INR", - "to_currency": "NGN" - } -] \ No newline at end of file + "doctype": "Currency Exchange", + "date": "2016-01-01", + "exchange_rate": 0.773, + "from_currency": "USD", + "to_currency": "EUR", + "for_buying": 0, + "for_selling": 1 + }, + { + "doctype": "Currency Exchange", + "date": "2016-01-01", + "exchange_rate": 0.0167, + "from_currency": "INR", + "to_currency": "USD", + "for_buying": 1, + "for_selling": 0 + }, + { + "doctype": "Currency Exchange", + "date": "2016-01-10", + "exchange_rate": 65.1, + "from_currency": "USD", + "to_currency": "INR", + "for_buying": 1, + "for_selling": 0 + }, + { + "doctype": "Currency Exchange", + "date": "2016-01-30", + "exchange_rate": 62.9, + "from_currency": "USD", + "to_currency": "INR", + "for_buying": 1, + "for_selling": 1 + }, + { + "doctype": "Currency Exchange", + "date": "2016-01-10", + "exchange_rate": 65.1, + "from_currency": "INR", + "to_currency": "NGN", + "for_buying": 1, + "for_selling": 1 + } +] From 88de00fb9496e8f67f2970bc35104bbb5e952584 Mon Sep 17 00:00:00 2001 From: "FinByz Tech Pvt. Ltd" Date: Thu, 17 Oct 2019 17:12:23 +0530 Subject: [PATCH 141/679] Rendered Email template in email Campaign --- erpnext/crm/doctype/email_campaign/email_campaign.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 98e4927beb6..6ea24a5f204 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -73,13 +73,13 @@ def send_mail(entry, email_campaign): email_template = frappe.get_doc("Email Template", entry.get("email_template")) sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email') - + context = { "doc":frappe.get_doc(email_campaign.email_campaign_for,email_campaign.recipient) } # send mail and link communication to document comm = make( doctype = "Email Campaign", name = email_campaign.name, subject = email_template.get("subject"), - content = email_template.get("response"), + content = frappe.render_template(email_template.get("response"),context), sender = sender, recipients = recipient, communication_medium = "Email", From 4c7ac65db1174d6659547741631f141cec905931 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 11 Nov 2019 17:19:52 +0530 Subject: [PATCH 142/679] fix: Made Campaign for field mandatory --- erpnext/crm/doctype/email_campaign/email_campaign.json | 5 +++-- erpnext/crm/doctype/email_campaign/email_campaign.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.json b/erpnext/crm/doctype/email_campaign/email_campaign.json index 32591362753..736a9d61736 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.json +++ b/erpnext/crm/doctype/email_campaign/email_campaign.json @@ -52,7 +52,8 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Email Campaign For ", - "options": "\nLead\nContact" + "options": "\nLead\nContact", + "reqd": 1 }, { "fieldname": "recipient", @@ -69,7 +70,7 @@ "options": "User" } ], - "modified": "2019-07-12 13:47:37.261213", + "modified": "2019-11-11 17:18:47.342839", "modified_by": "Administrator", "module": "CRM", "name": "Email Campaign", diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 6ea24a5f204..3050d05a7c8 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -73,13 +73,13 @@ def send_mail(entry, email_campaign): email_template = frappe.get_doc("Email Template", entry.get("email_template")) sender = frappe.db.get_value("User", email_campaign.get("sender"), 'email') - context = { "doc":frappe.get_doc(email_campaign.email_campaign_for,email_campaign.recipient) } + context = {"doc": frappe.get_doc(email_campaign.email_campaign_for, email_campaign.recipient)} # send mail and link communication to document comm = make( doctype = "Email Campaign", name = email_campaign.name, subject = email_template.get("subject"), - content = frappe.render_template(email_template.get("response"),context), + content = frappe.render_template(email_template.get("response"), context), sender = sender, recipients = recipient, communication_medium = "Email", From 8b2223ae5f163e4adec42429efc209717968a15c Mon Sep 17 00:00:00 2001 From: ci2014 Date: Mon, 11 Nov 2019 12:57:48 +0100 Subject: [PATCH 143/679] Move add_custom_button for Gantt and Kanban Board (#19193) Move add_custom_button for Gantt and Kanban Board to set_buttons, because in onload it is not working. --- erpnext/projects/doctype/project/project.js | 34 ++++++++++----------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 4a03a58f80a..25c97d1fb84 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -53,23 +53,6 @@ frappe.ui.form.on("Project", { filters: filters }; }); - - if (frappe.model.can_read("Task")) { - frm.add_custom_button(__("Gantt Chart"), function () { - frappe.route_options = { - "project": frm.doc.name - }; - frappe.set_route("List", "Task", "Gantt"); - }); - - frm.add_custom_button(__("Kanban Board"), () => { - frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', { - project: frm.doc.project_name - }).then(() => { - frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name); - }); - }); - } }, refresh: function (frm) { @@ -97,6 +80,23 @@ 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 = { + "project": frm.doc.name + }; + frappe.set_route("List", "Task", "Gantt"); + }); + + frm.add_custom_button(__("Kanban Board"), () => { + frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', { + project: frm.doc.project_name + }).then(() => { + frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name); + }); + }); + } }, create_duplicate: function(frm) { From a227b9a9a657ea3db7d3851f91e37dd74827a706 Mon Sep 17 00:00:00 2001 From: Mitchy25 <42224026+Mitchy25@users.noreply.github.com> Date: Tue, 12 Nov 2019 00:59:53 +1300 Subject: [PATCH 144/679] Fix Bank Reconcilaition for Payment Entries (#19190) * Fix Bank Reconciliation with Payment Entries * Update bank_reconciliation.js --- .../page/bank_reconciliation/bank_reconciliation.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js index 854b973beae..efc76f9158b 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js @@ -529,9 +529,16 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { frappe.db.get_doc(dt, event.value) .then(doc => { let displayed_docs = [] + let payment = [] if (dt === "Payment Entry") { payment.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency; payment.doctype = dt + payment.posting_date = doc.posting_date; + payment.party = doc.party; + payment.reference_no = doc.reference_no; + payment.reference_date = doc.reference_date; + payment.paid_amount = doc.paid_amount; + payment.name = doc.name; displayed_docs.push(payment); } else if (dt === "Journal Entry") { doc.accounts.forEach(payment => { @@ -564,8 +571,8 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper; details_wrapper.append(frappe.render_template("linked_payment_header")); - displayed_docs.forEach(values => { - details_wrapper.append(frappe.render_template("linked_payment_row", values)); + displayed_docs.forEach(payment => { + details_wrapper.append(frappe.render_template("linked_payment_row", payment)); }) }) } From 001ee5ee1b2608aa4d2064bdb234399d01dbe79c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Mon, 11 Nov 2019 17:43:48 +0530 Subject: [PATCH 145/679] fix: dictionary changed size during iteration (#19546) --- erpnext/stock/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index 2ac0bae6dac..d7629176a51 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -271,6 +271,7 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto 'fieldtype': 'Currency' if d.get("convertible") == 'rate' else 'Float' }) + update_dict_values = [] for row_idx, row in enumerate(result): data = row.items() if is_dict_obj else enumerate(row) for key, value in data: @@ -286,7 +287,11 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto row.insert(key+1, new_value) else: new_key = "{0}_{1}".format(key, frappe.scrub(include_uom)) - row[new_key] = new_value + update_dict_values.append([row, new_key, new_value]) + + for data in update_dict_values: + row, key, value = data + row[key] = value def get_available_serial_nos(item_code, warehouse): return frappe.get_all("Serial No", filters = {'item_code': item_code, From 39152f935c901385267ce033bd002cadf2f472db Mon Sep 17 00:00:00 2001 From: Diksha Date: Mon, 11 Nov 2019 19:22:51 +0530 Subject: [PATCH 146/679] fix(employee): show only active employees in the error display while marking a reporting to employee as left --- erpnext/hr/doctype/employee/employee.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 3fc330e2d26..703ec06f83b 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -167,10 +167,11 @@ class Employee(NestedSet): def validate_status(self): if self.status == 'Left': reports_to = frappe.db.get_all('Employee', - filters={'reports_to': self.name} + filters={'reports_to': self.name, 'status': "Active"}, + fields=['name','employee_name'] ) if reports_to: - link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name) for employee in reports_to] + link_to_employees = [frappe.utils.get_link_to_form('Employee', employee.name, label=employee.employee_name) for employee in reports_to] throw(_("Employee status cannot be set to 'Left' as following employees are currently reporting to this employee: ") + ', '.join(link_to_employees), EmployeeLeftValidationError) if not self.relieving_date: From 929676fceb90d2d016f459e4ad88d2079606c597 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 12 Nov 2019 12:00:30 +0530 Subject: [PATCH 147/679] 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 3cc3b57926a7062a9ae1c3bd3a28041542ee0140 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 12 Nov 2019 14:32:50 +0530 Subject: [PATCH 148/679] fix: Expense claim paid through employee advance getting fetched as outstanding in Payment Entry (#19427) * fix: Expense claim paid through employee advance getting fetched as outstanding in Payment Entry * fix: Codacy * fix: Minor UX fixes * fix: Also credit payable amount in case of advance payment * fix: Against voucher in GL enrty --- .../doctype/payment_entry/payment_entry.js | 2 +- .../hr/doctype/employee/employee_dashboard.py | 2 +- .../doctype/expense_claim/expense_claim.json | 869 +++++++++--------- .../hr/doctype/expense_claim/expense_claim.py | 30 +- 4 files changed, 464 insertions(+), 439 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 1e0b1bcbf16..adf47ed2764 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -554,7 +554,7 @@ frappe.ui.form.on('Payment Entry', { frappe.flags.allocate_payment_amount = true; frm.events.validate_filters_data(frm, filters); frm.events.get_outstanding_documents(frm, filters); - }, __("Filters"), __("Get Outstanding Invoices")); + }, __("Filters"), __("Get Outstanding Documents")); }, validate_filters_data: function(frm, filters) { diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 162b697ac86..11ad83ba37e 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -21,7 +21,7 @@ def get_data(): }, { 'label': _('Expense'), - 'items': ['Expense Claim', 'Travel Request'] + 'items': ['Expense Claim', 'Travel Request', 'Employee Advance'] }, { 'label': _('Benefit'), diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 4e2778f48d7..5c2f4901713 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -1,435 +1,436 @@ { - "allow_import": 1, - "autoname": "naming_series:", - "creation": "2013-01-10 16:34:14", - "doctype": "DocType", - "document_type": "Setup", - "engine": "InnoDB", - "field_order": [ - "naming_series", - "employee", - "employee_name", - "department", - "column_break_5", - "expense_approver", - "approval_status", - "is_paid", - "expense_details", - "expenses", - "sb1", - "taxes", - "transactions_section", - "total_sanctioned_amount", - "total_taxes_and_charges", - "total_advance_amount", - "column_break_17", - "grand_total", - "total_claimed_amount", - "total_amount_reimbursed", - "section_break_16", - "posting_date", - "vehicle_log", - "task", - "cb1", - "remark", - "title", - "email_id", - "accounting_details", - "company", - "mode_of_payment", - "clearance_date", - "column_break_24", - "payable_account", - "accounting_dimensions_section", - "project", - "dimension_col_break", - "cost_center", - "more_details", - "status", - "amended_from", - "advance_payments", - "advances" - ], - "fields": [ - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "no_copy": 1, - "options": "HR-EXP-.YYYY.-", - "print_hide": 1, - "reqd": 1, - "set_only_once": 1 - }, - { - "fieldname": "employee", - "fieldtype": "Link", - "in_global_search": 1, - "label": "From Employee", - "oldfieldname": "employee", - "oldfieldtype": "Link", - "options": "Employee", - "reqd": 1, - "search_index": 1 - }, - { - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Employee Name", - "oldfieldname": "employee_name", - "oldfieldtype": "Data", - "read_only": 1, - "width": "150px" - }, - { - "fetch_from": "employee.department", - "fieldname": "department", - "fieldtype": "Link", - "label": "Department", - "options": "Department", - "read_only": 1 - }, - { - "fieldname": "column_break_5", - "fieldtype": "Column Break" - }, - { - "fieldname": "expense_approver", - "fieldtype": "Link", - "label": "Expense Approver", - "options": "User" - }, - { - "default": "Draft", - "fieldname": "approval_status", - "fieldtype": "Select", - "label": "Approval Status", - "no_copy": 1, - "options": "Draft\nApproved\nRejected", - "search_index": 1 - }, - { - "fieldname": "total_claimed_amount", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Total Claimed Amount", - "no_copy": 1, - "oldfieldname": "total_claimed_amount", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "width": "160px" - }, - { - "fieldname": "total_sanctioned_amount", - "fieldtype": "Currency", - "label": "Total Sanctioned Amount", - "no_copy": 1, - "oldfieldname": "total_sanctioned_amount", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "read_only": 1, - "width": "160px" - }, - { - "default": "0", - "depends_on": "eval:(doc.docstatus==0 || doc.is_paid)", - "fieldname": "is_paid", - "fieldtype": "Check", - "label": "Is Paid" - }, - { - "fieldname": "expense_details", - "fieldtype": "Section Break", - "oldfieldtype": "Section Break" - }, - { - "fieldname": "expenses", - "fieldtype": "Table", - "label": "Expenses", - "oldfieldname": "expense_voucher_details", - "oldfieldtype": "Table", - "options": "Expense Claim Detail", - "reqd": 1 - }, - { - "fieldname": "sb1", - "fieldtype": "Section Break", - "options": "Simple" - }, - { - "default": "Today", - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Posting Date", - "oldfieldname": "posting_date", - "oldfieldtype": "Date", - "reqd": 1 - }, - { - "fieldname": "vehicle_log", - "fieldtype": "Link", - "label": "Vehicle Log", - "options": "Vehicle Log", - "read_only": 1 - }, - { - "fieldname": "project", - "fieldtype": "Link", - "label": "Project", - "options": "Project" - }, - { - "fieldname": "task", - "fieldtype": "Link", - "label": "Task", - "options": "Task", - "remember_last_selected_value": 1 - }, - { - "fieldname": "cb1", - "fieldtype": "Column Break" - }, - { - "fieldname": "total_amount_reimbursed", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Total Amount Reimbursed", - "no_copy": 1, - "options": "Company:company:default_currency", - "read_only": 1 - }, - { - "fieldname": "remark", - "fieldtype": "Small Text", - "label": "Remark", - "no_copy": 1, - "oldfieldname": "remark", - "oldfieldtype": "Small Text" - }, - { - "allow_on_submit": 1, - "default": "{employee_name}", - "fieldname": "title", - "fieldtype": "Data", - "hidden": 1, - "label": "Title", - "no_copy": 1 - }, - { - "fieldname": "email_id", - "fieldtype": "Data", - "hidden": 1, - "label": "Employees Email Id", - "oldfieldname": "email_id", - "oldfieldtype": "Data", - "print_hide": 1 - }, - { - "fieldname": "accounting_details", - "fieldtype": "Section Break", - "label": "Accounting Details" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "oldfieldname": "company", - "oldfieldtype": "Link", - "options": "Company", - "remember_last_selected_value": 1, - "reqd": 1 - }, - { - "depends_on": "is_paid", - "fieldname": "mode_of_payment", - "fieldtype": "Link", - "label": "Mode of Payment", - "options": "Mode of Payment" - }, - { - "fieldname": "clearance_date", - "fieldtype": "Date", - "label": "Clearance Date" - }, - { - "fieldname": "column_break_24", - "fieldtype": "Column Break" - }, - { - "fieldname": "payable_account", - "fieldtype": "Link", - "label": "Payable Account", - "options": "Account" - }, - { - "fieldname": "cost_center", - "fieldtype": "Link", - "label": "Cost Center", - "options": "Cost Center" - }, - { - "collapsible": 1, - "fieldname": "more_details", - "fieldtype": "Section Break", - "label": "More Details" - }, - { - "default": "Draft", - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "label": "Status", - "no_copy": 1, - "options": "Draft\nPaid\nUnpaid\nRejected\nSubmitted\nCancelled", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Amended From", - "no_copy": 1, - "oldfieldname": "amended_from", - "oldfieldtype": "Data", - "options": "Expense Claim", - "print_hide": 1, - "read_only": 1, - "report_hide": 1, - "width": "160px" - }, - { - "fieldname": "advance_payments", - "fieldtype": "Section Break", - "label": "Advance Payments" - }, - { - "fieldname": "advances", - "fieldtype": "Table", - "label": "Advances", - "options": "Expense Claim Advance" - }, - { - "fieldname": "total_advance_amount", - "fieldtype": "Currency", - "label": "Total Advance Amount", - "options": "Company:company:default_currency", - "read_only": 1 - }, - { - "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", - "label": "Accounting Dimensions" - }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - }, - { - "fieldname": "taxes", - "fieldtype": "Table", - "label": "Expense Taxes and Charges", - "options": "Expense Taxes and Charges" - }, - { - "fieldname": "section_break_16", - "fieldtype": "Section Break" - }, - { - "fieldname": "transactions_section", - "fieldtype": "Section Break" - }, - { - "fieldname": "grand_total", - "fieldtype": "Currency", - "in_list_view": 1, - "label": "Grand Total", - "options": "Company:company:default_currency", - "read_only": 1 - }, - { - "fieldname": "column_break_17", - "fieldtype": "Column Break" - }, - { - "fieldname": "total_taxes_and_charges", - "fieldtype": "Currency", - "label": "Total Taxes and Charges", - "options": "Company:company:default_currency", - "read_only": 1 - } - ], - "icon": "fa fa-money", - "idx": 1, - "is_submittable": 1, - "modified": "2019-06-26 18:05:52.530462", - "modified_by": "Administrator", - "module": "HR", - "name": "Expense Claim", - "name_case": "Title Case", - "owner": "harshada@webnotestech.com", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "create": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "share": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Expense Approver", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "search_fields": "employee,employee_name", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "timeline_field": "employee", - "title_field": "title" - } \ No newline at end of file + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2013-01-10 16:34:14", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "naming_series", + "employee", + "employee_name", + "department", + "column_break_5", + "expense_approver", + "approval_status", + "is_paid", + "expense_details", + "expenses", + "sb1", + "taxes", + "transactions_section", + "total_sanctioned_amount", + "total_taxes_and_charges", + "total_advance_amount", + "column_break_17", + "grand_total", + "total_claimed_amount", + "total_amount_reimbursed", + "section_break_16", + "posting_date", + "vehicle_log", + "task", + "cb1", + "remark", + "title", + "email_id", + "accounting_details", + "company", + "mode_of_payment", + "clearance_date", + "column_break_24", + "payable_account", + "accounting_dimensions_section", + "project", + "dimension_col_break", + "cost_center", + "more_details", + "status", + "amended_from", + "advance_payments", + "advances" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "HR-EXP-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "employee", + "fieldtype": "Link", + "in_global_search": 1, + "label": "From Employee", + "oldfieldname": "employee", + "oldfieldtype": "Link", + "options": "Employee", + "reqd": 1, + "search_index": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Employee Name", + "oldfieldname": "employee_name", + "oldfieldtype": "Data", + "read_only": 1, + "width": "150px" + }, + { + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "expense_approver", + "fieldtype": "Link", + "label": "Expense Approver", + "options": "User" + }, + { + "default": "Draft", + "fieldname": "approval_status", + "fieldtype": "Select", + "label": "Approval Status", + "no_copy": 1, + "options": "Draft\nApproved\nRejected", + "search_index": 1 + }, + { + "fieldname": "total_claimed_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Claimed Amount", + "no_copy": 1, + "oldfieldname": "total_claimed_amount", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "read_only": 1, + "width": "160px" + }, + { + "fieldname": "total_sanctioned_amount", + "fieldtype": "Currency", + "label": "Total Sanctioned Amount", + "no_copy": 1, + "oldfieldname": "total_sanctioned_amount", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "read_only": 1, + "width": "160px" + }, + { + "default": "0", + "depends_on": "eval:(doc.docstatus==0 || doc.is_paid)", + "fieldname": "is_paid", + "fieldtype": "Check", + "label": "Is Paid" + }, + { + "fieldname": "expense_details", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break" + }, + { + "fieldname": "expenses", + "fieldtype": "Table", + "label": "Expenses", + "oldfieldname": "expense_voucher_details", + "oldfieldtype": "Table", + "options": "Expense Claim Detail", + "reqd": 1 + }, + { + "fieldname": "sb1", + "fieldtype": "Section Break", + "options": "Simple" + }, + { + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date", + "oldfieldname": "posting_date", + "oldfieldtype": "Date", + "reqd": 1 + }, + { + "fieldname": "vehicle_log", + "fieldtype": "Link", + "label": "Vehicle Log", + "options": "Vehicle Log", + "read_only": 1 + }, + { + "fieldname": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, + { + "fieldname": "task", + "fieldtype": "Link", + "label": "Task", + "options": "Task", + "remember_last_selected_value": 1 + }, + { + "fieldname": "cb1", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_amount_reimbursed", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Amount Reimbursed", + "no_copy": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "remark", + "fieldtype": "Small Text", + "label": "Remark", + "no_copy": 1, + "oldfieldname": "remark", + "oldfieldtype": "Small Text" + }, + { + "allow_on_submit": 1, + "default": "{employee_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1 + }, + { + "fieldname": "email_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Employees Email Id", + "oldfieldname": "email_id", + "oldfieldtype": "Data", + "print_hide": 1 + }, + { + "fieldname": "accounting_details", + "fieldtype": "Section Break", + "label": "Accounting Details" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "depends_on": "is_paid", + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, + { + "fieldname": "clearance_date", + "fieldtype": "Date", + "label": "Clearance Date" + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break" + }, + { + "fieldname": "payable_account", + "fieldtype": "Link", + "label": "Payable Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "collapsible": 1, + "fieldname": "more_details", + "fieldtype": "Section Break", + "label": "More Details" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nPaid\nUnpaid\nRejected\nSubmitted\nCancelled", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "oldfieldname": "amended_from", + "oldfieldtype": "Data", + "options": "Expense Claim", + "print_hide": 1, + "read_only": 1, + "report_hide": 1, + "width": "160px" + }, + { + "fieldname": "advance_payments", + "fieldtype": "Section Break", + "label": "Advance Payments" + }, + { + "fieldname": "advances", + "fieldtype": "Table", + "label": "Advances", + "options": "Expense Claim Advance" + }, + { + "fieldname": "total_advance_amount", + "fieldtype": "Currency", + "label": "Total Advance Amount", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Expense Taxes and Charges", + "options": "Expense Taxes and Charges" + }, + { + "fieldname": "section_break_16", + "fieldtype": "Section Break" + }, + { + "fieldname": "transactions_section", + "fieldtype": "Section Break" + }, + { + "fieldname": "grand_total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Grand Total", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "column_break_17", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_taxes_and_charges", + "fieldtype": "Currency", + "label": "Total Taxes and Charges", + "options": "Company:company:default_currency", + "read_only": 1 + } + ], + "icon": "fa fa-money", + "idx": 1, + "is_submittable": 1, + "modified": "2019-11-08 14:13:08.964547", + "modified_by": "Administrator", + "module": "HR", + "name": "Expense Claim", + "name_case": "Title Case", + "owner": "harshada@webnotestech.com", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Expense Approver", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "search_fields": "employee,employee_name", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "timeline_field": "employee", + "title_field": "title" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index caeb2dd9466..f0036277c88 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -144,6 +144,33 @@ class ExpenseClaim(AccountsController): "against_voucher": self.name }) ) + + gl_entry.append( + self.get_gl_dict({ + "account": data.advance_account, + "debit": data.allocated_amount, + "debit_in_account_currency": data.allocated_amount, + "against": self.payable_account, + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": self.doctype, + "against_voucher": self.name + }) + ) + + gl_entry.append( + self.get_gl_dict({ + "account": self.payable_account, + "credit": data.allocated_amount, + "credit_in_account_currency": data.allocated_amount, + "against": data.advance_account, + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": "Employee Advance", + "against_voucher": data.employee_advance + }) + ) + self.add_tax_gl_entries(gl_entry) if self.is_paid and self.grand_total: @@ -192,9 +219,6 @@ class ExpenseClaim(AccountsController): if not self.cost_center: frappe.throw(_("Cost center is required to book an expense claim")) - if not self.payable_account: - frappe.throw(_("Please set default payable account for the company {0}").format(getlink("Company",self.company))) - if self.is_paid: if not self.mode_of_payment: frappe.throw(_("Mode of payment is required to make a payment").format(self.employee)) From 29a2e16f62e5a3927e3231b5428b26ddf69e5a71 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 12 Nov 2019 14:43:41 +0530 Subject: [PATCH 149/679] fix: Add Serial No. button not responding (#19550) --- erpnext/public/js/utils.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index ffc5e6ad365..6f43d9ef8c6 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -63,12 +63,15 @@ $.extend(erpnext, { let callback = ''; let on_close = ''; - if (grid_row.doc.serial_no) { - grid_row.doc.has_serial_no = true; - } - - me.show_serial_batch_selector(grid_row.frm, grid_row.doc, - callback, on_close, true); + frappe.model.get_value('Item', {'name':grid_row.doc.item_code}, 'has_serial_no', + (data) => { + if(data) { + grid_row.doc.has_serial_no = data.has_serial_no; + me.show_serial_batch_selector(grid_row.frm, grid_row.doc, + callback, on_close, true); + } + } + ); }); }, }); From 4fa61940097b9bbef78ec6bb54ccf77dfd5904cc Mon Sep 17 00:00:00 2001 From: MorezMartin Date: Tue, 12 Nov 2019 13:49:01 +0100 Subject: [PATCH 150/679] feat: [production_plan -> fetching item description] Fetch item description from Material Request or Sales Order (#19541) * Change packed item * Remove description field on update_packed_items * add possibility to modify description on packed items * Fetch description from Material Request or Sales Order in production plan, add the possibility to modify the description un production plan * sync with fork * Fetch description from Material Request or Sales Order in production plan, Add the possibility to modify description in production plan * code cleaning syncing fork * code cleaning syncing fork * code cleaning syncing fork * code cleaning syncing fork * rewied and add item_details.description in case of blank field --- .../production_plan/production_plan.py | 35 ++++++++++--------- .../production_plan/test_production_plan.py | 2 -- 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 0f49f73dfb9..5d2696933bc 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -99,7 +99,7 @@ class ProductionPlan(Document): self.get_mr_items() def get_so_items(self): - so_list = [d.sales_order for d in self.get("sales_orders", []) if d.sales_order] + so_list = [d.sales_order for d in self.sales_orders if d.sales_order] if not so_list: msgprint(_("Please enter Sales Orders in the above table")) return [] @@ -109,7 +109,7 @@ class ProductionPlan(Document): item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code)) items = frappe.db.sql("""select distinct parent, item_code, warehouse, - (qty - work_order_qty) * conversion_factor as pending_qty, name + (qty - work_order_qty) * conversion_factor as pending_qty, description, name from `tabSales Order Item` so_item where parent in (%s) and docstatus = 1 and qty > work_order_qty and exists (select name from `tabBOM` bom where bom.item=so_item.item_code @@ -121,7 +121,7 @@ class ProductionPlan(Document): packed_items = frappe.db.sql("""select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty) - as pending_qty, pi.parent_item, so_item.name + as pending_qty, pi.parent_item, pi.description, so_item.name from `tabSales Order Item` so_item, `tabPacked Item` pi where so_item.parent = pi.parent and so_item.docstatus = 1 and pi.parent_item = so_item.item_code @@ -134,7 +134,7 @@ class ProductionPlan(Document): self.calculate_total_planned_qty() def get_mr_items(self): - mr_list = [d.material_request for d in self.get("material_requests", []) if d.material_request] + mr_list = [d.material_request for d in self.material_requests if d.material_request] if not mr_list: msgprint(_("Please enter Material Requests in the above table")) return [] @@ -143,7 +143,7 @@ class ProductionPlan(Document): if self.item_code: item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code)) - items = frappe.db.sql("""select distinct parent, name, item_code, warehouse, + items = frappe.db.sql("""select distinct parent, name, item_code, warehouse, description, (qty - ordered_qty) as pending_qty from `tabMaterial Request Item` mr_item where parent in (%s) and docstatus = 1 and qty > ordered_qty @@ -162,7 +162,7 @@ class ProductionPlan(Document): 'include_exploded_items': 1, 'warehouse': data.warehouse, 'item_code': data.item_code, - 'description': item_details and item_details.description or '', + 'description': data.description or item_details.description, 'stock_uom': item_details and item_details.stock_uom or '', 'bom_no': item_details and item_details.bom_no or '', 'planned_qty': data.pending_qty, @@ -174,10 +174,12 @@ class ProductionPlan(Document): if self.get_items_from == "Sales Order": pi.sales_order = data.parent pi.sales_order_item = data.name + pi.description = data.description elif self.get_items_from == "Material Request": pi.material_request = data.parent pi.material_request_item = data.name + pi.description = data.description def calculate_total_planned_qty(self): self.total_planned_qty = 0 @@ -195,7 +197,6 @@ class ProductionPlan(Document): for data in self.po_items: if data.name == production_plan_item: data.produced_qty = produced_qty - data.pending_qty = data.planned_qty - data.produced_qty data.db_update() self.calculate_total_produced_qty() @@ -302,6 +303,7 @@ class ProductionPlan(Document): wo_list.extend(work_orders) frappe.flags.mute_messages = False + if wo_list: wo_list = ["""%s""" % \ (p, p) for p in wo_list] @@ -309,16 +311,15 @@ class ProductionPlan(Document): else : msgprint(_("No Work Orders created")) - def make_work_order_for_sub_assembly_items(self, item): work_orders = [] bom_data = {} - get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty")) + get_sub_assembly_items(item.get("bom_no"), bom_data) for key, data in bom_data.items(): data.update({ - 'qty': data.get("stock_qty"), + 'qty': data.get("stock_qty") * item.get("qty"), 'production_plan': self.name, 'company': self.company, 'fg_warehouse': item.get("fg_warehouse"), @@ -561,7 +562,7 @@ def get_sales_orders(self): item_filter += " and so_item.item_code = %(item)s" open_so = frappe.db.sql(""" - select distinct so.name, so.transaction_date, so.customer, so.base_grand_total as grand_total + select distinct so.name, so.transaction_date, so.customer, so.base_grand_total from `tabSales Order` so, `tabSales Order Item` so_item where so_item.parent = so.name and so.docstatus = 1 and so.status not in ("Stopped", "Closed") @@ -625,7 +626,7 @@ def get_items_for_material_requests(doc, ignore_existing_ordered_qty=None): for data in po_items: planned_qty = data.get('required_qty') or data.get('planned_qty') ignore_existing_ordered_qty = data.get('ignore_existing_ordered_qty') or ignore_existing_ordered_qty - warehouse = warehouse or data.get("warehouse") + warehouse = data.get("warehouse") or warehouse item_details = {} if data.get("bom") or data.get("bom_no"): @@ -708,11 +709,11 @@ def get_item_data(item_code): return { "bom_no": item_details.get("bom_no"), - "stock_uom": item_details.get("stock_uom"), - "description": item_details.get("description") + "stock_uom": item_details.get("stock_uom") +# "description": item_details.get("description") } -def get_sub_assembly_items(bom_no, bom_data, qty): +def get_sub_assembly_items(bom_no, bom_data): data = get_children('BOM', parent = bom_no) for d in data: if d.expandable: @@ -729,6 +730,6 @@ def get_sub_assembly_items(bom_no, bom_data, qty): }) bom_item = bom_data.get(key) - bom_item["stock_qty"] += ((d.stock_qty * qty) / d.parent_bom_qty) + bom_item["stock_qty"] += d.stock_qty - get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"]) + get_sub_assembly_items(bom_item.get("bom_no"), bom_data) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index 44796417d42..f70c9cc43fc 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -11,11 +11,9 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import get_sa from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests -from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory class TestProductionPlan(unittest.TestCase): def setUp(self): - set_perpetual_inventory(0) for item in ['Test Production Item 1', 'Subassembly Item 1', 'Raw Material Item 1', 'Raw Material Item 2']: create_item(item, valuation_rate=100) From 010714757ce367be2df33172137452588409cd18 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 12 Nov 2019 18:20:07 +0530 Subject: [PATCH 151/679] fix: '<' not supported between instances of 'str' and 'NoneType' (#19553) --- erpnext/projects/doctype/timesheet/timesheet.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index bc88250c8aa..c4481c9aa0e 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -188,6 +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 (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 (args.to_time > time_log.from_time and args.to_time < time_log.to_time) or From d00c59830ef476e6ba2c5939f8aeafdc9f6805de Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 12 Nov 2019 19:17:43 +0530 Subject: [PATCH 152/679] feat: Disable CWIP Accounting checkbox added in Company and Asset Category (#19262) * feat: Disable CWIP Accounting checkbox added in Company and Asset Category Asset Settings is removed completely Disable CWIP Accounting checkbox will give priority to Asset Category * fix: Changed checkbox name to 'Enable Capital Work in Progress Accounting' - checkbox will be disabled by default - Enabling it in Company will globally enable it - When globally disabled , it's value on the asset category will be considered * chore: Added patch to set pre-existing CWIP checkbox value into new checkbox * fix(test): Asset * fix: Asset Test and Patch * fix(test): Opening Invoice Creation Tool * Update asset.py * fix: Patch and other fixes --- .../purchase_invoice/purchase_invoice.py | 29 +- erpnext/accounts/general_ledger.py | 12 +- erpnext/assets/doctype/asset/asset.js | 17 +- erpnext/assets/doctype/asset/asset.json | 994 +++++++++--------- erpnext/assets/doctype/asset/asset.py | 21 +- erpnext/assets/doctype/asset/test_asset.py | 41 +- .../asset_category/asset_category.json | 363 ++----- .../doctype/asset_category/asset_category.py | 13 + .../assets/doctype/asset_settings/__init__.py | 0 .../doctype/asset_settings/asset_settings.js | 5 - .../asset_settings/asset_settings.json | 148 --- .../doctype/asset_settings/asset_settings.py | 9 - .../asset_settings/test_asset_settings.js | 23 - .../asset_settings/test_asset_settings.py | 9 - erpnext/config/assets.py | 4 - erpnext/patches.txt | 1 + .../set_cwip_and_delete_asset_settings.py | 22 + erpnext/setup/doctype/company/company.json | 21 +- .../purchase_receipt/purchase_receipt.py | 12 +- 19 files changed, 723 insertions(+), 1021 deletions(-) delete mode 100644 erpnext/assets/doctype/asset_settings/__init__.py delete mode 100644 erpnext/assets/doctype/asset_settings/asset_settings.js delete mode 100644 erpnext/assets/doctype/asset_settings/asset_settings.json delete mode 100644 erpnext/assets/doctype/asset_settings/asset_settings.py delete mode 100644 erpnext/assets/doctype/asset_settings/test_asset_settings.js delete mode 100644 erpnext/assets/doctype/asset_settings/test_asset_settings.py create mode 100644 erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 9c1a9ece779..f1c490e2cdb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -18,7 +18,7 @@ from erpnext.accounts.general_ledger import make_gl_entries, merge_similar_entri from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.accounts.general_ledger import get_round_off_account_and_cost_center -from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_disabled +from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from frappe.model.mapper import get_mapped_doc from six import iteritems from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\ @@ -226,6 +226,8 @@ class PurchaseInvoice(BuyingController): # in case of auto inventory accounting, # expense account is always "Stock Received But Not Billed" for a stock item # except epening entry, drop-ship entry and fixed asset items + if item.item_code: + asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") if auto_accounting_for_stock and item.item_code in stock_items \ and self.is_opening == 'No' and not item.is_fixed_asset \ @@ -236,7 +238,8 @@ 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 is_cwip_accounting_disabled(): + + elif item.is_fixed_asset and not is_cwip_accounting_enabled(self.company, asset_category): if not item.asset: frappe.throw(_("Row {0}: asset is required for item {1}") .format(item.idx, item.item_code)) @@ -392,7 +395,8 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) - if not is_cwip_accounting_disabled(): + + if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) self.make_tax_gl_entries(gl_entries) @@ -405,6 +409,15 @@ class PurchaseInvoice(BuyingController): return gl_entries + def check_asset_cwip_enabled(self): + # Check if there exists any item with cwip accounting enabled in it's asset category + 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): + return 1 + return 0 + def make_supplier_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total @@ -448,6 +461,8 @@ class PurchaseInvoice(BuyingController): for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) + if item.item_code: + asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items: # warehouse account @@ -490,8 +505,9 @@ class PurchaseInvoice(BuyingController): "remarks": self.get("remarks") or _("Accounting Entry for Stock"), "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 is_cwip_accounting_disabled()): + elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(self.company, + asset_category)): expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) @@ -532,7 +548,10 @@ class PurchaseInvoice(BuyingController): def get_asset_gl_entry(self, gl_entries): for item in self.get("items"): - if item.is_fixed_asset: + if item.item_code and item.is_fixed_asset : + asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") + + if item.is_fixed_asset and is_cwip_accounting_enabled(self.company, asset_category) : eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 43d9ad6435a..d4dac72601d 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -168,14 +168,20 @@ def validate_account_for_perpetual_inventory(gl_map): StockValueAndAccountBalanceOutOfSync) def validate_cwip_accounts(gl_map): - if not cint(frappe.db.get_value("Asset Settings", None, "disable_cwip_accounting")) \ - and gl_map[0].voucher_type == "Journal Entry": + 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")]) + + 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 where account_type = 'Capital Work in Progress' and is_group=0""")] for entry in gl_map: if entry.account in cwip_accounts: - frappe.throw(_("Account: {0} is capital Work in progress and can not be updated by Journal Entry").format(entry.account)) + frappe.throw( + _("Account: {0} is capital Work in progress and can not be updated by Journal Entry").format(entry.account)) def round_off_debit_credit(gl_map): precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index c5cad738018..c7390a2ef11 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -203,7 +203,7 @@ frappe.ui.form.on('Asset', { }, opening_accumulated_depreciation: function(frm) { - erpnext.asset.set_accululated_depreciation(frm); + erpnext.asset.set_accumulated_depreciation(frm); }, make_schedules_editable: function(frm) { @@ -282,17 +282,6 @@ frappe.ui.form.on('Asset', { }, calculate_depreciation: function(frm) { - frappe.db.get_value("Asset Settings", {'name':"Asset Settings"}, 'schedule_based_on_fiscal_year', (data) => { - if (data.schedule_based_on_fiscal_year == 1) { - frm.set_df_property("depreciation_method", "options", "\nStraight Line\nManual"); - frm.toggle_reqd("available_for_use_date", true); - frm.toggle_display("frequency_of_depreciation", false); - frappe.db.get_value("Fiscal Year", {'name': frappe.sys_defaults.fiscal_year}, "year_end_date", (data) => { - frm.set_value("next_depreciation_date", data.year_end_date); - }) - } - }) - frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); }, @@ -371,12 +360,12 @@ frappe.ui.form.on('Depreciation Schedule', { }, depreciation_amount: function(frm, cdt, cdn) { - erpnext.asset.set_accululated_depreciation(frm); + erpnext.asset.set_accumulated_depreciation(frm); } }) -erpnext.asset.set_accululated_depreciation = function(frm) { +erpnext.asset.set_accumulated_depreciation = function(frm) { if(frm.doc.depreciation_method != "Manual") return; var accumulated_depreciation = flt(frm.doc.opening_accumulated_depreciation); diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index c60ec5ec3f9..8fda330bb7c 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -1,497 +1,499 @@ { - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "creation": "2016-03-01 17:01:27.920130", - "doctype": "DocType", - "document_type": "Document", - "field_order": [ - "naming_series", - "asset_name", - "item_code", - "item_name", - "asset_category", - "asset_owner", - "asset_owner_company", - "supplier", - "customer", - "image", - "column_break_3", - "company", - "location", - "custodian", - "department", - "purchase_date", - "disposal_date", - "journal_entry_for_scrap", - "accounting_dimensions_section", - "cost_center", - "dimension_col_break", - "section_break_5", - "gross_purchase_amount", - "available_for_use_date", - "column_break_18", - "calculate_depreciation", - "is_existing_asset", - "opening_accumulated_depreciation", - "number_of_depreciations_booked", - "section_break_23", - "finance_books", - "section_break_33", - "depreciation_method", - "value_after_depreciation", - "total_number_of_depreciations", - "column_break_24", - "frequency_of_depreciation", - "next_depreciation_date", - "section_break_14", - "schedules", - "insurance_details", - "policy_number", - "insurer", - "insured_value", - "column_break_48", - "insurance_start_date", - "insurance_end_date", - "comprehensive_insurance", - "section_break_31", - "maintenance_required", - "other_details", - "status", - "booked_fixed_asset", - "column_break_51", - "purchase_receipt", - "purchase_receipt_amount", - "purchase_invoice", - "default_finance_book", - "amended_from" - ], - "fields": [ - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "options": "ACC-ASS-.YYYY.-" - }, - { - "fieldname": "asset_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Asset Name", - "reqd": 1 - }, - { - "fieldname": "item_code", - "fieldtype": "Link", - "in_standard_filter": 1, - "label": "Item Code", - "options": "Item", - "reqd": 1 - }, - { - "fetch_from": "item_code.item_name", - "fieldname": "item_name", - "fieldtype": "Read Only", - "label": "Item Name" - }, - { - "fetch_from": "item_code.asset_category", - "fieldname": "asset_category", - "fieldtype": "Link", - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Asset Category", - "options": "Asset Category", - "read_only": 1 - }, - { - "fieldname": "asset_owner", - "fieldtype": "Select", - "label": "Asset Owner", - "options": "\nCompany\nSupplier\nCustomer" - }, - { - "depends_on": "eval:doc.asset_owner == \"Company\"", - "fieldname": "asset_owner_company", - "fieldtype": "Link", - "label": "Asset Owner Company", - "options": "Company" - }, - { - "depends_on": "eval:doc.asset_owner == \"Supplier\"", - "fieldname": "supplier", - "fieldtype": "Link", - "label": "Supplier", - "options": "Supplier" - }, - { - "depends_on": "eval:doc.asset_owner == \"Customer\"", - "fieldname": "customer", - "fieldtype": "Link", - "label": "Customer", - "options": "Customer" - }, - { - "allow_on_submit": 1, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "label": "Image", - "no_copy": 1, - "print_hide": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "options": "Company", - "remember_last_selected_value": 1, - "reqd": 1 - }, - { - "fieldname": "location", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Location", - "options": "Location", - "reqd": 1 - }, - { - "fieldname": "custodian", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Custodian", - "options": "Employee" - }, - { - "fieldname": "cost_center", - "fieldtype": "Link", - "label": "Cost Center", - "options": "Cost Center" - }, - { - "fieldname": "department", - "fieldtype": "Link", - "label": "Department", - "options": "Department" - }, - { - "fieldname": "purchase_date", - "fieldtype": "Date", - "label": "Purchase Date", - "reqd": 1 - }, - { - "fieldname": "disposal_date", - "fieldtype": "Date", - "label": "Disposal Date", - "read_only": 1 - }, - { - "fieldname": "journal_entry_for_scrap", - "fieldtype": "Link", - "label": "Journal Entry for Scrap", - "no_copy": 1, - "options": "Journal Entry", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "section_break_5", - "fieldtype": "Section Break" - }, - { - "fieldname": "gross_purchase_amount", - "fieldtype": "Currency", - "label": "Gross Purchase Amount", - "options": "Company:company:default_currency", - "reqd": 1 - }, - { - "fieldname": "available_for_use_date", - "fieldtype": "Date", - "label": "Available-for-use Date" - }, - { - "fieldname": "column_break_18", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "calculate_depreciation", - "fieldtype": "Check", - "label": "Calculate Depreciation" - }, - { - "default": "0", - "fieldname": "is_existing_asset", - "fieldtype": "Check", - "label": "Is Existing Asset" - }, - { - "depends_on": "is_existing_asset", - "fieldname": "opening_accumulated_depreciation", - "fieldtype": "Currency", - "label": "Opening Accumulated Depreciation", - "no_copy": 1, - "options": "Company:company:default_currency" - }, - { - "depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)", - "fieldname": "number_of_depreciations_booked", - "fieldtype": "Int", - "label": "Number of Depreciations Booked", - "no_copy": 1 - }, - { - "depends_on": "calculate_depreciation", - "fieldname": "section_break_23", - "fieldtype": "Section Break", - "label": "Depreciation" - }, - { - "fieldname": "finance_books", - "fieldtype": "Table", - "label": "Finance Books", - "options": "Asset Finance Book" - }, - { - "fieldname": "section_break_33", - "fieldtype": "Section Break", - "hidden": 1 - }, - { - "fieldname": "depreciation_method", - "fieldtype": "Select", - "label": "Depreciation Method", - "options": "\nStraight Line\nDouble Declining Balance\nManual" - }, - { - "fieldname": "value_after_depreciation", - "fieldtype": "Currency", - "hidden": 1, - "label": "Value After Depreciation", - "options": "Company:company:default_currency", - "read_only": 1 - }, - { - "fieldname": "total_number_of_depreciations", - "fieldtype": "Int", - "label": "Total Number of Depreciations" - }, - { - "fieldname": "column_break_24", - "fieldtype": "Column Break" - }, - { - "fieldname": "frequency_of_depreciation", - "fieldtype": "Int", - "label": "Frequency of Depreciation (Months)" - }, - { - "fieldname": "next_depreciation_date", - "fieldtype": "Date", - "label": "Next Depreciation Date", - "no_copy": 1 - }, - { - "depends_on": "calculate_depreciation", - "fieldname": "section_break_14", - "fieldtype": "Section Break", - "label": "Depreciation Schedule" - }, - { - "fieldname": "schedules", - "fieldtype": "Table", - "label": "Depreciation Schedules", - "no_copy": 1, - "options": "Depreciation Schedule" - }, - { - "collapsible": 1, - "fieldname": "insurance_details", - "fieldtype": "Section Break", - "label": "Insurance details" - }, - { - "fieldname": "policy_number", - "fieldtype": "Data", - "label": "Policy number" - }, - { - "fieldname": "insurer", - "fieldtype": "Data", - "label": "Insurer" - }, - { - "fieldname": "insured_value", - "fieldtype": "Data", - "label": "Insured value" - }, - { - "fieldname": "column_break_48", - "fieldtype": "Column Break" - }, - { - "fieldname": "insurance_start_date", - "fieldtype": "Date", - "label": "Insurance Start Date" - }, - { - "fieldname": "insurance_end_date", - "fieldtype": "Date", - "label": "Insurance End Date" - }, - { - "fieldname": "comprehensive_insurance", - "fieldtype": "Data", - "label": "Comprehensive Insurance" - }, - { - "fieldname": "section_break_31", - "fieldtype": "Section Break", - "label": "Maintenance" - }, - { - "allow_on_submit": 1, - "default": "0", - "description": "Check if Asset requires Preventive Maintenance or Calibration", - "fieldname": "maintenance_required", - "fieldtype": "Check", - "label": "Maintenance Required" - }, - { - "collapsible": 1, - "fieldname": "other_details", - "fieldtype": "Section Break", - "label": "Other Details" - }, - { - "allow_on_submit": 1, - "default": "Draft", - "fieldname": "status", - "fieldtype": "Select", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Status", - "no_copy": 1, - "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt", - "read_only": 1 - }, - { - "default": "0", - "fieldname": "booked_fixed_asset", - "fieldtype": "Check", - "label": "Booked Fixed Asset", - "no_copy": 1, - "read_only": 1 - }, - { - "fieldname": "column_break_51", - "fieldtype": "Column Break" - }, - { - "fieldname": "purchase_receipt", - "fieldtype": "Link", - "label": "Purchase Receipt", - "no_copy": 1, - "options": "Purchase Receipt", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "purchase_receipt_amount", - "fieldtype": "Currency", - "hidden": 1, - "label": "Purchase Receipt Amount", - "no_copy": 1, - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "purchase_invoice", - "fieldtype": "Link", - "label": "Purchase Invoice", - "no_copy": 1, - "options": "Purchase Invoice", - "read_only": 1 - }, - { - "fetch_from": "company.default_finance_book", - "fieldname": "default_finance_book", - "fieldtype": "Link", - "hidden": 1, - "label": "Default Finance Book", - "options": "Finance Book", - "read_only": 1 - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Asset", - "print_hide": 1, - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", - "label": "Accounting Dimensions" - }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - } - ], - "idx": 72, - "image_field": "image", - "is_submittable": 1, - "modified": "2019-05-25 22:26:19.786201", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "import": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Quality Manager", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "asset_name" - } \ No newline at end of file + "allow_import": 1, + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2016-03-01 17:01:27.920130", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "naming_series", + "asset_name", + "item_code", + "item_name", + "asset_category", + "asset_owner", + "asset_owner_company", + "supplier", + "customer", + "image", + "column_break_3", + "company", + "location", + "custodian", + "department", + "purchase_date", + "disposal_date", + "journal_entry_for_scrap", + "accounting_dimensions_section", + "cost_center", + "dimension_col_break", + "section_break_5", + "gross_purchase_amount", + "available_for_use_date", + "column_break_18", + "calculate_depreciation", + "is_existing_asset", + "opening_accumulated_depreciation", + "number_of_depreciations_booked", + "section_break_23", + "finance_books", + "section_break_33", + "depreciation_method", + "value_after_depreciation", + "total_number_of_depreciations", + "column_break_24", + "frequency_of_depreciation", + "next_depreciation_date", + "section_break_14", + "schedules", + "insurance_details", + "policy_number", + "insurer", + "insured_value", + "column_break_48", + "insurance_start_date", + "insurance_end_date", + "comprehensive_insurance", + "section_break_31", + "maintenance_required", + "other_details", + "status", + "booked_fixed_asset", + "column_break_51", + "purchase_receipt", + "purchase_receipt_amount", + "purchase_invoice", + "default_finance_book", + "amended_from" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "ACC-ASS-.YYYY.-" + }, + { + "fieldname": "asset_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Asset Name", + "reqd": 1 + }, + { + "fieldname": "item_code", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1 + }, + { + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Read Only", + "label": "Item Name" + }, + { + "fetch_from": "item_code.asset_category", + "fieldname": "asset_category", + "fieldtype": "Link", + "in_global_search": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Asset Category", + "options": "Asset Category", + "read_only": 1 + }, + { + "fieldname": "asset_owner", + "fieldtype": "Select", + "label": "Asset Owner", + "options": "\nCompany\nSupplier\nCustomer" + }, + { + "depends_on": "eval:doc.asset_owner == \"Company\"", + "fieldname": "asset_owner_company", + "fieldtype": "Link", + "label": "Asset Owner Company", + "options": "Company" + }, + { + "depends_on": "eval:doc.asset_owner == \"Supplier\"", + "fieldname": "supplier", + "fieldtype": "Link", + "label": "Supplier", + "options": "Supplier" + }, + { + "depends_on": "eval:doc.asset_owner == \"Customer\"", + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "allow_on_submit": 1, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, + { + "fieldname": "location", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Location", + "options": "Location", + "reqd": 1 + }, + { + "fieldname": "custodian", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Custodian", + "options": "Employee" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" + }, + { + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department" + }, + { + "fieldname": "purchase_date", + "fieldtype": "Date", + "label": "Purchase Date", + "reqd": 1 + }, + { + "fieldname": "disposal_date", + "fieldtype": "Date", + "label": "Disposal Date", + "read_only": 1 + }, + { + "fieldname": "journal_entry_for_scrap", + "fieldtype": "Link", + "label": "Journal Entry for Scrap", + "no_copy": 1, + "options": "Journal Entry", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "gross_purchase_amount", + "fieldtype": "Currency", + "label": "Gross Purchase Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fieldname": "available_for_use_date", + "fieldtype": "Date", + "label": "Available-for-use Date", + "reqd": 1 + }, + { + "fieldname": "column_break_18", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "calculate_depreciation", + "fieldtype": "Check", + "label": "Calculate Depreciation" + }, + { + "default": "0", + "fieldname": "is_existing_asset", + "fieldtype": "Check", + "label": "Is Existing Asset" + }, + { + "depends_on": "is_existing_asset", + "fieldname": "opening_accumulated_depreciation", + "fieldtype": "Currency", + "label": "Opening Accumulated Depreciation", + "no_copy": 1, + "options": "Company:company:default_currency" + }, + { + "depends_on": "eval:(doc.is_existing_asset && doc.opening_accumulated_depreciation)", + "fieldname": "number_of_depreciations_booked", + "fieldtype": "Int", + "label": "Number of Depreciations Booked", + "no_copy": 1 + }, + { + "depends_on": "calculate_depreciation", + "fieldname": "section_break_23", + "fieldtype": "Section Break", + "label": "Depreciation" + }, + { + "fieldname": "finance_books", + "fieldtype": "Table", + "label": "Finance Books", + "options": "Asset Finance Book" + }, + { + "fieldname": "section_break_33", + "fieldtype": "Section Break", + "hidden": 1 + }, + { + "fieldname": "depreciation_method", + "fieldtype": "Select", + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nManual" + }, + { + "fieldname": "value_after_depreciation", + "fieldtype": "Currency", + "hidden": 1, + "label": "Value After Depreciation", + "options": "Company:company:default_currency", + "read_only": 1 + }, + { + "fieldname": "total_number_of_depreciations", + "fieldtype": "Int", + "label": "Total Number of Depreciations" + }, + { + "fieldname": "column_break_24", + "fieldtype": "Column Break" + }, + { + "fieldname": "frequency_of_depreciation", + "fieldtype": "Int", + "label": "Frequency of Depreciation (Months)" + }, + { + "fieldname": "next_depreciation_date", + "fieldtype": "Date", + "label": "Next Depreciation Date", + "no_copy": 1 + }, + { + "depends_on": "calculate_depreciation", + "fieldname": "section_break_14", + "fieldtype": "Section Break", + "label": "Depreciation Schedule" + }, + { + "fieldname": "schedules", + "fieldtype": "Table", + "label": "Depreciation Schedules", + "no_copy": 1, + "options": "Depreciation Schedule" + }, + { + "collapsible": 1, + "fieldname": "insurance_details", + "fieldtype": "Section Break", + "label": "Insurance details" + }, + { + "fieldname": "policy_number", + "fieldtype": "Data", + "label": "Policy number" + }, + { + "fieldname": "insurer", + "fieldtype": "Data", + "label": "Insurer" + }, + { + "fieldname": "insured_value", + "fieldtype": "Data", + "label": "Insured value" + }, + { + "fieldname": "column_break_48", + "fieldtype": "Column Break" + }, + { + "fieldname": "insurance_start_date", + "fieldtype": "Date", + "label": "Insurance Start Date" + }, + { + "fieldname": "insurance_end_date", + "fieldtype": "Date", + "label": "Insurance End Date" + }, + { + "fieldname": "comprehensive_insurance", + "fieldtype": "Data", + "label": "Comprehensive Insurance" + }, + { + "fieldname": "section_break_31", + "fieldtype": "Section Break", + "label": "Maintenance" + }, + { + "allow_on_submit": 1, + "default": "0", + "description": "Check if Asset requires Preventive Maintenance or Calibration", + "fieldname": "maintenance_required", + "fieldtype": "Check", + "label": "Maintenance Required" + }, + { + "collapsible": 1, + "fieldname": "other_details", + "fieldtype": "Section Break", + "label": "Other Details" + }, + { + "allow_on_submit": 1, + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nSubmitted\nPartially Depreciated\nFully Depreciated\nSold\nScrapped\nIn Maintenance\nOut of Order\nIssue\nReceipt", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "booked_fixed_asset", + "fieldtype": "Check", + "label": "Booked Fixed Asset", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_51", + "fieldtype": "Column Break" + }, + { + "fieldname": "purchase_receipt", + "fieldtype": "Link", + "label": "Purchase Receipt", + "no_copy": 1, + "options": "Purchase Receipt", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "purchase_receipt_amount", + "fieldtype": "Currency", + "hidden": 1, + "label": "Purchase Receipt Amount", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "label": "Purchase Invoice", + "no_copy": 1, + "options": "Purchase Invoice", + "read_only": 1 + }, + { + "fetch_from": "company.default_finance_book", + "fieldname": "default_finance_book", + "fieldtype": "Link", + "hidden": 1, + "label": "Default Finance Book", + "options": "Finance Book", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Asset", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "fieldname": "accounting_dimensions_section", + "fieldtype": "Section Break", + "label": "Accounting Dimensions" + }, + { + "fieldname": "dimension_col_break", + "fieldtype": "Column Break" + } + ], + "idx": 72, + "image_field": "image", + "is_submittable": 1, + "modified": "2019-10-07 15:34:30.976208", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "asset_name" +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 6e2bbc16260..94e6f6168ef 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -30,7 +30,8 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.update_stock_movement() - if not self.booked_fixed_asset and not is_cwip_accounting_disabled(): + if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.company, + self.asset_category): self.make_gl_entries() def on_cancel(self): @@ -76,10 +77,13 @@ class Asset(AccountsController): self.set('finance_books', finance_books) def validate_asset_values(self): + if not self.asset_category: + self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") + if not flt(self.gross_purchase_amount): frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) - if not is_cwip_accounting_disabled(): + if is_cwip_accounting_enabled(self.company, 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)) @@ -424,7 +428,7 @@ def update_maintenance_status(): asset.set_status('Out of Order') def make_post_gl_entry(): - if is_cwip_accounting_disabled(): + if not is_cwip_accounting_enabled(self.company, self.asset_category): return assets = frappe.db.sql_list(""" select name from `tabAsset` @@ -574,8 +578,13 @@ def make_journal_entry(asset_name): return je -def is_cwip_accounting_disabled(): - return cint(frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting")) +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 + + 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): days = date_diff(to_date, from_date) @@ -587,4 +596,4 @@ def get_total_days(date, frequency): period_start_date = add_months(date, cint(frequency) * -1) - return date_diff(date, period_start_date) \ No newline at end of file + return date_diff(date, period_start_date) diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index c09b94fa8ea..7085b31e050 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -14,7 +14,6 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas class TestAsset(unittest.TestCase): def setUp(self): set_depreciation_settings_in_company() - remove_prorated_depreciation_schedule() create_asset_data() frappe.db.sql("delete from `tabTax Rule`") @@ -70,11 +69,13 @@ class TestAsset(unittest.TestCase): {"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 + 'qty': 1, + 'asset': asset.name }) doc.set_missing_values() @@ -200,7 +201,6 @@ class TestAsset(unittest.TestCase): self.assertEqual(schedules, expected_schedules) def test_schedule_for_prorated_straight_line_method(self): - set_prorated_depreciation_schedule() pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -233,8 +233,6 @@ class TestAsset(unittest.TestCase): self.assertEqual(schedules, expected_schedules) - remove_prorated_depreciation_schedule() - def test_depreciation(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location") @@ -487,6 +485,8 @@ class TestAsset(unittest.TestCase): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( make_purchase_invoice as make_purchase_invoice_from_pr) + #frappe.db.set_value("Asset Category","Computers","enable_cwip_accounting", 1) + pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location") @@ -565,6 +565,7 @@ class TestAsset(unittest.TestCase): where voucher_type='Asset' and voucher_no = %s order by account""", asset_doc.name) + self.assertEqual(gle, expected_gle) def test_expense_head(self): @@ -575,7 +576,6 @@ class TestAsset(unittest.TestCase): 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"): create_asset_category() @@ -596,15 +596,15 @@ def create_asset(**args): asset = frappe.get_doc({ "doctype": "Asset", - "asset_name": "Macbook Pro 1", + "asset_name": args.asset_name or "Macbook Pro 1", "asset_category": "Computers", - "item_code": "Macbook Pro", - "company": "_Test Company", + "item_code": args.item_code or "Macbook Pro", + "company": args.company or"_Test Company", "purchase_date": "2015-01-01", "calculate_depreciation": 0, "gross_purchase_amount": 100000, "expected_value_after_useful_life": 10000, - "warehouse": "_Test Warehouse - _TC", + "warehouse": args.warehouse or "_Test Warehouse - _TC", "available_for_use_date": "2020-06-06", "location": "Test Location", "asset_owner": "Company", @@ -616,6 +616,9 @@ def create_asset(**args): except frappe.DuplicateEntryError: pass + if args.submit: + asset.submit() + return asset def create_asset_category(): @@ -623,6 +626,7 @@ def create_asset_category(): asset_category.asset_category_name = "Computers" asset_category.total_number_of_depreciations = 3 asset_category.frequency_of_depreciation = 3 + asset_category.enable_cwip_accounting = 1 asset_category.append("accounts", { "company_name": "_Test Company", "fixed_asset_account": "_Test Fixed Asset - _TC", @@ -656,19 +660,4 @@ def set_depreciation_settings_in_company(): company.save() # Enable booking asset depreciation entry automatically - frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) - -def remove_prorated_depreciation_schedule(): - asset_settings = frappe.get_doc("Asset Settings", "Asset Settings") - asset_settings.schedule_based_on_fiscal_year = 0 - asset_settings.save() - - frappe.db.commit() - -def set_prorated_depreciation_schedule(): - asset_settings = frappe.get_doc("Asset Settings", "Asset Settings") - asset_settings.schedule_based_on_fiscal_year = 1 - asset_settings.number_of_days_in_fiscal_year = 360 - asset_settings.save() - - frappe.db.commit() + frappe.db.set_value("Accounts Settings", None, "book_asset_depreciation_entry_automatically", 1) \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json index 882cbe2eaa8..7483b41d4de 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.json +++ b/erpnext/assets/doctype/asset_category/asset_category.json @@ -1,284 +1,115 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:asset_category_name", - "beta": 0, - "creation": "2016-03-01 17:41:39.778765", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:asset_category_name", + "creation": "2016-03-01 17:41:39.778765", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "asset_category_name", + "column_break_3", + "depreciation_options", + "enable_cwip_accounting", + "finance_book_detail", + "finance_books", + "section_break_2", + "accounts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "asset_category_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": "Asset Category 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "asset_category_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Asset Category Name", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 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, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_book_detail", - "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": "Finance Book Detail", - "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": "finance_book_detail", + "fieldtype": "Section Break", + "label": "Finance Book Detail" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "finance_books", - "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": "Finance Books", - "length": 0, - "no_copy": 0, - "options": "Asset Finance Book", - "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": "finance_books", + "fieldtype": "Table", + "label": "Finance Books", + "options": "Asset Finance Book" + }, { - "allow_bulk_edit": 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, - "label": "Accounts", - "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_break_2", + "fieldtype": "Section Break", + "label": "Accounts" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "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": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Asset Category Account", - "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, - "unique": 0 + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Asset Category Account", + "reqd": 1 + }, + { + "fieldname": "depreciation_options", + "fieldtype": "Section Break", + "label": "Depreciation Options" + }, + { + "default": "0", + "fieldname": "enable_cwip_accounting", + "fieldtype": "Check", + "label": "Enable Capital Work in Progress Accounting" } - ], - "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-05-12 14:56:04.116425", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Category", - "name_case": "", - "owner": "Administrator", + ], + "modified": "2019-10-11 12:19:59.759136", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Category", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 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": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 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": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 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": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Quality Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Quality Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0 + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index bbdc6ec2cfa..5cb634abcd4 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -10,11 +10,24 @@ 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: for field in ("Total Number of Depreciations", "Frequency of Depreciation"): 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(asset, fieldname, account=None, asset_category = None, company = None): if not asset_category and company: diff --git a/erpnext/assets/doctype/asset_settings/__init__.py b/erpnext/assets/doctype/asset_settings/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/assets/doctype/asset_settings/asset_settings.js b/erpnext/assets/doctype/asset_settings/asset_settings.js deleted file mode 100644 index 3b421486c38..00000000000 --- a/erpnext/assets/doctype/asset_settings/asset_settings.js +++ /dev/null @@ -1,5 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Asset Settings', { -}); diff --git a/erpnext/assets/doctype/asset_settings/asset_settings.json b/erpnext/assets/doctype/asset_settings/asset_settings.json deleted file mode 100644 index edc5ce169ca..00000000000 --- a/erpnext/assets/doctype/asset_settings/asset_settings.json +++ /dev/null @@ -1,148 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-01-03 10:30:32.983381", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "depreciation_options", - "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": "Depreciation Options", - "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": "disable_cwip_accounting", - "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": "Disable CWIP Accounting", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2019-05-26 18:31:19.930563", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Settings", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "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, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_settings/asset_settings.py b/erpnext/assets/doctype/asset_settings/asset_settings.py deleted file mode 100644 index e303ebd23f9..00000000000 --- a/erpnext/assets/doctype/asset_settings/asset_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -from frappe.model.document import Document - -class AssetSettings(Document): - pass diff --git a/erpnext/assets/doctype/asset_settings/test_asset_settings.js b/erpnext/assets/doctype/asset_settings/test_asset_settings.js deleted file mode 100644 index eac2c928f37..00000000000 --- a/erpnext/assets/doctype/asset_settings/test_asset_settings.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Asset Settings", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Asset Settings - () => frappe.tests.make('Asset Settings', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/assets/doctype/asset_settings/test_asset_settings.py b/erpnext/assets/doctype/asset_settings/test_asset_settings.py deleted file mode 100644 index 75f146a27ee..00000000000 --- a/erpnext/assets/doctype/asset_settings/test_asset_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import unittest - -class TestAssetSettings(unittest.TestCase): - pass diff --git a/erpnext/config/assets.py b/erpnext/config/assets.py index 3c9452f5a4f..4cf7cf08067 100644 --- a/erpnext/config/assets.py +++ b/erpnext/config/assets.py @@ -21,10 +21,6 @@ def get_data(): "name": "Asset Category", "onboard": 1, }, - { - "type": "doctype", - "name": "Asset Settings", - }, { "type": "doctype", "name": "Asset Movement", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index aeb12f51b52..0155b278201 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -640,6 +640,7 @@ erpnext.patches.v12_0.create_default_energy_point_rules erpnext.patches.v12_0.set_produced_qty_field_in_sales_order_for_work_order erpnext.patches.v12_0.generate_leave_ledger_entries erpnext.patches.v12_0.set_default_shopify_app_type +erpnext.patches.v12_0.set_cwip_and_delete_asset_settings erpnext.patches.v12_0.set_expense_account_in_landed_cost_voucher_taxes erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings erpnext.patches.v12_0.set_payment_entry_status 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 new file mode 100644 index 00000000000..3d07fe57a5a --- /dev/null +++ b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import cint + + +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.sql(""" SELECT value FROM `tabSingles` WHERE doctype='Asset Settings' + and field='disable_cwip_accounting' """, as_dict=1) + + companies = [x['name'] for x in frappe.get_all("Company", "name")] + for company in companies: + enable_cwip_accounting = cint(not cint(cwip_value[0]['value'])) + frappe.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 diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index bc3418997dd..2d181b53ca4 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -72,6 +72,7 @@ "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", @@ -720,12 +721,18 @@ "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-07-04 22:20:45.104307", + "modified": "2019-10-09 14:42:04.440974", "modified_by": "Administrator", "module": "Setup", "name": "Company", @@ -767,6 +774,18 @@ { "read": 1, "role": "Projects User" + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 } ], "show_name_in_global_search": 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 3362d4b0f51..1bfdca50ea1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -14,7 +14,7 @@ from erpnext.accounts.utils import get_account_currency from frappe.desk.notifications import clear_doctype_notifications from frappe.model.mapper import get_mapped_doc from erpnext.buying.utils import check_on_hold_or_closed_status -from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_disabled +from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from six import iteritems @@ -338,12 +338,13 @@ class PurchaseReceipt(BuyingController): def get_asset_gl_entry(self, gl_entries, expenses_included_in_valuation=None): arbnb_account, cwip_account = None, None - cwip_disabled = is_cwip_accounting_disabled() - if not expenses_included_in_valuation: expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") for d in self.get("items"): + asset_category = frappe.get_cached_value("Item", d.item_code, "asset_category") + cwip_enabled = is_cwip_accounting_enabled(self.company, asset_category) + if d.is_fixed_asset and not (arbnb_account and cwip_account): arbnb_account = self.get_company_default("asset_received_but_not_billed") @@ -351,8 +352,7 @@ class PurchaseReceipt(BuyingController): cwip_account = get_asset_account("capital_work_in_progress_account", d.asset, company = self.company) - if d.is_fixed_asset and not cwip_disabled: - + if d.is_fixed_asset and cwip_enabled: asset_amount = flt(d.net_amount) + flt(d.item_tax_amount/self.conversion_rate) base_asset_amount = flt(d.base_net_amount + d.item_tax_amount) @@ -381,7 +381,7 @@ class PurchaseReceipt(BuyingController): if d.is_fixed_asset and flt(d.landed_cost_voucher_amount): asset_account = (get_asset_category_account(d.asset, 'fixed_asset_account', - company = self.company) if cwip_disabled else cwip_account) + company = self.company) if not cwip_enabled else cwip_account) gl_entries.append(self.get_gl_dict({ "account": expenses_included_in_valuation, From 06c812957415e34b52d0aee657b465da107a2320 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 13 Nov 2019 10:51:43 +0530 Subject: [PATCH 153/679] fix(batch): fetch company on splitting the batch (#19558) --- erpnext/stock/doctype/batch/batch.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py index f609a0be7dc..3e890b4dd4e 100644 --- a/erpnext/stock/doctype/batch/batch.py +++ b/erpnext/stock/doctype/batch/batch.py @@ -185,9 +185,17 @@ def get_batches_by_oldest(item_code, warehouse): def split_batch(batch_no, item_code, warehouse, qty, new_batch_id=None): """Split the batch into a new batch""" batch = frappe.get_doc(dict(doctype='Batch', item=item_code, batch_id=new_batch_id)).insert() + + company = frappe.db.get_value('Stock Ledger Entry', dict( + item_code=item_code, + batch_no=batch_no, + warehouse=warehouse + ), ['company']) + stock_entry = frappe.get_doc(dict( doctype='Stock Entry', purpose='Repack', + company=company, items=[ dict( item_code=item_code, From ffbfaf7099117a1e3a634b7c42d0d9dcb1dcd7df Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 13 Nov 2019 10:59:23 +0530 Subject: [PATCH 154/679] fix: email digest showing incorrect upcoming events (#19552) --- .../doctype/email_digest/email_digest.py | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py index 1de5ccb1427..0bcddc21517 100644 --- a/erpnext/setup/doctype/email_digest/email_digest.py +++ b/erpnext/setup/doctype/email_digest/email_digest.py @@ -4,8 +4,8 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import fmt_money, formatdate, format_time, now_datetime, \ - get_url_to_form, get_url_to_list, flt, get_link_to_report +from frappe.utils import (fmt_money, formatdate, format_time, now_datetime, + get_url_to_form, get_url_to_list, flt, get_link_to_report, add_to_date, today) from datetime import timedelta from dateutil.relativedelta import relativedelta from frappe.core.doctype.user.user import STANDARD_USERS @@ -151,8 +151,9 @@ class EmailDigest(Document): def get_calendar_events(self): """Get calendar events for given user""" from frappe.desk.doctype.event.event import get_events - events = get_events(self.future_from_date.strftime("%Y-%m-%d"), - self.future_to_date.strftime("%Y-%m-%d")) or [] + from_date, to_date = get_future_date_for_calendaer_event(self.frequency) + + events = get_events(from_date, to_date) event_count = 0 for i, e in enumerate(events): @@ -825,4 +826,14 @@ def get_count_for_period(account, fieldname, from_date, to_date): last_year_closing_count = get_count_on(account, fieldname, fy_start_date - timedelta(days=1)) count = count_on_to_date + (last_year_closing_count - count_before_from_date) - return count \ No newline at end of file + return count + +def get_future_date_for_calendaer_event(frequency): + from_date = to_date = today() + + if frequency == "Weekly": + to_date = add_to_date(from_date, weeks=1) + elif frequency == "Monthly": + to_date = add_to_date(from_date, months=1) + + return from_date, to_date \ No newline at end of file From f75ea952e3aeb203b5c6569fc5e9604dee236a77 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 11:03:53 +0530 Subject: [PATCH 155/679] 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 156/679] 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 157/679] 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 158/679] 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 159/679] 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 160/679] 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 5ea4328359a526721206d91c150fae38329b797d Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Wed, 13 Nov 2019 12:46:19 +0530 Subject: [PATCH 161/679] fix: Accumulated Values filter disappearing --- erpnext/accounts/report/balance_sheet/balance_sheet.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.js b/erpnext/accounts/report/balance_sheet/balance_sheet.js index 4bc29da2c7d..8c11514aa64 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.js +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.js @@ -2,7 +2,7 @@ // License: GNU General Public License v3. See license.txt frappe.require("assets/erpnext/js/financial_statements.js", function() { - frappe.query_reports["Balance Sheet"] = erpnext.financial_statements; + frappe.query_reports["Balance Sheet"] = $.extend({}, erpnext.financial_statements); frappe.query_reports["Balance Sheet"]["filters"].push({ "fieldname": "accumulated_values", From 67f191df4edecd43de1a7d4904792fe088e8aae2 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 14:14:10 +0530 Subject: [PATCH 162/679] 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 163/679] 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 164/679] 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 165/679] 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 f805a76e792ca27392db5695b8ccd7902a6c02cc Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 13 Nov 2019 17:27:16 +0530 Subject: [PATCH 166/679] chore: pinned requirements --- requirements.txt | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/requirements.txt b/requirements.txt index 28ba9f676ff..429f894edce 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ frappe -unidecode -pygithub -googlemaps -python-stdnum -braintree -gocardless_pro -woocommerce -pandas -plaid-python \ No newline at end of file +Unidecode==1.1.1 +PyGithub==1.43.8 +googlemaps==3.1.1 +python-stdnum==1.11 +braintree==3.57.1 +gocardless-pro==1.10.0 +WooCommerce==2.1.1 +pandas==0.25.1 +plaid-python==3.4.0 From 3a72cb46bce2168703d976a782a7bdaa9eb1d0de Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Nov 2019 17:58:10 +0530 Subject: [PATCH 167/679] fix: Set due date in accounts receivable based on payment terms (#19563) --- .../report/accounts_receivable/accounts_receivable.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index bcbd427186c..14906f2c2e6 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -188,7 +188,11 @@ class ReceivablePayableReport(object): self.data.append(row) def set_invoice_details(self, row): - row.update(self.invoice_details.get(row.voucher_no, {})) + invoice_details = self.invoice_details.get(row.voucher_no, {}) + if row.due_date: + invoice_details.pop("due_date", None) + row.update(invoice_details) + if row.voucher_type == 'Sales Invoice': if self.filters.show_delivery_notes: self.set_delivery_notes(row) From ba8fc21594eafcd7accf60a29aca6d14b48485e8 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 13 Nov 2019 18:11:58 +0530 Subject: [PATCH 168/679] fix: merge similar entries for serialized items in stock reconciliation (#19408) --- .../stock_reconciliation.py | 60 ++++++++++++++++++- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 98a8c594830..daf320e40ab 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -52,9 +52,10 @@ class StockReconciliation(StockController): def _changed(item): item_dict = get_stock_balance_for(item.item_code, item.warehouse, self.posting_date, self.posting_time, batch_no=item.batch_no) - if (((item.qty is None or item.qty==item_dict.get("qty")) and - (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and not item.serial_no) - or (item.serial_no and item.serial_no == item_dict.get("serial_nos"))): + + if ((item.qty is None or item.qty==item_dict.get("qty")) and + (item.valuation_rate is None or item.valuation_rate==item_dict.get("rate")) and + (not item.serial_no or (item.serial_no == item_dict.get("serial_nos")) )): return False else: # set default as current rates @@ -182,9 +183,11 @@ class StockReconciliation(StockController): from erpnext.stock.stock_ledger import get_previous_sle sl_entries = [] + has_serial_no = False for row in self.items: item = frappe.get_doc("Item", row.item_code) if item.has_serial_no or item.has_batch_no: + has_serial_no = True self.get_sle_for_serialized_items(row, sl_entries) else: previous_sle = get_previous_sle({ @@ -212,8 +215,14 @@ class StockReconciliation(StockController): sl_entries.append(self.get_sle_for_items(row)) if sl_entries: + if has_serial_no: + sl_entries = self.merge_similar_item_serial_nos(sl_entries) + self.make_sl_entries(sl_entries) + if has_serial_no and sl_entries: + self.update_valuation_rate_for_serial_no() + def get_sle_for_serialized_items(self, row, sl_entries): from erpnext.stock.stock_ledger import get_previous_sle @@ -275,8 +284,18 @@ class StockReconciliation(StockController): # update valuation rate self.update_valuation_rate_for_serial_nos(row, serial_nos) + def update_valuation_rate_for_serial_no(self): + for d in self.items: + if not d.serial_no: continue + + serial_nos = get_serial_nos(d.serial_no) + self.update_valuation_rate_for_serial_nos(d, serial_nos) + def update_valuation_rate_for_serial_nos(self, row, serial_nos): valuation_rate = row.valuation_rate if self.docstatus == 1 else row.current_valuation_rate + if valuation_rate is None: + return + for d in serial_nos: frappe.db.set_value("Serial No", d, 'purchase_rate', valuation_rate) @@ -321,11 +340,17 @@ class StockReconciliation(StockController): where voucher_type=%s and voucher_no=%s""", (self.doctype, self.name)) sl_entries = [] + + has_serial_no = False for row in self.items: if row.serial_no or row.batch_no or row.current_serial_no: + has_serial_no = True self.get_sle_for_serialized_items(row, sl_entries) if sl_entries: + if has_serial_no: + sl_entries = self.merge_similar_item_serial_nos(sl_entries) + sl_entries.reverse() allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") self.make_sl_entries(sl_entries, allow_negative_stock=allow_negative_stock) @@ -339,6 +364,35 @@ class StockReconciliation(StockController): "posting_time": self.posting_time }) + def merge_similar_item_serial_nos(self, sl_entries): + # If user has put the same item in multiple row with different serial no + new_sl_entries = [] + merge_similar_entries = {} + + for d in sl_entries: + if not d.serial_no or d.actual_qty < 0: + new_sl_entries.append(d) + continue + + key = (d.item_code, d.warehouse) + if key not in merge_similar_entries: + merge_similar_entries[key] = d + elif d.serial_no: + data = merge_similar_entries[key] + data.actual_qty += d.actual_qty + data.qty_after_transaction += d.qty_after_transaction + + data.valuation_rate = (data.valuation_rate + d.valuation_rate) / data.actual_qty + data.serial_no += '\n' + d.serial_no + + if data.incoming_rate: + data.incoming_rate = (data.incoming_rate + d.incoming_rate) / data.actual_qty + + for key, value in merge_similar_entries.items(): + new_sl_entries.append(value) + + return new_sl_entries + def get_gl_entries(self, warehouse_account=None): if not self.cost_center: msgprint(_("Please enter Cost Center"), raise_exception=1) From d064505ebe68b6ea6a89bfcf0bbaf1c0ec20d074 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 13 Nov 2019 18:17:48 +0530 Subject: [PATCH 169/679] fix: incorrect produced qty in the production plan (#19569) --- .../doctype/production_plan/production_plan.js | 5 +++++ erpnext/manufacturing/doctype/work_order/work_order.py | 10 +++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 51989378d8f..3b24d0fa0ff 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -3,6 +3,11 @@ frappe.ui.form.on('Production Plan', { setup: function(frm) { + frm.custom_make_buttons = { + 'Work Order': 'Work Order', + 'Material Request': 'Material Request', + }; + frm.fields_dict['po_items'].grid.get_field('warehouse').get_query = function(doc) { return { filters: { diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index ae4d9be2826..6ea3dc83ed6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -223,7 +223,15 @@ class WorkOrder(Document): def update_production_plan_status(self): production_plan = frappe.get_doc('Production Plan', self.production_plan) - production_plan.run_method("update_produced_qty", self.produced_qty, self.production_plan_item) + produced_qty = 0 + if self.production_plan_item: + total_qty = frappe.get_all("Work Order", fields = "sum(produced_qty) as produced_qty", + filters = {'docstatus': 1, 'production_plan': self.production_plan, + 'production_plan_item': self.production_plan_item}, as_list=1) + + produced_qty = total_qty[0][0] if total_qty else 0 + + production_plan.run_method("update_produced_qty", produced_qty, self.production_plan_item) def on_submit(self): if not self.wip_warehouse: From 732d6afad55c553a7f04ef7227125ee98017887f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 13 Nov 2019 18:49:23 +0530 Subject: [PATCH 170/679] fix: Show AR summary based on outstanding (#19573) --- .../accounts_receivable_summary/accounts_receivable_summary.py | 3 +++ 1 file changed, 3 insertions(+) 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 b90a7a9501b..8955830e09b 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -36,6 +36,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.report_date) or {} for party, party_dict in iteritems(self.party_total): + if party_dict.outstanding <= 0: + continue + row = frappe._dict() row.party = party From 94565d69d100cfab12bdada14013c48f63586329 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 13 Nov 2019 18:58:22 +0530 Subject: [PATCH 171/679] fix: travis failing (#19568) --- erpnext/setup/doctype/currency_exchange/currency_exchange.py | 4 ++++ .../setup/doctype/currency_exchange/test_currency_exchange.py | 4 +++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/currency_exchange/currency_exchange.py b/erpnext/setup/doctype/currency_exchange/currency_exchange.py index 60d367a4bbc..6480f60f59c 100644 --- a/erpnext/setup/doctype/currency_exchange/currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/currency_exchange.py @@ -14,10 +14,14 @@ class CurrencyExchange(Document): purpose = "" if not self.date: self.date = nowdate() + + # If both selling and buying enabled + purpose = "Selling-Buying" if cint(self.for_buying)==0 and cint(self.for_selling)==1: purpose = "Selling" if cint(self.for_buying)==1 and cint(self.for_selling)==0: purpose = "Buying" + self.name = '{0}-{1}-{2}{3}'.format(formatdate(get_datetime_str(self.date), "yyyy-MM-dd"), self.from_currency, self.to_currency, ("-" + purpose) if purpose else "") diff --git a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py index 857f666b2a6..c5c01c57758 100644 --- a/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py +++ b/erpnext/setup/doctype/currency_exchange/test_currency_exchange.py @@ -11,7 +11,9 @@ test_records = frappe.get_test_records('Currency Exchange') def save_new_records(test_records): for record in test_records: - purpose = str("") + # If both selling and buying enabled + purpose = "Selling-Buying" + if cint(record.get("for_buying"))==0 and cint(record.get("for_selling"))==1: purpose = "Selling" if cint(record.get("for_buying"))==1 and cint(record.get("for_selling"))==0: From 3e515e704ddfde9c733d96edbc8502d0f03c677c Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 13 Nov 2019 19:00:24 +0530 Subject: [PATCH 172/679] Monthly distribution of depreciation amount (#19493) * feat: allow monthly distribution of depreciation amount * chore: added comments * fix: monthly depr was not starting from use date's month --- erpnext/assets/doctype/asset/asset.json | 13 +++-- erpnext/assets/doctype/asset/asset.py | 72 +++++++++++++++++++++---- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 8fda330bb7c..6882f6a9924 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -33,6 +33,7 @@ "available_for_use_date", "column_break_18", "calculate_depreciation", + "allow_monthly_depreciation", "is_existing_asset", "opening_accumulated_depreciation", "number_of_depreciations_booked", @@ -216,8 +217,7 @@ { "fieldname": "available_for_use_date", "fieldtype": "Date", - "label": "Available-for-use Date", - "reqd": 1 + "label": "Available-for-use Date" }, { "fieldname": "column_break_18", @@ -450,12 +450,19 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "calculate_depreciation", + "fieldname": "allow_monthly_depreciation", + "fieldtype": "Check", + "label": "Allow Monthly Depreciation" } ], "idx": 72, "image_field": "image", "is_submittable": 1, - "modified": "2019-10-07 15:34:30.976208", + "modified": "2019-10-22 15:47:36.050828", "modified_by": "Administrator", "module": "Assets", "name": "Asset", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 94e6f6168ef..d1f8c1a8d3d 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, math, json from frappe import _ from six import string_types -from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, add_days +from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ @@ -149,19 +149,31 @@ class Asset(AccountsController): schedule_date = add_months(d.depreciation_start_date, n * cint(d.frequency_of_depreciation)) + # schedule date will be a year later from start date + # so monthly schedule date is calculated by removing 11 months from it + monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1) + # For first row if has_pro_rata and n==0: - depreciation_amount, days = get_pro_rata_amt(d, depreciation_amount, + depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount, self.available_for_use_date, d.depreciation_start_date) + + # For first depr schedule date will be the start date + # so monthly schedule date is calculated by removing month difference between use date and start date + monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1) + # For last row elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1: to_date = add_months(self.available_for_use_date, n * cint(d.frequency_of_depreciation)) - depreciation_amount, days = get_pro_rata_amt(d, + depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount, schedule_date, to_date) + monthly_schedule_date = add_months(schedule_date, 1) + schedule_date = add_days(schedule_date, days) + last_schedule_date = schedule_date if not depreciation_amount: continue value_after_depreciation -= flt(depreciation_amount, @@ -175,13 +187,50 @@ class Asset(AccountsController): skip_row = True if depreciation_amount > 0: - self.append("schedules", { - "schedule_date": schedule_date, - "depreciation_amount": depreciation_amount, - "depreciation_method": d.depreciation_method, - "finance_book": d.finance_book, - "finance_book_id": d.idx - }) + # With monthly depreciation, each depreciation is divided by months remaining until next date + if self.allow_monthly_depreciation: + # month range is 1 to 12 + # In pro rata case, for first and last depreciation, month range would be different + month_range = months \ + if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \ + else d.frequency_of_depreciation + + for r in range(month_range): + if (has_pro_rata and n == 0): + # For first entry of monthly depr + if r == 0: + days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) + per_day_amt = depreciation_amount / days + depreciation_amount_for_current_month = per_day_amt * days_until_first_depr + depreciation_amount -= depreciation_amount_for_current_month + date = monthly_schedule_date + amount = depreciation_amount_for_current_month + else: + date = add_months(monthly_schedule_date, r) + amount = depreciation_amount / (month_range - 1) + elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: + # For last entry of monthly depr + date = last_schedule_date + amount = depreciation_amount / month_range + else: + date = add_months(monthly_schedule_date, r) + amount = depreciation_amount / month_range + + self.append("schedules", { + "schedule_date": date, + "depreciation_amount": amount, + "depreciation_method": d.depreciation_method, + "finance_book": d.finance_book, + "finance_book_id": d.idx + }) + else: + self.append("schedules", { + "schedule_date": schedule_date, + "depreciation_amount": depreciation_amount, + "depreciation_method": d.depreciation_method, + "finance_book": d.finance_book, + "finance_book_id": d.idx + }) def check_is_pro_rata(self, row): has_pro_rata = False @@ -588,9 +637,10 @@ def is_cwip_accounting_enabled(company, asset_category=None): def get_pro_rata_amt(row, depreciation_amount, from_date, to_date): days = date_diff(to_date, from_date) + months = month_diff(to_date, from_date) total_days = get_total_days(to_date, row.frequency_of_depreciation) - return (depreciation_amount * flt(days)) / flt(total_days), days + return (depreciation_amount * flt(days)) / flt(total_days), days, months def get_total_days(date, frequency): period_start_date = add_months(date, From af7fe1937ec77028a770b490c942cbd014c36026 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 13 Nov 2019 19:00:56 +0530 Subject: [PATCH 173/679] fix: fetch leave approver defined in employee in leave application (#19559) * fix: fetch leave approver defined in employee in leave application * Update department_approver.py --- .../department_approver/department_approver.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 9f2f2013a77..7bf9905d07a 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -19,14 +19,20 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): approvers = [] department_details = {} department_list = [] - employee_department = filters.get("department") or frappe.get_value("Employee", filters.get("employee"), "department") + employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True) + if employee.leave_approver: + approver = frappe.db.get_value("User", leave_approver, ['name', 'first_name', 'last_name']) + approvers.append(approver) + return approvers + + employee_department = filters.get("department") or employee.department if employee_department: department_details = frappe.db.get_value("Department", {"name": employee_department}, ["lft", "rgt"], as_dict=True) if department_details: department_list = frappe.db.sql("""select name from `tabDepartment` where lft <= %s and rgt >= %s and disabled=0 - order by lft desc""", (department_details.lft, department_details.rgt), as_list = True) + order by lft desc""", (department_details.lft, department_details.rgt), as_list=True) if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" @@ -41,4 +47,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 \ No newline at end of file + return approvers From 9ffa9d4a64eb60ca02da187646af34df1a723fc4 Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 13 Nov 2019 19:10:20 +0530 Subject: [PATCH 174/679] fix(patch): Enable CWIP Accounting --- .../patches/v12_0/set_cwip_and_delete_asset_settings.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) 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 3d07fe57a5a..5842e9edbf8 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 @@ -9,13 +9,12 @@ def execute(): if frappe.db.exists("DocType","Asset Settings"): frappe.reload_doctype("Company") - cwip_value = frappe.db.sql(""" SELECT value FROM `tabSingles` WHERE doctype='Asset Settings' - and field='disable_cwip_accounting' """, as_dict=1) + cwip_value = frappe.db.get_single_value("Asset Settings","disable_cwip_accounting") companies = [x['name'] for x in frappe.get_all("Company", "name")] for company in companies: - enable_cwip_accounting = cint(not cint(cwip_value[0]['value'])) - frappe.set_value("Company", company, "enable_cwip_accounting", enable_cwip_accounting) + 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' """) From 1ad2d4a962cb0bd6b43d255b3ade228e8135d932 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Wed, 13 Nov 2019 19:21:53 +0530 Subject: [PATCH 175/679] fix: get tags for rfq (#19564) * fix: get tags for rfq * chore: remove console log --- .../request_for_quotation/request_for_quotation.js | 2 +- .../request_for_quotation/request_for_quotation.py | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js index 9ad06f9b7ef..2f0cfa64fc8 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js @@ -134,7 +134,7 @@ frappe.ui.form.on("Request for Quotation",{ if (args.search_type === "Tag" && args.tag) { return frappe.call({ type: "GET", - method: "frappe.desk.tags.get_tagged_docs", + method: "frappe.desk.doctype.tag.tag.get_tagged_docs", args: { "doctype": "Supplier", "tag": args.tag diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py index a10ce46e33e..95db33b0f8f 100644 --- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py +++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py @@ -344,13 +344,9 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc = @frappe.whitelist() def get_supplier_tag(): - data = frappe.db.sql("select _user_tags from `tabSupplier`") - - tags = [] - for tag in data: - tags += filter(bool, tag[0].split(",")) - - tags = list(set(tags)) - - return tags + if not frappe.cache().hget("Supplier", "Tags"): + filters = {"document_type": "Supplier"} + tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag])) + frappe.cache().hset("Supplier", "Tags", tags) + return frappe.cache().hget("Supplier", "Tags") From 73616d6b3366bd357c4df162349ac3ddff429232 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 14 Nov 2019 10:09:44 +0530 Subject: [PATCH 176/679] fix: duplication while bulk creation of item tax template (#19570) --- .../move_item_tax_to_item_tax_template.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 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 412f32030ae..f25b9eaf521 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 @@ -1,20 +1,30 @@ import frappe import json from six import iteritems +from frappe.model.naming import make_autoname def execute(): if "tax_type" not in frappe.db.get_table_columns("Item Tax"): return old_item_taxes = {} item_tax_templates = {} - rename_template_to_untitled = [] + + frappe.reload_doc("accounts", "doctype", "item_tax_template_detail", force=1) + frappe.reload_doc("accounts", "doctype", "item_tax_template", force=1) + existing_templates = frappe.db.sql("""select template.name, details.tax_type, details.tax_rate + from `tabItem Tax Template` template, `tabItem Tax Template Detail` details + where details.parent=template.name + """, as_dict=1) + + if len(existing_templates): + for d in existing_templates: + item_tax_templates.setdefault(d.name, {}) + item_tax_templates[d.name][d.tax_type] = d.tax_rate for d in frappe.db.sql("""select parent as item_code, tax_type, tax_rate from `tabItem Tax`""", as_dict=1): old_item_taxes.setdefault(d.item_code, []) old_item_taxes[d.item_code].append(d) - frappe.reload_doc("accounts", "doctype", "item_tax_template_detail", force=1) - frappe.reload_doc("accounts", "doctype", "item_tax_template", force=1) frappe.reload_doc("stock", "doctype", "item", force=1) frappe.reload_doc("stock", "doctype", "item_tax", force=1) frappe.reload_doc("selling", "doctype", "quotation_item", force=1) @@ -27,6 +37,8 @@ def execute(): frappe.reload_doc("accounts", "doctype", "purchase_invoice_item", force=1) frappe.reload_doc("accounts", "doctype", "accounts_settings", force=1) + frappe.db.auto_commit_on_many_writes = True + # for each item that have item tax rates for item_code in old_item_taxes.keys(): # make current item's tax map @@ -34,8 +46,7 @@ def execute(): for d in old_item_taxes[item_code]: item_tax_map[d.tax_type] = d.tax_rate - item_tax_template_name = get_item_tax_template(item_tax_templates, rename_template_to_untitled, - item_tax_map, item_code) + item_tax_template_name = get_item_tax_template(item_tax_templates, item_tax_map, item_code) # update the item tax table item = frappe.get_doc("Item", item_code) @@ -49,35 +60,33 @@ def execute(): 'Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice' ] + for dt in doctypes: for d in frappe.db.sql("""select name, parent, item_code, item_tax_rate from `tab{0} Item` - where ifnull(item_tax_rate, '') not in ('', '{{}}')""".format(dt), as_dict=1): + 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 = get_item_tax_template(item_tax_templates, rename_template_to_untitled, + item_tax_template_name = get_item_tax_template(item_tax_templates, item_tax_map, d.item_code, d.parent) - frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template) + frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) - idx = 1 - for oldname in rename_template_to_untitled: - frappe.rename_doc("Item Tax Template", oldname, "Untitled {}".format(idx)) - idx += 1 + frappe.db.auto_commit_on_many_writes = False settings = frappe.get_single("Accounts Settings") settings.add_taxes_from_item_tax_template = 0 settings.determine_address_tax_category_from = "Billing Address" settings.save() -def get_item_tax_template(item_tax_templates, rename_template_to_untitled, item_tax_map, item_code, parent=None): +def get_item_tax_template(item_tax_templates, item_tax_map, item_code, 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: - if not parent: - rename_template_to_untitled.append(template) return template # if no item tax template found, create one item_tax_template = frappe.new_doc("Item Tax Template") - item_tax_template.title = "{}--{}".format(parent, item_code) if parent else "Item-{}".format(item_code) + 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): parts = tax_type.strip().split(" - ") From 5503b6cff56d876b4a88046cb7810cf83155dea1 Mon Sep 17 00:00:00 2001 From: Himanshu Date: Thu, 14 Nov 2019 10:11:25 +0530 Subject: [PATCH 177/679] fix(Task): Do not create/schedule task after project end (#19184) * fix: do not create/schedule task after project end * fix: check difference between dates * fix: check project date * fix: task creation * fix: tests --- .../doctype/crop_cycle/crop_cycle.py | 14 +++++------ erpnext/projects/doctype/task/task.py | 25 +++++++++++++++++-- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py index bb9045ca81e..3e51933df7f 100644 --- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py +++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py @@ -51,27 +51,25 @@ class CropCycle(Document): self.create_task(disease_doc.treatment_task, self.name, start_date) def create_project(self, period, crop_tasks): - project = frappe.new_doc("Project") - project.update({ + project = frappe.get_doc({ + "doctype": "Project", "project_name": self.title, "expected_start_date": self.start_date, "expected_end_date": add_days(self.start_date, period - 1) - }) - project.insert() + }).insert() return project.name def create_task(self, crop_tasks, project_name, start_date): for crop_task in crop_tasks: - task = frappe.new_doc("Task") - task.update({ + frappe.get_doc({ + "doctype": "Task", "subject": crop_task.get("task_name"), "priority": crop_task.get("priority"), "project": project_name, "exp_start_date": add_days(start_date, crop_task.get("start_day") - 1), "exp_end_date": add_days(start_date, crop_task.get("end_day") - 1) - }) - task.insert() + }).insert() def reload_linked_analysis(self): linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis'] diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 90e9f05f225..54fce8d6db5 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -10,6 +10,7 @@ from frappe import _, throw from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate from frappe.utils.nestedset import NestedSet from frappe.desk.form.assign_to import close_all_assignments, clear +from frappe.utils import date_diff class CircularReferenceError(frappe.ValidationError): pass class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass @@ -28,16 +29,29 @@ class Task(NestedSet): def validate(self): self.validate_dates() + self.validate_parent_project_dates() self.validate_progress() self.validate_status() self.update_depends_on() def validate_dates(self): if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date): - frappe.throw(_("'Expected Start Date' can not be greater than 'Expected End Date'")) + frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \ + frappe.bold("Expected End Date"))) if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date): - frappe.throw(_("'Actual Start Date' can not be greater than 'Actual End Date'")) + frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ + frappe.bold("Actual End Date"))) + + def validate_parent_project_dates(self): + if not self.project or frappe.flags.in_test: + return + + expected_end_date = getdate(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") def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Completed": @@ -255,3 +269,10 @@ def add_multiple_tasks(data, parent): def on_doctype_update(): frappe.db.add_index("Task", ["lft", "rgt"]) + +def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date): + if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0: + 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 From 793ba8fc06ac5fec09b5c7a52cb73bd44b3f903b Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 14 Nov 2019 11:25:49 +0530 Subject: [PATCH 178/679] 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 fc3b924d4dd727431c82925bce6b2f9afd90d698 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 14 Nov 2019 12:02:10 +0530 Subject: [PATCH 179/679] fix: skip leave ledger entry creation for denied leaves (#19556) * fix: skip leave ledger entry creation for denied leave application * patch: remove incorrect leave ledger entries * fix: create reverse ledger entry before setting status to cancel --- .../leave_application/leave_application.py | 5 +++- erpnext/patches.txt | 4 +-- .../remove_denied_leaves_from_leave_ledger.py | 26 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index e1e5e8001d8..0e6630541c4 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -55,11 +55,11 @@ class LeaveApplication(Document): self.reload() def on_cancel(self): + self.create_leave_ledger_entry(submit=False) self.status = "Cancelled" # notify leave applier about cancellation self.notify_employee() self.cancel_attendance() - self.create_leave_ledger_entry(submit=False) def validate_applicable_after(self): if self.leave_type: @@ -351,6 +351,9 @@ class LeaveApplication(Document): pass def create_leave_ledger_entry(self, submit=True): + if self.status != 'Approved': + return + expiry_date = get_allocation_expiry(self.employee, self.leave_type, self.to_date, self.from_date) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 0155b278201..9e4dc12e653 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -638,11 +638,11 @@ erpnext.patches.v12_0.add_variant_of_in_item_attribute_table erpnext.patches.v12_0.rename_bank_account_field_in_journal_entry_account erpnext.patches.v12_0.create_default_energy_point_rules erpnext.patches.v12_0.set_produced_qty_field_in_sales_order_for_work_order -erpnext.patches.v12_0.generate_leave_ledger_entries erpnext.patches.v12_0.set_default_shopify_app_type erpnext.patches.v12_0.set_cwip_and_delete_asset_settings erpnext.patches.v12_0.set_expense_account_in_landed_cost_voucher_taxes 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 \ No newline at end of file +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 diff --git a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py new file mode 100644 index 00000000000..24f0e7cb211 --- /dev/null +++ b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py @@ -0,0 +1,26 @@ +# Copyright (c) 2018, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.utils import getdate, today + +def execute(): + ''' Delete leave ledger entry created + via leave applications with status != Approved ''' + if not frappe.db.a_row_exists("Leave Ledger Entry"): + return + + leave_application_list = get_denied_leave_application_list() + if leave_application_list: + delete_denied_leaves_from_leave_ledger_entry(leave_application_list) + +def get_denied_leave_application_list(): + return frappe.db.sql_list(''' Select name from `tabLeave Application` where status <> 'Approved' ''') + +def delete_denied_leaves_from_leave_ledger_entry(leave_application_list): + frappe.db.sql(''' Delete + FROM `tabLeave Ledger Entry` + WHERE + transaction_type = 'Leave Application' + AND transaction_name in {0} '''.format(tuple(leave_application_list))) #nosec \ No newline at end of file From 511780a4d4d6c1a03601f167635ef66ba4cbcb2f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 14 Nov 2019 12:47:08 +0530 Subject: [PATCH 180/679] 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 d69d0e30467d94f81f230c0facaada552ac5507c Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 14 Nov 2019 13:05:13 +0530 Subject: [PATCH 181/679] fix(patch): skip leave ledger entry creation for denied leaves (#19579) --- .../v12_0/remove_denied_leaves_from_leave_ledger.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py index 24f0e7cb211..7859606e5cb 100644 --- a/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py +++ b/erpnext/patches/v12_0/remove_denied_leaves_from_leave_ledger.py @@ -19,8 +19,10 @@ def get_denied_leave_application_list(): return frappe.db.sql_list(''' Select name from `tabLeave Application` where status <> 'Approved' ''') def delete_denied_leaves_from_leave_ledger_entry(leave_application_list): - frappe.db.sql(''' Delete - FROM `tabLeave Ledger Entry` - WHERE - transaction_type = 'Leave Application' - AND transaction_name in {0} '''.format(tuple(leave_application_list))) #nosec \ No newline at end of file + if leave_application_list: + frappe.db.sql(''' Delete + FROM `tabLeave Ledger Entry` + WHERE + transaction_type = 'Leave Application' + AND transaction_name in (%s) ''' % (', '.join(['%s'] * len(leave_application_list))), #nosec + tuple(leave_application_list)) \ No newline at end of file From ec082754b43906d95a5ccc68b5cded806efeab79 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 14 Nov 2019 13:28:24 +0530 Subject: [PATCH 182/679] fix: One serial no can be tagged in multiple invoices if used against different items (#19580) --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 0ebca8bf51e..fefd36a313a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -991,10 +991,8 @@ class SalesInvoice(SellingController): continue for serial_no in item.serial_no.split("\n"): - if serial_no and frappe.db.exists('Serial No', serial_no): - sno = frappe.get_doc('Serial No', serial_no) - sno.sales_invoice = invoice - sno.db_update() + if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code: + frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice) def validate_serial_numbers(self): """ @@ -1040,8 +1038,9 @@ class SalesInvoice(SellingController): continue for serial_no in item.serial_no.split("\n"): - sales_invoice = frappe.db.get_value("Serial No", serial_no, "sales_invoice") - if sales_invoice and self.name != sales_invoice: + 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: 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 e942f998976fc21443187f9e071f4003a5f86605 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 14 Nov 2019 15:26:18 +0530 Subject: [PATCH 183/679] Update work_order.js --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 15a33ca3298..107c79b89bc 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -540,7 +540,7 @@ erpnext.work_order = { calculate_total_cost: function(frm) { let variable_cost = flt(frm.doc.actual_operating_cost) || flt(frm.doc.planned_operating_cost); - frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost)) + frm.set_value("total_operating_cost", (flt(frm.doc.additional_operating_cost) + variable_cost)); }, set_default_warehouse: function(frm) { From c9e8a1bf96e379aaa239cd67c6aefb4233e124aa Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 14 Nov 2019 16:13:43 +0530 Subject: [PATCH 184/679] fix: Account Balance and Stock Value out of sync error message (#19526) * fix: Account Balance and Stock Value out of sync error message Added 'Make Adjustment Entry' button and enhanced message * fix: Split message and changed routing for translation --- erpnext/accounts/general_ledger.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index d4dac72601d..38f283c8d49 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -163,9 +163,16 @@ def validate_account_for_perpetual_inventory(gl_map): .format(account), StockAccountInvalidTransaction) elif account_bal != stock_bal: - frappe.throw(_("Account Balance ({0}) and Stock Value ({1}) is out of sync for account {2} and linked warehouse ({3}). Please create adjustment Journal Entry for amount {4}.") - .format(account_bal, stock_bal, account, comma_and(warehouse_list), stock_bal - account_bal), - StockValueAndAccountBalanceOutOfSync) + 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") + + frappe.throw("""{0}

{1}

+
+ +
""".format(error_reason, error_resolution, button_text), + StockValueAndAccountBalanceOutOfSync, title=_('Account Balance Out Of Sync')) def validate_cwip_accounts(gl_map): cwip_enabled = cint(frappe.get_cached_value("Company", From cf55c9c6da39c5e0fad8aa7bd7dbaa8337505179 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 14 Nov 2019 18:22:20 +0530 Subject: [PATCH 185/679] fix: stock reconciliation shwoing incorrect current serial no and qty --- .../doctype/stock_reconciliation/stock_reconciliation.py | 2 +- erpnext/stock/utils.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py index 98a8c594830..3683e60fd6b 100644 --- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py +++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py @@ -456,7 +456,7 @@ def get_qty_rate_for_serial_nos(item_code, warehouse, posting_date, posting_time } serial_nos_list = [serial_no.get("name") - for serial_no in get_available_serial_nos(item_code, warehouse)] + for serial_no in get_available_serial_nos(args)] qty = len(serial_nos_list) serial_nos = '\n'.join(serial_nos_list) diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py index d7629176a51..2c6c95393bc 100644 --- a/erpnext/stock/utils.py +++ b/erpnext/stock/utils.py @@ -293,9 +293,11 @@ def update_included_uom_in_report(columns, result, include_uom, conversion_facto row, key, value = data row[key] = value -def get_available_serial_nos(item_code, warehouse): - return frappe.get_all("Serial No", filters = {'item_code': item_code, - 'warehouse': warehouse, 'delivery_document_no': ''}) or [] +def get_available_serial_nos(args): + return frappe.db.sql(""" SELECT name from `tabSerial No` + WHERE item_code = %(item_code)s and warehouse = %(warehouse)s + and timestamp(purchase_date, purchase_time) <= timestamp(%(posting_date)s, %(posting_time)s) + """, args, as_dict=1) def add_additional_uom_columns(columns, result, include_uom, conversion_factors): if not include_uom or not conversion_factors: From d545f6fb6ba01456711d408a3459ddabd7529067 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 14 Nov 2019 19:26:49 +0530 Subject: [PATCH 186/679] fix: fetch approver from employee --- erpnext/hr/doctype/department_approver/department_approver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 7bf9905d07a..d6b66da0814 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -21,7 +21,7 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): 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", leave_approver, ['name', 'first_name', 'last_name']) + approver = frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name']) approvers.append(approver) return approvers From 74bbcb539f8303375edf6815e007017d95f8e98f Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 14 Nov 2019 22:44:15 +0530 Subject: [PATCH 187/679] fix: Ignore period closing voucher entries in accounts dashboard --- .../account_balance_timeline/account_balance_timeline.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py index 716bef381b3..43acded3a98 100644 --- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py +++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py @@ -93,7 +93,8 @@ def get_gl_entries(account, to_date): fields = ['posting_date', 'debit', 'credit'], filters = [ dict(posting_date = ('<', to_date)), - dict(account = ('in', child_accounts)) + dict(account = ('in', child_accounts)), + dict(voucher_type = ('!=', 'Period Closing Voucher')) ], order_by = 'posting_date asc') From 57bd1308eba1c7ced40121546b189215a286a210 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 15 Nov 2019 08:25:48 +0530 Subject: [PATCH 188/679] 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 189/679] 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 a076bddd833e6ec82fbfdf87cbbe0bbee3147368 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 15 Nov 2019 12:03:06 +0530 Subject: [PATCH 190/679] fix: Show 'Bill of Materials' custom button conditionally --- .../stock/doctype/stock_entry/stock_entry.js | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index cee09e78c6e..bb85ef0c484 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -181,12 +181,6 @@ frappe.ui.form.on('Stock Entry', { } } - if (frm.doc.docstatus === 0) { - frm.add_custom_button(__('Bill of Materials'), function(){ - frm.events.get_items_from_bom(frm); - }, __("Get items from")); - } - if (frm.doc.docstatus===0) { frm.add_custom_button(__('Purchase Invoice'), function() { erpnext.utils.map_current_doc({ @@ -257,6 +251,17 @@ frappe.ui.form.on('Stock Entry', { frm.trigger("setup_quality_inspection"); }, + stock_entry_type: function(frm){ + frm.remove_custom_button('Bill of Materials', "Get items from"); + + if (frm.doc.docstatus === 0 && ['Material Issue','Material Receipt', + 'Material Transfer','Send to Subcontractor'].includes(frm.doc.purpose)) { + frm.add_custom_button(__('Bill of Materials'), function(){ + frm.events.get_items_from_bom(frm); + }, __("Get items from")); + } + }, + purpose: function(frm) { frm.trigger('validate_purpose_consumption'); frm.fields_dict.items.grid.refresh(); @@ -411,10 +416,10 @@ frappe.ui.form.on('Stock Entry', { "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1}, {"fieldname":"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"} ] - if (frm.doc.stock_entry_type == 'Material Issue'){ + if (frm.doc.purpose == 'Material Issue'){ fields.splice(2,1); } - else if(frm.doc.stock_entry_type == 'Material Receipt'){ + else if(frm.doc.purpose == 'Material Receipt'){ fields.splice(1,1); } From 3798f8bd25d6ac02c7d09da61e45db8c9a5c51e4 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 15 Nov 2019 13:59:15 +0530 Subject: [PATCH 191/679] style(requirements): alphabetically sorted requirements --- requirements.txt | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/requirements.txt b/requirements.txt index 429f894edce..1ab84ad57b5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ -frappe -Unidecode==1.1.1 -PyGithub==1.43.8 -googlemaps==3.1.1 -python-stdnum==1.11 braintree==3.57.1 +frappe gocardless-pro==1.10.0 -WooCommerce==2.1.1 +googlemaps==3.1.1 pandas==0.25.1 plaid-python==3.4.0 +PyGithub==1.43.8 +python-stdnum==1.11 +Unidecode==1.1.1 +WooCommerce==2.1.1 From 28a7ce9a5027b47b2718d5a4264955ec57432910 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 28 Oct 2019 11:23:14 +0530 Subject: [PATCH 192/679] fix: On Specific case if no item code in name --- erpnext/manufacturing/doctype/bom/bom.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 225ae29429e..c15b52ea38a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -39,9 +39,11 @@ class BOM(WebsiteGenerator): names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)] # split by (-) if cancelled - names = [cint(name.split('-')[-1]) for name in names] - - idx = max(names) + 1 + if names: + names = [cint(name.split('-')[-1]) for name in names] + idx = max(names) + 1 + else: + idx = 1 else: idx = 1 From 642441688687286ba0fce0df1b2319529c723bdd Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Fri, 15 Nov 2019 14:18:45 +0530 Subject: [PATCH 193/679] fix: sales order item shwoing incorrect produced qty (#19584) --- .../doctype/work_order/work_order.py | 4 +++- ...ed_qty_field_in_sales_order_for_work_order.py | 14 +++++++++----- .../selling/doctype/sales_order/sales_order.py | 16 ++++++++++------ 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 6ea3dc83ed6..089cb8014d2 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -216,7 +216,9 @@ class WorkOrder(Document): self.db_set(fieldname, qty) from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item - update_produced_qty_in_so_item(self.sales_order_item) + + if self.sales_order and self.sales_order_item: + update_produced_qty_in_so_item(self.sales_order, self.sales_order_item) if self.production_plan: self.update_production_plan_status() diff --git a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py index 44d8fa767af..07026732fd4 100644 --- a/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py +++ b/erpnext/patches/v12_0/set_produced_qty_field_in_sales_order_for_work_order.py @@ -3,8 +3,12 @@ from frappe.utils import flt from erpnext.selling.doctype.sales_order.sales_order import update_produced_qty_in_so_item def execute(): - frappe.reload_doctype('Sales Order Item') - frappe.reload_doctype('Sales Order') - sales_order_items = frappe.db.get_all('Sales Order Item', ['name']) - for so_item in sales_order_items: - update_produced_qty_in_so_item(so_item.get('name')) \ No newline at end of file + frappe.reload_doctype('Sales Order Item') + frappe.reload_doctype('Sales Order') + + for d in frappe.get_all('Work Order', + fields = ['sales_order', 'sales_order_item'], + filters={'sales_order': ('!=', ''), 'sales_order_item': ('!=', '')}): + + # update produced qty in sales order + update_produced_qty_in_so_item(d.sales_order, d.sales_order_item) \ No newline at end of file diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index c4c3c0f81e3..e12b359bdf1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1038,14 +1038,18 @@ def create_pick_list(source_name, target_doc=None): return doc -def update_produced_qty_in_so_item(sales_order_item): +def update_produced_qty_in_so_item(sales_order, sales_order_item): #for multiple work orders against same sales order item linked_wo_with_so_item = frappe.db.get_all('Work Order', ['produced_qty'], { 'sales_order_item': sales_order_item, + 'sales_order': sales_order, 'docstatus': 1 }) - if len(linked_wo_with_so_item) > 0: - total_produced_qty = 0 - for wo in linked_wo_with_so_item: - total_produced_qty += flt(wo.get('produced_qty')) - frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) \ No newline at end of file + + total_produced_qty = 0 + for wo in linked_wo_with_so_item: + total_produced_qty += flt(wo.get('produced_qty')) + + if not total_produced_qty and frappe.flags.in_patch: return + + frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty) \ No newline at end of file From 53b65ab8ed82d4398e657911ee24c4c6d70af14f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 15 Nov 2019 16:42:32 +0530 Subject: [PATCH 194/679] 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 195/679] 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 73089470b18f2a04a28b3ba3391b1605e12becf4 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 15 Nov 2019 17:38:32 +0530 Subject: [PATCH 196/679] chore: pinned backwards compatible dependencies --- requirements.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index 1ab84ad57b5..c277545fab5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,10 @@ braintree==3.57.1 frappe -gocardless-pro==1.10.0 +gocardless-pro==1.11.0 googlemaps==3.1.1 -pandas==0.25.1 +pandas==0.24.2 plaid-python==3.4.0 -PyGithub==1.43.8 -python-stdnum==1.11 +PyGithub==1.44.1 +python-stdnum==1.12 Unidecode==1.1.1 WooCommerce==2.1.1 From d995609ffa5e3903fecb3102f359992688e63401 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 18 Nov 2019 11:46:55 +0530 Subject: [PATCH 197/679] Fixed asset refactor (#19369) * refactor: Asset Movement with multiple assets using table * refactor: Create Asset Movement from Asset List & Linking PR/PI with Asset * feat: Auto create asset on Purchase checkbox * refactor: LCV for asset created via PR/PI * refactor: get asset category accounts from item master * refactor: Purchase Receipt for asset purchasing * refactor: Purchase Invoice for asset purchasing * fix: post-refactor delete fixes * refactor: moved asset validation from pr/pi on asset submission * fix: Asset Category should be defined for auto purchasing assets * fix: undo serial_no_update removal (for non asset item) from LCV * fix: remove duplicate calls from item.js * fix: args position of all occurrence of get_asset_category_account() * fix: test cases * fix: landed cost voucher validations * refactor: test case for auto creation of asset * fix: removed invalid assertions * fix: patch errors on travis * fix: codacy fixes * fix: PI Items not fetching details from item * fix: asset movement from list view having default purpose 'Receipt' * chore: msgprint for selecting item code first while creating asset manually * fix: alert messages * minor: asset movement fixes * fix: lcv was made against submitted assets * minor: ux fixes * refac: move specific asset validation to SINV * chore: remove make_purchase_invoice from asset form * make asset movement on asset submission * add PR & PI queries based on item code * refac: not allow last movement cancellation * move asset movement creation on asset submission * asset movement naming series * add tests * fix: code review changes * chore: remove unecessary asset movement updation * refac: setting latest location while making asset movements * Added extra validations * fix: form dashboard make lcv button * fix: auto asset movement creation validation * fix: allow lcv against other items after removing submitted assets * chore: remove unwanted condition * fix: mismatch debit credit on purchase of asset with valuation tax * chore: toggle required field based on movement type * chore: fix lcv error message * fix: travis failing * fix: travis failing test * fix: wrong conditions after merge * fix: cannot cancel assets * fix: travis failing* fix change in deletion of assets * fix: codacy * fix: process cancellation of assets * refac: cancellation of pr only deletes auto created assets * fix: incorrect query --- .../purchase_invoice/purchase_invoice.js | 22 +- .../purchase_invoice/purchase_invoice.py | 108 +- .../purchase_invoice_item.json | 30 +- .../doctype/sales_invoice/sales_invoice.py | 10 + erpnext/assets/doctype/asset/asset.js | 161 +- erpnext/assets/doctype/asset/asset.json | 11 +- erpnext/assets/doctype/asset/asset.py | 126 +- erpnext/assets/doctype/asset/asset_list.js | 19 +- erpnext/assets/doctype/asset/test_asset.py | 1030 ++++---- .../doctype/asset_category/asset_category.py | 7 +- .../test_asset_maintenance.py | 8 +- .../doctype/asset_movement/asset_movement.js | 145 +- .../asset_movement/asset_movement.json | 103 +- .../doctype/asset_movement/asset_movement.py | 179 +- .../asset_movement/test_asset_movement.py | 130 +- .../doctype/asset_movement_item/__init__.py | 0 .../asset_movement_item.json | 86 + .../asset_movement_item.py | 10 + .../purchase_order_item.json | 11 +- erpnext/controllers/accounts_controller.py | 42 - erpnext/controllers/buying_controller.py | 137 +- erpnext/controllers/queries.py | 26 + erpnext/hr/doctype/employee/test_employee.py | 6 +- ..._asset_finance_book_against_old_entries.py | 4 - erpnext/stock/doctype/item/item.js | 21 +- erpnext/stock/doctype/item/item.json | 2217 +++++++++-------- .../landed_cost_item/landed_cost_item.json | 16 +- .../landed_cost_voucher.js | 4 + .../landed_cost_voucher.json | 648 +---- .../landed_cost_voucher.py | 43 +- .../purchase_receipt/purchase_receipt.js | 32 +- .../purchase_receipt/purchase_receipt.py | 157 +- .../purchase_receipt/test_purchase_receipt.py | 32 +- .../purchase_receipt_item.json | 53 +- 34 files changed, 2817 insertions(+), 2817 deletions(-) create mode 100644 erpnext/assets/doctype/asset_movement_item/__init__.py create mode 100644 erpnext/assets/doctype/asset_movement_item/asset_movement_item.json create mode 100644 erpnext/assets/doctype/asset_movement_item/asset_movement_item.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index f4b656d3f68..e4e2c7b10f3 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -331,15 +331,15 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ }) }, - asset: function(frm, cdt, cdn) { + item_code: function(frm, cdt, cdn) { var row = locals[cdt][cdn]; - if(row.asset) { + if(row.item_code) { frappe.call({ method: "erpnext.assets.doctype.asset_category.asset_category.get_asset_category_account", args: { - "asset": row.asset, + "item": row.item_code, "fieldname": "fixed_asset_account", - "account": row.expense_account + "company": frm.doc.company }, callback: function(r, rt) { frappe.model.set_value(cdt, cdn, "expense_account", r.message); @@ -430,19 +430,7 @@ cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) cur_frm.set_query("expense_account", "items", function(doc) { return { query: "erpnext.controllers.queries.get_expense_account", - filters: {'company': doc.company} - } -}); - -cur_frm.set_query("asset", "items", function(doc, cdt, cdn) { - var d = locals[cdt][cdn]; - return { - filters: { - 'item_code': d.item_code, - 'docstatus': 1, - 'company': doc.company, - 'status': 'Submitted' - } + filters: {'company': doc.company } } }); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index f1c490e2cdb..4fbf9a10095 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -98,7 +98,6 @@ class PurchaseInvoice(BuyingController): self.set_against_expense_account() self.validate_write_off_account() self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") - self.validate_fixed_asset() self.create_remarks() self.set_status() self.validate_purchase_receipt_if_update_stock() @@ -238,13 +237,8 @@ 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): - if not item.asset: - frappe.throw(_("Row {0}: asset is required for item {1}") - .format(item.idx, item.item_code)) - - item.expense_account = get_asset_category_account(item.asset, 'fixed_asset_account', + 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: item.expense_account = asset_received_but_not_billed @@ -363,7 +357,7 @@ class PurchaseInvoice(BuyingController): return if not gl_entries: gl_entries = self.get_gl_entries() - + if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -510,19 +504,49 @@ class PurchaseInvoice(BuyingController): asset_category)): expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) + + if not item.is_fixed_asset: + amount = flt(item.base_net_amount, item.precision("base_net_amount")) + else: + amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) - gl_entries.append( - self.get_gl_dict({ + gl_entries.append(self.get_gl_dict({ "account": expense_account, "against": self.supplier, - "debit": flt(item.base_net_amount, item.precision("base_net_amount")), - "debit_in_account_currency": (flt(item.base_net_amount, - item.precision("base_net_amount")) if account_currency==self.company_currency - else flt(item.net_amount, item.precision("net_amount"))), + "debit": amount, "cost_center": item.cost_center, "project": item.project - }, account_currency, item=item) - ) + }, account_currency, item=item)) + + # If asset is bought through this document and not linked to PR + if self.update_stock and item.landed_cost_voucher_amount: + expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") + # Amount added through landed-cost-voucher + gl_entries.append(self.get_gl_dict({ + "account": expenses_included_in_asset_valuation, + "against": expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project + }, item=item)) + + gl_entries.append(self.get_gl_dict({ + "account": expense_account, + "against": expenses_included_in_asset_valuation, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project + }, item=item)) + + # update gross amount of asset bought through this document + assets = frappe.db.get_all('Asset', + filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } + ) + for asset in assets: + frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) + frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) if self.auto_accounting_for_stock and self.is_opening == "No" and \ item.item_code in stock_items and item.item_tax_amount: @@ -547,30 +571,27 @@ class PurchaseInvoice(BuyingController): item.precision("item_tax_amount")) def get_asset_gl_entry(self, gl_entries): + arbnb_account = self.get_company_default("asset_received_but_not_billed") + eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") + 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 item.is_fixed_asset and is_cwip_accounting_enabled(self.company, asset_category) : - eiiav_account = self.get_company_default("expenses_included_in_asset_valuation") - + if item.is_fixed_asset: 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) - if (not item.expense_account or frappe.db.get_value('Account', - item.expense_account, 'account_type') not in ['Asset Received But Not Billed', 'Fixed Asset']): - arbnb_account = self.get_company_default("asset_received_but_not_billed") + item_exp_acc_type = frappe.db.get_value('Account', item.expense_account, 'account_type') + if (not item.expense_account or item_exp_acc_type not in ['Asset Received But Not Billed', 'Fixed Asset']): item.expense_account = arbnb_account if not self.update_stock: - asset_rbnb_currency = get_account_currency(item.expense_account) + arbnb_currency = get_account_currency(item.expense_account) gl_entries.append(self.get_gl_dict({ "account": item.expense_account, "against": self.supplier, "remarks": self.get("remarks") or _("Accounting Entry for Asset"), "debit": base_asset_amount, "debit_in_account_currency": (base_asset_amount - if asset_rbnb_currency == self.company_currency else asset_amount), + if arbnb_currency == self.company_currency else asset_amount), "cost_center": item.cost_center }, item=item)) @@ -587,8 +608,7 @@ class PurchaseInvoice(BuyingController): item.item_tax_amount / self.conversion_rate) }, item=item)) else: - cwip_account = get_asset_account("capital_work_in_progress_account", - item.asset, company = self.company) + cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) cwip_account_currency = get_account_currency(cwip_account) gl_entries.append(self.get_gl_dict({ @@ -613,6 +633,36 @@ class PurchaseInvoice(BuyingController): if asset_eiiav_currency == self.company_currency else item.item_tax_amount / self.conversion_rate) }, item=item)) + + # When update stock is checked + # Assets are bought through this document then it will be linked to this document + if self.update_stock: + if flt(item.landed_cost_voucher_amount): + gl_entries.append(self.get_gl_dict({ + "account": eiiav_account, + "against": cwip_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project + }, item=item)) + + gl_entries.append(self.get_gl_dict({ + "account": cwip_account, + "against": eiiav_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project + }, item=item)) + + # update gross amount of assets bought through this document + assets = frappe.db.get_all('Asset', + filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } + ) + for asset in assets: + frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) + frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) return gl_entries 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 3a19bb1b6bf..dc3a1be0c7e 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -71,8 +71,8 @@ "expense_account", "col_break5", "is_fixed_asset", - "asset", "asset_location", + "asset_category", "deferred_expense_section", "deferred_expense_account", "service_stop_date", @@ -116,6 +116,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.item_name", "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -191,6 +192,7 @@ "fieldtype": "Column Break" }, { + "fetch_from": "item_code.stock_uom", "fieldname": "uom", "fieldtype": "Link", "label": "UOM", @@ -414,6 +416,7 @@ "print_hide": 1 }, { + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "batch_no", "fieldtype": "Link", "label": "Batch No", @@ -425,12 +428,14 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "serial_no", "fieldtype": "Text", "label": "Serial No", "no_copy": 1 }, { + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "rejected_serial_no", "fieldtype": "Text", "label": "Rejected Serial No", @@ -615,6 +620,7 @@ }, { "default": "0", + "fetch_from": "item_code.is_fixed_asset", "fieldname": "is_fixed_asset", "fieldtype": "Check", "hidden": 1, @@ -623,14 +629,6 @@ "print_hide": 1, "read_only": 1 }, - { - "depends_on": "is_fixed_asset", - "fieldname": "asset", - "fieldtype": "Link", - "label": "Asset", - "no_copy": 1, - "options": "Asset" - }, { "depends_on": "is_fixed_asset", "fieldname": "asset_location", @@ -676,7 +674,7 @@ "fieldname": "pr_detail", "fieldtype": "Data", "hidden": 1, - "label": "PR Detail", + "label": "Purchase Receipt Detail", "no_copy": 1, "oldfieldname": "pr_detail", "oldfieldtype": "Data", @@ -754,11 +752,21 @@ "fieldtype": "Data", "label": "Manufacturer Part Number", "read_only": 1 + }, + { + "depends_on": "is_fixed_asset", + "fetch_from": "item_code.asset_category", + "fieldname": "asset_category", + "fieldtype": "Data", + "in_preview": 1, + "label": "Asset Category", + "options": "Asset Category", + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-09-17 22:32:05.984240", + "modified": "2019-11-03 13:43:23.782877", "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 fefd36a313a..3d96d487a84 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -135,7 +135,17 @@ 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: + asset = frappe.get_doc("Asset", d.asset) + if self.doctype == "Sales Invoice" and self.docstatus == 1: + if self.update_stock: + frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) + elif asset.status in ("Scrapped", "Cancelled", "Sold"): + frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) def before_save(self): set_account_for_mode_of_payment(self) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index c7390a2ef11..f0889bfa1b2 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -41,6 +41,21 @@ frappe.ui.form.on('Asset', { }); }, + setup: function(frm) { + frm.set_query("purchase_receipt", (doc) => { + return { + query: "erpnext.controllers.queries.get_purchase_receipts", + filters: { item_code: doc.item_code } + } + }); + frm.set_query("purchase_invoice", (doc) => { + return { + query: "erpnext.controllers.queries.get_purchase_invoices", + filters: { item_code: doc.item_code } + } + }); + }, + refresh: function(frm) { frappe.ui.form.trigger("Asset", "is_existing_asset"); frm.toggle_display("next_depreciation_date", frm.doc.docstatus < 1); @@ -78,11 +93,6 @@ frappe.ui.form.on('Asset', { }); } - if (frm.doc.status=='Submitted' && !frm.doc.is_existing_asset && !frm.doc.purchase_invoice) { - frm.add_custom_button(__("Purchase Invoice"), function() { - frm.trigger("make_purchase_invoice"); - }, __('Create')); - } if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) { frm.add_custom_button(__("Asset Maintenance"), function() { frm.trigger("create_asset_maintenance"); @@ -104,11 +114,36 @@ frappe.ui.form.on('Asset', { frm.trigger("setup_chart"); } + frm.trigger("toggle_reference_doc"); + if (frm.doc.docstatus == 0) { frm.toggle_reqd("finance_books", frm.doc.calculate_depreciation); } }, + toggle_reference_doc: function(frm) { + if (frm.doc.purchase_receipt && frm.doc.purchase_invoice && frm.doc.docstatus === 1) { + frm.set_df_property('purchase_invoice', 'read_only', 1); + frm.set_df_property('purchase_receipt', 'read_only', 1); + } + else if (frm.doc.purchase_receipt) { + // if purchase receipt link is set then set PI disabled + frm.toggle_reqd('purchase_invoice', 0); + frm.set_df_property('purchase_invoice', 'read_only', 1); + } + else if (frm.doc.purchase_invoice) { + // if purchase invoice link is set then set PR disabled + frm.toggle_reqd('purchase_receipt', 0); + frm.set_df_property('purchase_receipt', 'read_only', 1); + } + else { + frm.toggle_reqd('purchase_receipt', 1); + frm.set_df_property('purchase_receipt', 'read_only', 0); + frm.toggle_reqd('purchase_invoice', 1); + frm.set_df_property('purchase_invoice', 'read_only', 0); + } + }, + make_journal_entry: function(frm) { frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_journal_entry", @@ -176,21 +211,25 @@ frappe.ui.form.on('Asset', { item_code: function(frm) { if(frm.doc.item_code) { - frappe.call({ - method: "erpnext.assets.doctype.asset.asset.get_item_details", - args: { - item_code: frm.doc.item_code, - asset_category: frm.doc.asset_category - }, - callback: function(r, rt) { - if(r.message) { - frm.set_value('finance_books', r.message); - } - } - }) + frm.trigger('set_finance_book'); } }, + set_finance_book: function(frm) { + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.get_item_details", + args: { + item_code: frm.doc.item_code, + asset_category: frm.doc.asset_category + }, + callback: function(r, rt) { + if(r.message) { + frm.set_value('finance_books', r.message); + } + } + }) + }, + available_for_use_date: function(frm) { $.each(frm.doc.finance_books || [], function(i, d) { if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date; @@ -207,29 +246,14 @@ frappe.ui.form.on('Asset', { }, make_schedules_editable: function(frm) { - var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 - ? true : false; + if (frm.doc.finance_books) { + var is_editable = frm.doc.finance_books.filter(d => d.depreciation_method == "Manual").length > 0 + ? true : false; - frm.toggle_enable("schedules", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable); - frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable); - }, - - make_purchase_invoice: function(frm) { - frappe.call({ - args: { - "asset": frm.doc.name, - "item_code": frm.doc.item_code, - "gross_purchase_amount": frm.doc.gross_purchase_amount, - "company": frm.doc.company, - "posting_date": frm.doc.purchase_date - }, - method: "erpnext.assets.doctype.asset.asset.make_purchase_invoice", - callback: function(r) { - var doclist = frappe.model.sync(r.message); - frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }) + frm.toggle_enable("schedules", is_editable); + frm.fields_dict["schedules"].grid.toggle_enable("schedule_date", is_editable); + frm.fields_dict["schedules"].grid.toggle_enable("depreciation_amount", is_editable); + } }, make_sales_invoice: function(frm) { @@ -291,6 +315,65 @@ frappe.ui.form.on('Asset', { }) }, + purchase_receipt: function(frm) { + frm.trigger('toggle_reference_doc'); + + if (frm.doc.purchase_receipt) { + if (frm.doc.item_code) { + frappe.db.get_doc('Purchase Receipt', frm.doc.purchase_receipt).then(pr_doc => { + frm.set_value('company', pr_doc.company); + frm.set_value('purchase_date', pr_doc.posting_date); + const item = pr_doc.items.find(item => item.item_code === frm.doc.item_code); + if (!item) { + frm.set_value('purchase_receipt', ''); + frappe.msgprint({ + title: __('Invalid Purchase Receipt'), + message: __("The selected Purchase Receipt doesn't contains selected Asset Item."), + indicator: 'red' + }); + } + frm.set_value('gross_purchase_amount', item.base_net_rate); + frm.set_value('location', item.asset_location); + }); + } else { + frm.set_value('purchase_receipt', ''); + frappe.msgprint({ + title: __('Not Allowed'), + message: __("Please select Item Code first") + }); + } + } + }, + + purchase_invoice: function(frm) { + frm.trigger('toggle_reference_doc'); + if (frm.doc.purchase_invoice) { + if (frm.doc.item_code) { + frappe.db.get_doc('Purchase Invoice', frm.doc.purchase_invoice).then(pi_doc => { + frm.set_value('company', pi_doc.company); + frm.set_value('purchase_date', pi_doc.posting_date); + const item = pi_doc.items.find(item => item.item_code === frm.doc.item_code); + if (!item) { + frm.set_value('purchase_invoice', ''); + frappe.msgprint({ + title: __('Invalid Purchase Invoice'), + message: __("The selected Purchase Invoice doesn't contains selected Asset Item."), + indicator: 'red' + }); + } + frm.set_value('gross_purchase_amount', item.base_net_rate); + frm.set_value('location', item.asset_location); + }); + } else { + frm.set_value('purchase_invoice', ''); + frappe.msgprint({ + title: __('Not Allowed'), + message: __("Please select Item Code first") + }); + } + } + }, + set_depreciation_rate: function(frm, row) { if (row.total_number_of_depreciations && row.frequency_of_depreciation && row.expected_value_after_useful_life) { diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json index 6882f6a9924..97165a31d29 100644 --- a/erpnext/assets/doctype/asset/asset.json +++ b/erpnext/assets/doctype/asset/asset.json @@ -17,6 +17,7 @@ "supplier", "customer", "image", + "purchase_invoice", "column_break_3", "company", "location", @@ -25,6 +26,7 @@ "purchase_date", "disposal_date", "journal_entry_for_scrap", + "purchase_receipt", "accounting_dimensions_section", "cost_center", "dimension_col_break", @@ -62,9 +64,8 @@ "status", "booked_fixed_asset", "column_break_51", - "purchase_receipt", + "purchase_receipt_amount", - "purchase_invoice", "default_finance_book", "amended_from" ], @@ -403,8 +404,7 @@ "label": "Purchase Receipt", "no_copy": 1, "options": "Purchase Receipt", - "print_hide": 1, - "read_only": 1 + "print_hide": 1 }, { "fieldname": "purchase_receipt_amount", @@ -420,8 +420,7 @@ "fieldtype": "Link", "label": "Purchase Invoice", "no_copy": 1, - "options": "Purchase Invoice", - "read_only": 1 + "options": "Purchase Invoice" }, { "fetch_from": "company.default_finance_book", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d1f8c1a8d3d..9415eedc5c4 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -18,6 +18,7 @@ from erpnext.controllers.accounts_controller import AccountsController class Asset(AccountsController): def validate(self): self.validate_asset_values() + self.validate_asset_and_reference() self.validate_item() self.set_missing_values() self.prepare_depreciation_data() @@ -29,10 +30,13 @@ class Asset(AccountsController): def on_submit(self): self.validate_in_use_date() self.set_status() - self.update_stock_movement() + self.make_asset_movement() if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.company, self.asset_category): self.make_gl_entries() + + def before_cancel(self): + self.cancel_auto_gen_movement() def on_cancel(self): self.validate_cancellation() @@ -40,6 +44,18 @@ class Asset(AccountsController): self.set_status() delete_gl_entries(voucher_type='Asset', voucher_no=self.name) self.db_set('booked_fixed_asset', 0) + + def validate_asset_and_reference(self): + if self.purchase_invoice or self.purchase_receipt: + reference_doc = 'Purchase Invoice' if self.purchase_invoice else 'Purchase Receipt' + reference_name = self.purchase_invoice or self.purchase_receipt + reference_doc = frappe.get_doc(reference_doc, reference_name) + if reference_doc.get('company') != self.company: + frappe.throw(_("Company of asset {0} and purchase document {1} doesn't matches.").format(self.name, reference_doc.get('name'))) + + + if self.is_existing_asset and self.purchase_invoice: + frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) def prepare_depreciation_data(self): if self.calculate_depreciation: @@ -109,6 +125,36 @@ class Asset(AccountsController): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): frappe.throw(_("Available-for-use Date should be after purchase date")) + def cancel_auto_gen_movement(self): + reference_docname = self.purchase_invoice or self.purchase_receipt + movement = frappe.db.get_all('Asset Movement', filters={ 'reference_name': reference_docname, 'docstatus': 1 }) + if len(movement) > 1: + frappe.throw(_('Asset has multiple Asset Movement Entries which has to be \ + cancelled manually to cancel this asset.')) + movement = frappe.get_doc('Asset Movement', movement[0].get('name')) + movement.flags.ignore_validate = True + movement.cancel() + + def make_asset_movement(self): + reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' + reference_docname = self.purchase_receipt or self.purchase_invoice + assets = [{ + 'asset': self.name, + 'asset_name': self.asset_name, + 'target_location': self.location, + 'to_employee': self.custodian + }] + asset_movement = frappe.get_doc({ + 'doctype': 'Asset Movement', + 'assets': assets, + 'purpose': 'Receipt', + 'company': self.company, + 'transaction_date': getdate(nowdate()), + 'reference_doctype': reference_doctype, + 'reference_name': reference_docname + }).insert() + asset_movement.submit() + def set_depreciation_rate(self): for d in self.get("finance_books"): d.rate_of_depreciation = flt(self.get_depreciation_rate(d, on_validate=True), @@ -398,22 +444,13 @@ class Asset(AccountsController): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 - def update_stock_movement(self): - asset_movement = frappe.db.get_value('Asset Movement', - {'asset': self.name, 'reference_name': self.purchase_receipt, 'docstatus': 0}, 'name') - - if asset_movement: - doc = frappe.get_doc('Asset Movement', asset_movement) - doc.naming_series = 'ACC-ASM-.YYYY.-' - doc.submit() - def make_gl_entries(self): gl_entries = [] - if ((self.purchase_receipt or (self.purchase_invoice and - frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock'))) + if ((self.purchase_receipt \ + or (self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock'))) and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): - fixed_aseet_account = get_asset_category_account(self.name, 'fixed_asset_account', + fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, asset_category = self.asset_category, company = self.company) cwip_account = get_asset_account("capital_work_in_progress_account", @@ -421,7 +458,7 @@ class Asset(AccountsController): gl_entries.append(self.get_gl_dict({ "account": cwip_account, - "against": fixed_aseet_account, + "against": fixed_asset_account, "remarks": self.get("remarks") or _("Accounting Entry for Asset"), "posting_date": self.available_for_use_date, "credit": self.purchase_receipt_amount, @@ -430,7 +467,7 @@ class Asset(AccountsController): })) gl_entries.append(self.get_gl_dict({ - "account": fixed_aseet_account, + "account": fixed_asset_account, "against": cwip_account, "remarks": self.get("remarks") or _("Accounting Entry for Asset"), "posting_date": self.available_for_use_date, @@ -491,25 +528,6 @@ def get_asset_naming_series(): meta = frappe.get_meta('Asset') return meta.get_field("naming_series").options -@frappe.whitelist() -def make_purchase_invoice(asset, item_code, gross_purchase_amount, company, posting_date): - pi = frappe.new_doc("Purchase Invoice") - pi.company = company - pi.currency = frappe.get_cached_value('Company', company, "default_currency") - pi.set_posting_time = 1 - pi.posting_date = posting_date - pi.append("items", { - "item_code": item_code, - "is_fixed_asset": 1, - "asset": asset, - "expense_account": get_asset_category_account(asset, 'fixed_asset_account'), - "qty": 1, - "price_list_rate": gross_purchase_amount, - "rate": gross_purchase_amount - }) - pi.set_missing_values() - return pi - @frappe.whitelist() def make_sales_invoice(asset, item_code, company, serial_no=None): si = frappe.new_doc("Sales Invoice") @@ -584,7 +602,7 @@ def get_item_details(item_code, asset_category): def get_asset_account(account_name, asset=None, asset_category=None, company=None): account = None if asset: - account = get_asset_category_account(asset, account_name, + account = get_asset_category_account(account_name, asset=asset, asset_category = asset_category, company = company) if not account: @@ -627,6 +645,44 @@ def make_journal_entry(asset_name): return je +@frappe.whitelist() +def make_asset_movement(assets): + import json + from six import string_types + + if isinstance(assets, string_types): + assets = json.loads(assets) + + if len(assets) == 0: + frappe.throw(_('Atleast one asset has to be selected.')) + + asset_movement = frappe.new_doc("Asset Movement") + asset_movement.quantity = len(assets) + prev_reference_docname = '' + + for asset in assets: + asset = frappe.get_doc('Asset', asset.get('name')) + # get PR/PI linked with asset + reference_docname = asset.get('purchase_receipt') if asset.get('purchase_receipt') \ + else asset.get('purchase_invoice') + # checks if all the assets are linked with a single PR/PI + if prev_reference_docname == '': + prev_reference_docname = reference_docname + elif prev_reference_docname != reference_docname: + frappe.throw(_('Assets selected should belong to same reference document.')) + + asset_movement.company = asset.get('company') + asset_movement.reference_doctype = 'Purchase Receipt' if asset.get('purchase_receipt') else 'Purchase Invoice' + asset_movement.reference_name = prev_reference_docname + asset_movement.append("assets", { + 'asset': asset.get('name'), + 'source_location': asset.get('location'), + 'from_employee': asset.get('custodian') + }) + + 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")) diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 3b95a17afce..46cde6ee812 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -30,8 +30,23 @@ frappe.listview_settings['Asset'] = { } else if (doc.status === "Draft") { return [__("Draft"), "red", "status,=,Draft"]; - } - + }, + onload: function(me) { + me.page.add_action_item('Make Asset Movement', function() { + const assets = me.get_checked_items(); + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + args:{ + "assets": assets + }, + callback: function (r) { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }); }, } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 7085b31e050..53fd6d394d8 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -7,7 +7,7 @@ import frappe import unittest from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, add_months from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset -from erpnext.assets.doctype.asset.asset import make_sales_invoice, make_purchase_invoice +from erpnext.assets.doctype.asset.asset import make_sales_invoice from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice @@ -39,15 +39,15 @@ class TestAsset(unittest.TestCase): }) asset.submit() - pi = make_purchase_invoice(asset.name, asset.item_code, asset.gross_purchase_amount, - asset.company, asset.purchase_date) + pi = make_invoice(pr.name) pi.supplier = "_Test Supplier" pi.insert() pi.submit() asset.load_from_db() self.assertEqual(asset.supplier, "_Test Supplier") self.assertEqual(asset.purchase_date, getdate(purchase_date)) - self.assertEqual(asset.purchase_invoice, pi.name) + # Asset won't have reference to PI when purchased through PR + self.assertEqual(asset.purchase_receipt, pr.name) expected_gle = ( ("Asset Received But Not Billed - _TC", 100000.0, 0.0), @@ -60,521 +60,517 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) pi.cancel() - + asset.cancel() asset.load_from_db() - self.assertEqual(asset.supplier, None) - self.assertEqual(asset.purchase_invoice, None) + pr.load_from_db() + pr.cancel() + self.assertEqual(asset.docstatus, 2) 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): - from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( - make_purchase_invoice as make_purchase_invoice_from_pr) - - #frappe.db.set_value("Asset Category","Computers","enable_cwip_accounting", 1) - - 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) - ) - - 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(gle, expected_gle) - - pi = make_purchase_invoice_from_pr(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), - ) - - 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(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"): @@ -636,6 +632,8 @@ def create_asset_category(): asset_category.insert() def create_fixed_asset_item(): + meta = frappe.get_meta('Asset') + naming_series = meta.get_field("naming_series").options.splitlines()[0] or 'ACC-ASS-.YYYY.-' try: frappe.get_doc({ "doctype": "Item", @@ -646,7 +644,9 @@ def create_fixed_asset_item(): "item_group": "All Item Groups", "stock_uom": "Nos", "is_stock_item": 0, - "is_fixed_asset": 1 + "is_fixed_asset": 1, + "auto_create_assets": 1, + "asset_naming_series": naming_series }).insert() except frappe.DuplicateEntryError: pass diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 5cb634abcd4..14f3922c05f 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -29,8 +29,11 @@ class AssetCategory(Document): frappe.bold(d.idx), frappe.bold(d.company_name))) @frappe.whitelist() -def get_asset_category_account(asset, fieldname, account=None, asset_category = None, company = None): - if not asset_category and company: +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"): + asset_category = frappe.db.get_value("Item", item, ["asset_category"]) + + elif not asset_category or not company: if account: if frappe.db.get_value("Account", account, "account_type") != "Fixed Asset": account=None diff --git a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py index 735302a0c3b..6c2fd67a9af 100644 --- a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py @@ -73,8 +73,10 @@ def create_asset_data(): 'doctype': 'Location', 'location_name': 'Test Location' }).insert() - + if not frappe.db.exists("Item", "Photocopier"): + meta = frappe.get_meta('Asset') + naming_series = meta.get_field("naming_series").options frappe.get_doc({ "doctype": "Item", "item_code": "Photocopier", @@ -83,7 +85,9 @@ def create_asset_data(): "company": "_Test Company", "is_fixed_asset": 1, "is_stock_item": 0, - "asset_category": "Equipment" + "asset_category": "Equipment", + "auto_create_assets": 1, + "asset_naming_series": naming_series }).insert() def create_maintenance_team(): diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index 7ef6461b5a3..a71212ea47b 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -2,27 +2,138 @@ // For license information, please see license.txt frappe.ui.form.on('Asset Movement', { - select_serial_no: function(frm) { - if (frm.doc.select_serial_no) { - let serial_no = frm.doc.serial_no - ? frm.doc.serial_no + '\n' + frm.doc.select_serial_no : frm.doc.select_serial_no; - frm.set_value("serial_no", serial_no); - frm.set_value("quantity", serial_no.split('\n').length); - } - }, - - serial_no: function(frm) { - const qty = frm.doc.serial_no ? frm.doc.serial_no.split('\n').length : 0; - frm.set_value("quantity", qty); - }, - - setup: function(frm) { - frm.set_query("select_serial_no", function() { + setup: (frm) => { + frm.set_query("to_employee", "assets", (doc) => { return { filters: { - "asset": frm.doc.asset + company: doc.company } }; + }) + frm.set_query("from_employee", "assets", (doc) => { + return { + filters: { + company: doc.company + } + }; + }) + frm.set_query("reference_name", (doc) => { + return { + filters: { + company: doc.company, + docstatus: 1 + } + }; + }) + frm.set_query("reference_doctype", () => { + return { + filters: { + name: ["in", ["Purchase Receipt", "Purchase Invoice"]] + } + }; + }) + }, + + onload: (frm) => { + frm.trigger('set_required_fields'); + }, + + purpose: (frm) => { + frm.trigger('set_required_fields'); + }, + + set_required_fields: (frm, cdt, cdn) => { + let fieldnames_to_be_altered; + if (frm.doc.purpose === 'Transfer') { + fieldnames_to_be_altered = { + target_location: { read_only: 0, reqd: 1 }, + source_location: { read_only: 1, reqd: 1 }, + from_employee: { read_only: 1, reqd: 0 }, + to_employee: { read_only: 1, reqd: 0 } + }; + } + else if (frm.doc.purpose === 'Receipt') { + fieldnames_to_be_altered = { + target_location: { read_only: 0, reqd: 1 }, + source_location: { read_only: 1, reqd: 0 }, + from_employee: { read_only: 0, reqd: 1 }, + to_employee: { read_only: 1, reqd: 0 } + }; + } + else if (frm.doc.purpose === 'Issue') { + fieldnames_to_be_altered = { + target_location: { read_only: 1, reqd: 0 }, + source_location: { read_only: 1, reqd: 1 }, + from_employee: { read_only: 1, reqd: 0 }, + to_employee: { read_only: 0, reqd: 1 } + }; + } + Object.keys(fieldnames_to_be_altered).forEach(fieldname => { + let property_to_be_altered = fieldnames_to_be_altered[fieldname]; + Object.keys(property_to_be_altered).forEach(property => { + let value = property_to_be_altered[property]; + frm.set_df_property(fieldname, property, value, cdn, 'assets'); + }); }); + frm.refresh_field('assets'); + }, + + reference_name: function(frm) { + if (frm.doc.reference_name && frm.doc.reference_doctype) { + const reference_doctype = frm.doc.reference_doctype === 'Purchase Invoice' ? 'purchase_invoice' : 'purchase_receipt'; + // On selection of reference name, + // sets query to display assets linked to that reference doc + frm.set_query('asset', 'assets', function() { + return { + filters: { + [reference_doctype] : frm.doc.reference_name + } + }; + }); + + // fetches linked asset & adds to the assets table + frappe.db.get_list('Asset', { + fields: ['name', 'location', 'custodian'], + filters: { + [reference_doctype] : frm.doc.reference_name + } + }).then((docs) => { + if (docs.length == 0) { + frappe.msgprint(frappe._(`Please select ${frm.doc.reference_doctype} which has assets.`)); + frm.doc.reference_name = ''; + frm.refresh_field('reference_name'); + return; + } + frm.doc.assets = []; + docs.forEach(doc => { + frm.add_child('assets', { + asset: doc.name, + source_location: doc.location, + from_employee: doc.custodian + }); + frm.refresh_field('assets'); + }) + }).catch((err) => { + console.log(err); // eslint-disable-line + }); + } else { + // if reference is deleted then remove query + frm.set_query('asset', 'assets', () => ({ filters: {} })); + } } }); + +frappe.ui.form.on('Asset Movement Item', { + asset: function(frm, cdt, cdn) { + // on manual entry of an asset auto sets their source location / employee + const asset_name = locals[cdt][cdn].asset; + if (asset_name){ + frappe.db.get_doc('Asset', asset_name).then((asset_doc) => { + 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); + }); + } + } +}); \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index 68076e1f743..19af81d65bf 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -1,27 +1,20 @@ { "allow_import": 1, - "autoname": "naming_series:", + "autoname": "format:ACC-ASM-{YYYY}-{#####}", "creation": "2016-04-25 18:00:23.559973", "doctype": "DocType", + "engine": "InnoDB", "field_order": [ - "naming_series", "company", "purpose", - "asset", - "transaction_date", "column_break_4", - "quantity", - "select_serial_no", - "serial_no", - "section_break_7", - "source_location", - "target_location", - "column_break_10", - "from_employee", - "to_employee", + "transaction_date", "reference", "reference_doctype", + "column_break_9", "reference_name", + "section_break_10", + "assets", "amended_from" ], "fields": [ @@ -36,23 +29,12 @@ "reqd": 1 }, { - "default": "Transfer", "fieldname": "purpose", "fieldtype": "Select", "label": "Purpose", "options": "\nIssue\nReceipt\nTransfer", "reqd": 1 }, - { - "fieldname": "asset", - "fieldtype": "Link", - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Asset", - "options": "Asset", - "reqd": 1 - }, { "fieldname": "transaction_date", "fieldtype": "Datetime", @@ -64,56 +46,6 @@ "fieldname": "column_break_4", "fieldtype": "Column Break" }, - { - "fieldname": "quantity", - "fieldtype": "Float", - "label": "Quantity" - }, - { - "fieldname": "select_serial_no", - "fieldtype": "Link", - "label": "Select Serial No", - "options": "Serial No" - }, - { - "fieldname": "serial_no", - "fieldtype": "Small Text", - "label": "Serial No" - }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "fieldname": "source_location", - "fieldtype": "Link", - "label": "Source Location", - "options": "Location" - }, - { - "fieldname": "target_location", - "fieldtype": "Link", - "label": "Target Location", - "options": "Location" - }, - { - "fieldname": "column_break_10", - "fieldtype": "Column Break" - }, - { - "fieldname": "from_employee", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "From Employee", - "options": "Employee" - }, - { - "fieldname": "to_employee", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "To Employee", - "options": "Employee" - }, { "fieldname": "reference", "fieldtype": "Section Break", @@ -125,7 +57,7 @@ "label": "Reference DocType", "no_copy": 1, "options": "DocType", - "read_only": 1 + "reqd": 1 }, { "fieldname": "reference_name", @@ -133,7 +65,7 @@ "label": "Reference Name", "no_copy": 1, "options": "reference_doctype", - "read_only": 1 + "reqd": 1 }, { "fieldname": "amended_from", @@ -145,16 +77,23 @@ "read_only": 1 }, { - "default": "ACC-ASM-.YYYY.-", - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "ACC-ASM-.YYYY.-", + "fieldname": "section_break_10", + "fieldtype": "Section Break" + }, + { + "fieldname": "assets", + "fieldtype": "Table", + "label": "Assets", + "options": "Asset Movement Item", "reqd": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" } ], "is_submittable": 1, - "modified": "2019-09-16 16:27:53.887634", + "modified": "2019-11-13 15:37:48.870147", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index a1d3308b4d0..714845dfac9 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -5,101 +5,142 @@ from __future__ import unicode_literals import frappe from frappe import _ -from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from frappe.model.document import Document class AssetMovement(Document): def validate(self): self.validate_asset() self.validate_location() + self.validate_employee() def validate_asset(self): - status, company = frappe.db.get_value("Asset", self.asset, ["status", "company"]) - if self.purpose == 'Transfer' and status in ("Draft", "Scrapped", "Sold"): - frappe.throw(_("{0} asset cannot be transferred").format(status)) + for d in self.assets: + status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"]) + if self.purpose == 'Transfer' and status in ("Draft", "Scrapped", "Sold"): + frappe.throw(_("{0} asset cannot be transferred").format(status)) - if company != self.company: - frappe.throw(_("Asset {0} does not belong to company {1}").format(self.asset, self.company)) + if company != self.company: + frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company)) - if self.serial_no and len(get_serial_nos(self.serial_no)) != self.quantity: - frappe.throw(_("Number of serial nos and quantity must be the same")) - - if not(self.source_location or self.target_location or self.from_employee or self.to_employee): - frappe.throw(_("Either location or employee must be required")) - - if (not self.serial_no and - frappe.db.get_value('Serial No', {'asset': self.asset}, 'name')): - frappe.throw(_("Serial no is required for the asset {0}").format(self.asset)) + if not(d.source_location or d.target_location or d.from_employee or d.to_employee): + frappe.throw(_("Either location or employee must be required")) def validate_location(self): - if self.purpose in ['Transfer', 'Issue']: - if not self.serial_no and not (self.from_employee or self.to_employee): - self.source_location = frappe.db.get_value("Asset", self.asset, "location") + for d in self.assets: + if self.purpose in ['Transfer', 'Issue']: + if not d.source_location: + d.source_location = frappe.db.get_value("Asset", d.asset, "location") - if self.purpose == 'Issue' and not (self.source_location or self.from_employee): - frappe.throw(_("Source Location is required for the asset {0}").format(self.asset)) + if not d.source_location: + frappe.throw(_("Source Location is required for the Asset {0}").format(d.asset)) - if self.serial_no and self.source_location: - s_nos = get_serial_nos(self.serial_no) - serial_nos = frappe.db.sql_list(""" select name from `tabSerial No` where location != '%s' - and name in (%s)""" %(self.source_location, ','.join(['%s'] * len(s_nos))), tuple(s_nos)) + if d.source_location: + current_location = frappe.db.get_value("Asset", d.asset, "location") - if serial_nos: - frappe.throw(_("Serial nos {0} does not belongs to the location {1}"). - format(','.join(serial_nos), self.source_location)) + if current_location != d.source_location: + frappe.throw(_("Asset {0} does not belongs to the location {1}"). + format(d.asset, d.source_location)) + + if self.purpose == 'Issue': + if d.target_location: + frappe.throw(_("Issuing cannot be done to a location. \ + Please enter employee who has issued Asset {0}").format(d.asset), title="Incorrect Movement Purpose") + if not d.to_employee: + frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset)) + + if self.purpose == 'Transfer': + if d.to_employee: + frappe.throw(_("Transferring cannot be done to an Employee. \ + Please enter location where Asset {0} has to be transferred").format( + d.asset), title="Incorrect Movement Purpose") + if not d.target_location: + frappe.throw(_("Target Location is required while transferring Asset {0}").format(d.asset)) + if d.source_location == d.target_location: + frappe.throw(_("Source and Target Location cannot be same")) + + if self.purpose == 'Receipt': + # only when asset is bought and first entry is made + if not d.source_location and not (d.target_location or d.to_employee): + frappe.throw(_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)) + elif d.source_location: + # when asset is received from an employee + if d.target_location and not d.from_employee: + frappe.throw(_("From employee is required while receiving Asset {0} to a target location").format(d.asset)) + if d.from_employee and not d.target_location: + frappe.throw(_("Target Location is required while receiving Asset {0} from an employee").format(d.asset)) + if d.to_employee and d.target_location: + frappe.throw(_("Asset {0} cannot be received at a location and \ + given to employee in a single movement").format(d.asset)) - if self.source_location and self.source_location == self.target_location and self.purpose == 'Transfer': - frappe.throw(_("Source and Target Location cannot be same")) + def validate_employee(self): + for d in self.assets: + if d.from_employee: + current_custodian = frappe.db.get_value("Asset", d.asset, "custodian") - if self.purpose == 'Receipt' and not (self.target_location or self.to_employee): - frappe.throw(_("Target Location is required for the asset {0}").format(self.asset)) + if current_custodian != d.from_employee: + frappe.throw(_("Asset {0} does not belongs to the custodian {1}"). + format(d.asset, d.from_employee)) + + if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: + frappe.throw(_("Employee {0} does not belongs to the company {1}"). + format(d.to_employee, self.company)) def on_submit(self): self.set_latest_location_in_asset() + + def before_cancel(self): + self.validate_last_movement() def on_cancel(self): self.set_latest_location_in_asset() + + def validate_last_movement(self): + for d in self.assets: + auto_gen_movement_entry = frappe.db.sql( + """ + SELECT asm.name + FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm + WHERE + asm.docstatus=1 and + asm_item.parent=asm.name and + asm_item.asset=%s and + asm.company=%s and + asm_item.source_location is NULL and + asm.purpose=%s + ORDER BY + asm.transaction_date asc + """, (d.asset, self.company, 'Receipt'), as_dict=1) + if auto_gen_movement_entry[0].get('name') == self.name: + frappe.throw(_('{0} will be cancelled automatically on asset cancellation as it was \ + auto generated for Asset {1}').format(self.name, d.asset)) def set_latest_location_in_asset(self): - location, employee = '', '' + current_location, current_employee = '', '' cond = "1=1" - args = { - 'asset': self.asset, - 'company': self.company - } + for d in self.assets: + args = { + 'asset': d.asset, + 'company': self.company + } - if self.serial_no: - cond = "serial_no like %(txt)s" - args.update({ - 'txt': "%%%s%%" % self.serial_no - }) + # latest entry corresponds to current document's location, employee when transaction date > previous dates + # In case of cancellation it corresponds to previous latest document's location, employee + latest_movement_entry = frappe.db.sql( + """ + SELECT asm_item.target_location, asm_item.to_employee + FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm + WHERE + asm_item.parent=asm.name and + asm_item.asset=%(asset)s and + asm.company=%(company)s and + asm.docstatus=1 and {0} + ORDER BY + asm.transaction_date desc limit 1 + """.format(cond), args) + if latest_movement_entry: + current_location = latest_movement_entry[0][0] + current_employee = latest_movement_entry[0][1] - latest_movement_entry = frappe.db.sql("""select target_location, to_employee from `tabAsset Movement` - where asset=%(asset)s and docstatus=1 and company=%(company)s and {0} - order by transaction_date desc limit 1""".format(cond), args) - - if latest_movement_entry: - location = latest_movement_entry[0][0] - employee = latest_movement_entry[0][1] - elif self.purpose in ['Transfer', 'Receipt']: - movement_entry = frappe.db.sql("""select source_location, from_employee from `tabAsset Movement` - where asset=%(asset)s and docstatus=2 and company=%(company)s and {0} - order by transaction_date asc limit 1""".format(cond), args) - if movement_entry: - location = movement_entry[0][0] - employee = movement_entry[0][1] - - if not self.serial_no: - frappe.db.set_value("Asset", self.asset, "location", location) - - if not employee and self.purpose in ['Receipt', 'Transfer']: - employee = self.to_employee - - if self.serial_no: - for d in get_serial_nos(self.serial_no): - if (location or (self.purpose == 'Issue' and self.source_location)): - frappe.db.set_value('Serial No', d, 'location', location) - - if employee or self.docstatus==2 or self.purpose == 'Issue': - frappe.db.set_value('Serial No', d, 'employee', employee) + frappe.db.set_value('Asset', d.asset, 'location', current_location) + frappe.db.set_value('Asset', d.asset, 'custodian', current_employee) diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index 4d853374455..c3755a3fb9a 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -5,6 +5,7 @@ from __future__ import unicode_literals import frappe import unittest +import erpnext from erpnext.stock.doctype.item.test_item import make_item from frappe.utils import now, nowdate, get_last_day, add_days from erpnext.assets.doctype.asset.test_asset import create_asset_data @@ -16,7 +17,6 @@ class TestAssetMovement(unittest.TestCase): def setUp(self): create_asset_data() make_location() - make_serialized_item() def test_movement(self): pr = make_purchase_receipt(item_code="Macbook Pro", @@ -38,68 +38,72 @@ class TestAssetMovement(unittest.TestCase): if asset.docstatus == 0: asset.submit() + + # check asset movement is created if not frappe.db.exists("Location", "Test Location 2"): frappe.get_doc({ 'doctype': 'Location', 'location_name': 'Test Location 2' }).insert() - movement1 = create_asset_movement(asset= asset.name, purpose = 'Transfer', - company=asset.company, source_location="Test Location", target_location="Test Location 2") + movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, + assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}], + reference_doctype = 'Purchase Receipt', reference_name = pr.name) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") - movement2 = create_asset_movement(asset= asset.name, purpose = 'Transfer', - company=asset.company, source_location = "Test Location 2", target_location="Test Location") + movement2 = create_asset_movement(purpose = 'Transfer', company = asset.company, + assets = [{ 'asset': asset.name , 'source_location': 'Test Location 2', 'target_location': 'Test Location'}], + reference_doctype = 'Purchase Receipt', reference_name = pr.name) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") movement1.cancel() self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") - movement2.cancel() - self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") - - def test_movement_for_serialized_asset(self): - asset_item = "Test Serialized Asset Item" - pr = make_purchase_receipt(item_code=asset_item, rate = 1000, qty=3, location = "Mumbai") - asset_name = frappe.db.get_value('Asset', {'purchase_receipt': pr.name}, 'name') - + employee = make_employee("testassetmovemp@example.com", company="_Test Company") + movement3 = create_asset_movement(purpose = 'Issue', company = asset.company, + assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'to_employee': employee}], + reference_doctype = 'Purchase Receipt', reference_name = pr.name) + + # after issuing asset should belong to an employee not at a location + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None) + self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee) + + def test_last_movement_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) - month_end_date = get_last_day(nowdate()) - asset.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - 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": 200, + "expected_value_after_useful_life": 10000, + "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, "frequency_of_depreciation": 10, - "depreciation_start_date": month_end_date + "depreciation_start_date": "2020-06-06" }) - asset.submit() - serial_nos = frappe.db.get_value('Asset Movement', {'reference_name': pr.name}, 'serial_no') + if asset.docstatus == 0: + asset.submit() + + if not frappe.db.exists("Location", "Test Location 2"): + frappe.get_doc({ + 'doctype': 'Location', + 'location_name': 'Test Location 2' + }).insert() + + movement = frappe.get_doc({'doctype': 'Asset Movement', 'reference_name': pr.name }) + self.assertRaises(frappe.ValidationError, movement.cancel) - mov1 = create_asset_movement(asset=asset_name, purpose = 'Transfer', - company=asset.company, source_location = "Mumbai", target_location="Pune", serial_no=serial_nos) - self.assertEqual(mov1.target_location, "Pune") + movement1 = create_asset_movement(purpose = 'Transfer', company = asset.company, + assets = [{ 'asset': asset.name , 'source_location': 'Test Location', 'target_location': 'Test Location 2'}], + reference_doctype = 'Purchase Receipt', reference_name = pr.name) + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2") - serial_no = frappe.db.get_value('Serial No', {'asset': asset_name}, 'name') - - employee = make_employee("testassetemp@example.com") - create_asset_movement(asset=asset_name, purpose = 'Transfer', - company=asset.company, serial_no=serial_no, to_employee=employee) - - self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'employee'), employee) - - create_asset_movement(asset=asset_name, purpose = 'Transfer', company=asset.company, - serial_no=serial_no, from_employee=employee, to_employee="_T-Employee-00001") - - self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'location'), "Pune") - - mov4 = create_asset_movement(asset=asset_name, purpose = 'Transfer', - company=asset.company, source_location = "Pune", target_location="Nagpur", serial_no=serial_nos) - self.assertEqual(mov4.target_location, "Nagpur") - self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'location'), "Nagpur") - self.assertEqual(frappe.db.get_value('Serial No', serial_no, 'employee'), "_T-Employee-00001") + movement1.cancel() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location") def create_asset_movement(**args): args = frappe._dict(args) @@ -109,22 +113,14 @@ def create_asset_movement(**args): movement = frappe.new_doc("Asset Movement") movement.update({ - "asset": args.asset, + "assets": args.assets, "transaction_date": args.transaction_date, - "target_location": args.target_location, "company": args.company, 'purpose': args.purpose or 'Receipt', - 'serial_no': args.serial_no, - 'quantity': len(get_serial_nos(args.serial_no)) if args.serial_no else 1, - 'from_employee': "_T-Employee-00001" or args.from_employee, - 'to_employee': args.to_employee + 'reference_doctype': args.reference_doctype, + 'reference_name': args.reference_name }) - if args.source_location: - movement.update({ - 'source_location': args.source_location - }) - movement.insert() movement.submit() @@ -137,33 +133,3 @@ def make_location(): 'doctype': 'Location', 'location_name': location }).insert(ignore_permissions = True) - -def make_serialized_item(): - asset_item = "Test Serialized Asset Item" - - if not frappe.db.exists('Item', asset_item): - asset_category = frappe.get_all('Asset Category') - - if asset_category: - asset_category = asset_category[0].name - - if not asset_category: - doc = frappe.get_doc({ - 'doctype': 'Asset Category', - 'asset_category_name': 'Test Asset Category', - 'depreciation_method': 'Straight Line', - 'total_number_of_depreciations': 12, - 'frequency_of_depreciation': 1, - 'accounts': [{ - 'company_name': '_Test Company', - 'fixed_asset_account': '_Test Fixed Asset - _TC', - 'accumulated_depreciation_account': 'Depreciation - _TC', - 'depreciation_expense_account': 'Depreciation - _TC' - }] - }).insert() - - asset_category = doc.name - - make_item(asset_item, {'is_stock_item':0, - 'stock_uom': 'Box', 'is_fixed_asset': 1, 'has_serial_no': 1, - 'asset_category': asset_category, 'serial_no_series': 'ABC.###'}) diff --git a/erpnext/assets/doctype/asset_movement_item/__init__.py b/erpnext/assets/doctype/asset_movement_item/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_movement_item/asset_movement_item.json b/erpnext/assets/doctype/asset_movement_item/asset_movement_item.json new file mode 100644 index 00000000000..994c3c09898 --- /dev/null +++ b/erpnext/assets/doctype/asset_movement_item/asset_movement_item.json @@ -0,0 +1,86 @@ +{ + "creation": "2019-10-07 18:49:00.737806", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "asset", + "source_location", + "from_employee", + "column_break_2", + "asset_name", + "target_location", + "to_employee" + ], + "fields": [ + { + "fieldname": "asset", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Asset", + "options": "Asset", + "reqd": 1 + }, + { + "fetch_from": "asset.asset_name", + "fieldname": "asset_name", + "fieldtype": "Data", + "label": "Asset Name", + "read_only": 1 + }, + { + "fieldname": "source_location", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Source Location", + "options": "Location" + }, + { + "fieldname": "target_location", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Target Location", + "options": "Location" + }, + { + "fieldname": "from_employee", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "From Employee", + "options": "Employee" + }, + { + "fieldname": "to_employee", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_list_view": 1, + "label": "To Employee", + "options": "Employee" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "hidden": 1, + "label": "Company", + "options": "Company", + "read_only": 1 + } + ], + "istable": 1, + "modified": "2019-10-09 15:59:08.265141", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Movement 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/assets/doctype/asset_movement_item/asset_movement_item.py b/erpnext/assets/doctype/asset_movement_item/asset_movement_item.py new file mode 100644 index 00000000000..4c6aaab58a0 --- /dev/null +++ b/erpnext/assets/doctype/asset_movement_item/asset_movement_item.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 AssetMovementItem(Document): + pass diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 66ad97ac09e..c409c1f46e0 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -43,6 +43,7 @@ "base_amount", "pricing_rules", "is_free_item", + "is_fixed_asset", "section_break_29", "net_rate", "net_amount", @@ -699,11 +700,19 @@ "fieldtype": "Data", "label": "Manufacturer Part Number", "read_only": 1 + }, + { + "default": "0", + "fetch_from": "item_code.is_fixed_asset", + "fieldname": "is_fixed_asset", + "fieldtype": "Check", + "label": "Is Fixed Asset", + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-09-17 22:32:34.703923", + "modified": "2019-11-07 17:19:12.090355", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 94152284670..a912ef00d15 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -718,48 +718,6 @@ class AccountsController(TransactionBase): # at quotation / sales order level and we shouldn't stop someone # from creating a sales invoice if sales order is already created - def validate_fixed_asset(self): - for d in self.get("items"): - if d.is_fixed_asset: - # if d.qty > 1: - # frappe.throw(_("Row #{0}: Qty must be 1, as item is a fixed asset. Please use separate row for multiple qty.").format(d.idx)) - - if d.meta.get_field("asset") and d.asset: - asset = frappe.get_doc("Asset", d.asset) - - if asset.company != self.company: - frappe.throw(_("Row #{0}: Asset {1} does not belong to company {2}") - .format(d.idx, d.asset, self.company)) - - elif asset.item_code != d.item_code: - frappe.throw(_("Row #{0}: Asset {1} does not linked to Item {2}") - .format(d.idx, d.asset, d.item_code)) - - # elif asset.docstatus != 1: - # frappe.throw(_("Row #{0}: Asset {1} must be submitted").format(d.idx, d.asset)) - - elif self.doctype == "Purchase Invoice": - # if asset.status != "Submitted": - # frappe.throw(_("Row #{0}: Asset {1} is already {2}") - # .format(d.idx, d.asset, asset.status)) - if getdate(asset.purchase_date) != getdate(self.posting_date): - frappe.throw( - _("Row #{0}: Posting Date must be same as purchase date {1} of asset {2}").format(d.idx, - asset.purchase_date, - d.asset)) - elif asset.is_existing_asset: - frappe.throw( - _("Row #{0}: Purchase Invoice cannot be made against an existing asset {1}").format( - d.idx, d.asset)) - - elif self.docstatus == "Sales Invoice" and self.docstatus == 1: - if self.update_stock: - frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - - elif asset.status in ("Scrapped", "Cancelled", "Sold"): - frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}") - .format(d.idx, d.asset, asset.status)) - def delink_advance_entries(self, linked_doc_name): total_allocated_amount = 0 for adv in self.advances: diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 0dde8980051..d0befcbcf38 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -101,7 +101,7 @@ class BuyingController(StockController): msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items')) def get_asset_items(self): - if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']: + if self.doctype not in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']: return [] return [d.item_code for d in self.items if d.is_fixed_asset] @@ -150,25 +150,26 @@ class BuyingController(StockController): TODO: rename item_tax_amount to valuation_tax_amount """ - stock_items = self.get_stock_items() + self.get_asset_items() + stock_and_asset_items = self.get_stock_items() + self.get_asset_items() - stock_items_qty, stock_items_amount = 0, 0 - last_stock_item_idx = 1 + stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0 + last_item_idx = 1 for d in self.get(parentfield): - if d.item_code and d.item_code in stock_items: - stock_items_qty += flt(d.qty) - stock_items_amount += flt(d.base_net_amount) - last_stock_item_idx = d.idx + if d.item_code and d.item_code in stock_and_asset_items: + stock_and_asset_items_qty += flt(d.qty) + stock_and_asset_items_amount += flt(d.base_net_amount) + last_item_idx = d.idx total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes") if d.category in ["Valuation", "Valuation and Total"]]) valuation_amount_adjustment = total_valuation_amount for i, item in enumerate(self.get(parentfield)): - if item.item_code and item.qty and item.item_code in stock_items: - item_proportion = flt(item.base_net_amount) / stock_items_amount if stock_items_amount \ - else flt(item.qty) / stock_items_qty - if i == (last_stock_item_idx - 1): + if item.item_code and item.qty and item.item_code in stock_and_asset_items: + item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \ + else flt(item.qty) / stock_and_asset_items_qty + + if i == (last_item_idx - 1): item.item_tax_amount = flt(valuation_amount_adjustment, self.precision("item_tax_amount", item)) else: @@ -572,43 +573,28 @@ class BuyingController(StockController): asset_items = self.get_asset_items() if asset_items: - self.make_serial_nos_for_asset(asset_items) + self.auto_make_assets(asset_items) - def make_serial_nos_for_asset(self, asset_items): + def auto_make_assets(self, asset_items): items_data = get_asset_item_details(asset_items) for d in self.items: if d.is_fixed_asset: item_data = items_data.get(d.item_code) - if not d.asset: - asset = self.make_asset(d) - d.db_set('asset', asset) - if item_data.get('has_serial_no'): - # If item has serial no - if item_data.get('serial_no_series') and not d.serial_no: - serial_nos = get_auto_serial_nos(item_data.get('serial_no_series'), d.qty) - elif d.serial_no: - serial_nos = d.serial_no - elif not d.serial_no: - frappe.throw(_("Serial no is mandatory for the item {0}").format(d.item_code)) - - auto_make_serial_nos({ - 'serial_no': serial_nos, - 'item_code': d.item_code, - 'via_stock_ledger': False, - 'company': self.company, - 'supplier': self.supplier, - 'actual_qty': d.qty, - 'purchase_document_type': self.doctype, - 'purchase_document_no': self.name, - 'asset': d.asset, - 'location': d.asset_location - }) - d.db_set('serial_no', serial_nos) - - if d.asset: - self.make_asset_movement(d) + if item_data.get('auto_create_assets'): + # If asset has to be auto created + # Check for asset naming series + if item_data.get('asset_naming_series'): + 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)) + else: + frappe.throw(_("Asset Naming Series is mandatory for the auto creation for item {0}").format(d.item_code)) + else: + frappe.msgprint(_("Assets not created. You will have to create asset manually.")) + def make_asset(self, row): if not row.asset_location: @@ -617,7 +603,7 @@ class BuyingController(StockController): item_data = frappe.db.get_value('Item', row.item_code, ['asset_naming_series', 'asset_category'], as_dict=1) - purchase_amount = flt(row.base_net_amount + row.item_tax_amount) + purchase_amount = flt(row.base_rate + row.item_tax_amount) asset = frappe.get_doc({ 'doctype': 'Asset', 'item_code': row.item_code, @@ -640,57 +626,42 @@ class BuyingController(StockController): asset.set_missing_values() asset.insert() - asset_link = frappe.utils.get_link_to_form('Asset', asset.name) - frappe.msgprint(_("Asset {0} created").format(asset_link)) - return asset.name - - def make_asset_movement(self, row): - asset_movement = frappe.get_doc({ - 'doctype': 'Asset Movement', - 'asset': row.asset, - 'target_location': row.asset_location, - 'purpose': 'Receipt', - 'serial_no': row.serial_no, - 'quantity': len(get_serial_nos(row.serial_no)), - 'company': self.company, - 'transaction_date': self.posting_date, - 'reference_doctype': self.doctype, - 'reference_name': self.name - }).insert() - - return asset_movement.name - def update_fixed_asset(self, field, delete_asset = False): for d in self.get("items"): - if d.is_fixed_asset and d.asset: - asset = frappe.get_doc("Asset", d.asset) + if d.is_fixed_asset: + is_auto_create_enabled = frappe.db.get_value('Item', d.item_code, 'auto_create_assets') + assets = frappe.db.get_all('Asset', filters={ field : self.name, 'item_code' : d.item_code }) - if delete_asset and asset.docstatus == 0: - frappe.delete_doc("Asset", asset.name) - d.db_set('asset', None) - continue + for asset in assets: + asset = frappe.get_doc('Asset', asset.name) + if delete_asset and is_auto_create_enabled: + # need to delete movements to delete assets otherwise throws link exists error + movements = frappe.db.get_all('Asset Movement', filters={ 'reference_name': self.name }) + for movement in movements: + frappe.delete_doc('Asset Movement', movement.name, force=1) + frappe.delete_doc("Asset", asset.name, force=1) + continue - if self.docstatus in [0, 1] and not asset.get(field): - asset.set(field, self.name) - asset.purchase_date = self.posting_date - asset.supplier = self.supplier - elif self.docstatus == 2: - asset.set(field, None) - asset.supplier = None + if self.docstatus in [0, 1] and not asset.get(field): + asset.set(field, self.name) + asset.purchase_date = self.posting_date + asset.supplier = self.supplier + elif self.docstatus == 2: + asset.set(field, None) + asset.supplier = None - asset.flags.ignore_validate_update_after_submit = True - asset.flags.ignore_mandatory = True - if asset.docstatus == 0: - asset.flags.ignore_validate = True + asset.flags.ignore_validate_update_after_submit = True + asset.flags.ignore_mandatory = True + if asset.docstatus == 0: + asset.flags.ignore_validate = True - asset.save() + asset.save() def delete_linked_asset(self): if self.doctype == 'Purchase Invoice' and not self.get('update_stock'): return frappe.db.sql("delete from `tabAsset Movement` where reference_name=%s", self.name) - frappe.db.sql("delete from `tabSerial No` where purchase_document_no=%s", self.name) def validate_schedule_date(self): if not self.get("items"): @@ -764,7 +735,7 @@ def get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, purchas def get_asset_item_details(asset_items): asset_items_data = {} - for d in frappe.get_all('Item', fields = ["name", "has_serial_no", "serial_no_series"], + for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"], filters = {'name': ('in', asset_items)}): asset_items_data.setdefault(d.name, d) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index a9e50bab5a4..3830ca03615 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -476,3 +476,29 @@ def item_manufacturer_query(doctype, txt, searchfield, start, page_len, filters) as_list=1 ) return item_manufacturers + +@frappe.whitelist() +def get_purchase_receipts(doctype, txt, searchfield, start, page_len, filters): + query = """ + select pr.name + from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pritem + where pr.docstatus = 1 and pritem.parent = pr.name + and pr.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt))) + + if filters and filters.get('item_code'): + query += " and pritem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code'))) + + return frappe.db.sql(query, filters) + +@frappe.whitelist() +def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters): + query = """ + select pi.name + from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` piitem + where pi.docstatus = 1 and piitem.parent = pi.name + and pi.name like {txt}""".format(txt = frappe.db.escape('%{0}%'.format(txt))) + + if filters and filters.get('item_code'): + query += " and piitem.item_code = {item_code}".format(item_code = frappe.db.escape(filters.get('item_code'))) + + return frappe.db.sql(query, filters) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 5a63beb2836..d3410de2eb7 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -45,7 +45,7 @@ class TestEmployee(unittest.TestCase): employee1_doc.status = 'Left' self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) -def make_employee(user): +def make_employee(user, company=None): if not frappe.db.get_value("User", user): frappe.get_doc({ "doctype": "User", @@ -55,12 +55,12 @@ def make_employee(user): "roles": [{"doctype": "Has Role", "role": "Employee"}] }).insert() - if not frappe.db.get_value("Employee", {"user_id": user}): + if not frappe.db.get_value("Employee", { "user_id": user, "company": company or erpnext.get_default_company() }): employee = frappe.get_doc({ "doctype": "Employee", "naming_series": "EMP-", "first_name": user, - "company": erpnext.get_default_company(), + "company": company or erpnext.get_default_company(), "user_id": user, "date_of_birth": "1990-05-08", "date_of_joining": "2013-01-01", diff --git a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py index 1c8bd689329..ee709ac2d49 100644 --- a/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py +++ b/erpnext/patches/v11_0/make_asset_finance_book_against_old_entries.py @@ -17,10 +17,6 @@ def execute(): frappe.db.sql(""" update `tabAsset` ast, `tabWarehouse` wh set ast.location = wh.warehouse_name where ast.warehouse = wh.name""") - frappe.db.sql(""" update `tabAsset Movement` ast_mv - set ast_mv.source_location = (select warehouse_name from `tabWarehouse` where name = ast_mv.source_warehouse), - ast_mv.target_location = (select warehouse_name from `tabWarehouse` where name = ast_mv.target_warehouse)""") - for d in frappe.get_all('Asset'): doc = frappe.get_doc('Asset', d.name) if doc.calculate_depreciation: diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index bfc5e6d438e..2f4abbcea66 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -25,7 +25,7 @@ frappe.ui.form.on("Item", { }, refresh: function(frm) { - if(frm.doc.is_stock_item) { + if (frm.doc.is_stock_item) { frm.add_custom_button(__("Balance"), function() { frappe.route_options = { "item_code": frm.doc.name @@ -46,10 +46,15 @@ frappe.ui.form.on("Item", { }, __("View")); } - if(!frm.doc.is_fixed_asset) { + 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'); + } + // clear intro frm.set_intro(); @@ -132,6 +137,11 @@ frappe.ui.form.on("Item", { }, is_fixed_asset: function(frm) { + // 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_display(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); + frm.call({ method: "set_asset_naming_series", doc: frm.doc, @@ -139,7 +149,7 @@ frappe.ui.form.on("Item", { frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1); frm.trigger("set_asset_naming_series"); } - }) + }); }, set_asset_naming_series: function(frm) { @@ -148,6 +158,11 @@ 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); + }, + page_name: frappe.utils.warn_page_name_change, item_code: function(frm) { diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index 46efd4ee26f..a2aab3f69ee 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -1,1105 +1,1114 @@ { - "allow_guest_to_view": 1, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:item_code", - "creation": "2013-05-03 10:45:46", - "description": "A Product or a Service that is bought, sold or kept in stock.", - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "name_and_description_section", - "naming_series", - "item_code", - "variant_of", - "item_name", - "item_group", - "is_item_from_hub", - "stock_uom", - "column_break0", - "disabled", - "allow_alternative_item", - "is_stock_item", - "include_item_in_manufacturing", - "opening_stock", - "valuation_rate", - "standard_rate", - "is_fixed_asset", - "asset_category", - "asset_naming_series", - "over_delivery_receipt_allowance", - "over_billing_allowance", - "image", - "section_break_11", - "brand", - "description", - "sb_barcodes", - "barcodes", - "inventory_section", - "shelf_life_in_days", - "end_of_life", - "default_material_request_type", - "valuation_method", - "column_break1", - "warranty_period", - "weight_per_unit", - "weight_uom", - "reorder_section", - "reorder_levels", - "unit_of_measure_conversion", - "uoms", - "serial_nos_and_batches", - "has_batch_no", - "create_new_batch", - "batch_number_series", - "has_expiry_date", - "retain_sample", - "sample_quantity", - "column_break_37", - "has_serial_no", - "serial_no_series", - "variants_section", - "has_variants", - "variant_based_on", - "attributes", - "defaults", - "item_defaults", - "purchase_details", - "is_purchase_item", - "purchase_uom", - "min_order_qty", - "safety_stock", - "purchase_details_cb", - "lead_time_days", - "last_purchase_rate", - "is_customer_provided_item", - "customer", - "supplier_details", - "delivered_by_supplier", - "column_break2", - "supplier_items", - "foreign_trade_details", - "country_of_origin", - "column_break_59", - "customs_tariff_number", - "sales_details", - "sales_uom", - "is_sales_item", - "column_break3", - "max_discount", - "deferred_revenue", - "deferred_revenue_account", - "enable_deferred_revenue", - "column_break_85", - "no_of_months", - "deferred_expense_section", - "deferred_expense_account", - "enable_deferred_expense", - "column_break_88", - "no_of_months_exp", - "customer_details", - "customer_items", - "item_tax_section_break", - "taxes", - "inspection_criteria", - "inspection_required_before_purchase", - "inspection_required_before_delivery", - "quality_inspection_template", - "manufacturing", - "default_bom", - "is_sub_contracted_item", - "column_break_74", - "customer_code", - "website_section", - "show_in_website", - "show_variant_in_website", - "route", - "weightage", - "slideshow", - "website_image", - "thumbnail", - "cb72", - "website_warehouse", - "website_item_groups", - "set_meta_tags", - "sb72", - "copy_from_item_group", - "website_specifications", - "web_long_description", - "website_content", - "total_projected_qty", - "hub_publishing_sb", - "publish_in_hub", - "hub_category_to_publish", - "hub_warehouse", - "synced_with_hub" - ], - "fields": [ - { - "fieldname": "name_and_description_section", - "fieldtype": "Section Break", - "oldfieldtype": "Section Break", - "options": "fa fa-flag" - }, - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "options": "STO-ITEM-.YYYY.-", - "set_only_once": 1 - }, - { - "bold": 1, - "fieldname": "item_code", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Item Code", - "oldfieldname": "item_code", - "oldfieldtype": "Data", - "unique": 1, - "reqd": 1 - }, - { - "depends_on": "variant_of", - "description": "If item is a variant of another item then description, image, pricing, taxes etc will be set from the template unless explicitly specified", - "fieldname": "variant_of", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "in_standard_filter": 1, - "label": "Variant Of", - "options": "Item", - "read_only": 1, - "search_index": 1, - "set_only_once": 1 - }, - { - "bold": 1, - "fieldname": "item_name", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Item Name", - "oldfieldname": "item_name", - "oldfieldtype": "Data", - "search_index": 1 - }, - { - "fieldname": "item_group", - "fieldtype": "Link", - "in_list_view": 1, - "in_preview": 1, - "in_standard_filter": 1, - "label": "Item Group", - "oldfieldname": "item_group", - "oldfieldtype": "Link", - "options": "Item Group", - "reqd": 1, - "search_index": 1 - }, - { - "default": "0", - "fieldname": "is_item_from_hub", - "fieldtype": "Check", - "label": "Is Item from Hub", - "read_only": 1 - }, - { - "fieldname": "stock_uom", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Unit of Measure", - "oldfieldname": "stock_uom", - "oldfieldtype": "Link", - "options": "UOM", - "reqd": 1 - }, - { - "fieldname": "column_break0", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "disabled", - "fieldtype": "Check", - "label": "Disabled" - }, - { - "default": "0", - "fieldname": "allow_alternative_item", - "fieldtype": "Check", - "label": "Allow Alternative Item" - }, - { - "bold": 1, - "default": "1", - "fieldname": "is_stock_item", - "fieldtype": "Check", - "label": "Maintain Stock", - "oldfieldname": "is_stock_item", - "oldfieldtype": "Select" - }, - { - "default": "1", - "fieldname": "include_item_in_manufacturing", - "fieldtype": "Check", - "label": "Include Item In Manufacturing" - }, - { - "bold": 1, - "depends_on": "eval:(doc.__islocal&&doc.is_stock_item && !doc.has_serial_no && !doc.has_batch_no)", - "fieldname": "opening_stock", - "fieldtype": "Float", - "label": "Opening Stock" - }, - { - "depends_on": "is_stock_item", - "fieldname": "valuation_rate", - "fieldtype": "Currency", - "label": "Valuation Rate" - }, - { - "bold": 1, - "depends_on": "eval:doc.__islocal", - "fieldname": "standard_rate", - "fieldtype": "Currency", - "label": "Standard Selling Rate" - }, - { - "default": "0", - "fieldname": "is_fixed_asset", - "fieldtype": "Check", - "label": "Is Fixed Asset", - "set_only_once": 1 - }, - { - "depends_on": "is_fixed_asset", - "fieldname": "asset_category", - "fieldtype": "Link", - "label": "Asset Category", - "options": "Asset Category" - }, - { - "depends_on": "is_fixed_asset", - "fieldname": "asset_naming_series", - "fieldtype": "Select", - "label": "Asset Naming Series" - }, - { - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "in_preview": 1, - "label": "Image", - "options": "image", - "print_hide": 1 - }, - { - "collapsible": 1, - "fieldname": "section_break_11", - "fieldtype": "Section Break", - "label": "Description" - }, - { - "fieldname": "brand", - "fieldtype": "Link", - "label": "Brand", - "oldfieldname": "brand", - "oldfieldtype": "Link", - "options": "Brand", - "print_hide": 1 - }, - { - "fieldname": "description", - "fieldtype": "Text Editor", - "in_preview": 1, - "label": "Description", - "oldfieldname": "description", - "oldfieldtype": "Text" - }, - { - "fieldname": "sb_barcodes", - "fieldtype": "Section Break", - "label": "Barcodes" - }, - { - "fieldname": "barcodes", - "fieldtype": "Table", - "label": "Barcodes", - "options": "Item Barcode" - }, - { - "collapsible": 1, - "collapsible_depends_on": "is_stock_item", - "depends_on": "is_stock_item", - "fieldname": "inventory_section", - "fieldtype": "Section Break", - "label": "Inventory", - "oldfieldtype": "Section Break", - "options": "fa fa-truck" - }, - { - "fieldname": "shelf_life_in_days", - "fieldtype": "Int", - "label": "Shelf Life In Days" - }, - { - "default": "2099-12-31", - "depends_on": "is_stock_item", - "fieldname": "end_of_life", - "fieldtype": "Date", - "label": "End of Life", - "oldfieldname": "end_of_life", - "oldfieldtype": "Date" - }, - { - "default": "Purchase", - "fieldname": "default_material_request_type", - "fieldtype": "Select", - "label": "Default Material Request Type", - "options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided" - }, - { - "depends_on": "is_stock_item", - "fieldname": "valuation_method", - "fieldtype": "Select", - "label": "Valuation Method", - "options": "\nFIFO\nMoving Average", - "set_only_once": 1 - }, - { - "depends_on": "is_stock_item", - "fieldname": "column_break1", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "width": "50%" - }, - { - "depends_on": "eval:doc.is_stock_item", - "fieldname": "warranty_period", - "fieldtype": "Data", - "label": "Warranty Period (in days)", - "oldfieldname": "warranty_period", - "oldfieldtype": "Data" - }, - { - "depends_on": "is_stock_item", - "fieldname": "weight_per_unit", - "fieldtype": "Float", - "label": "Weight Per Unit" - }, - { - "depends_on": "eval:doc.is_stock_item", - "fieldname": "weight_uom", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Weight UOM", - "options": "UOM" - }, - { - "collapsible": 1, - "depends_on": "is_stock_item", - "fieldname": "reorder_section", - "fieldtype": "Section Break", - "label": "Auto re-order", - "options": "fa fa-rss" - }, - { - "description": "Will also apply for variants unless overrridden", - "fieldname": "reorder_levels", - "fieldtype": "Table", - "label": "Reorder level based on Warehouse", - "options": "Item Reorder" - }, - { - "collapsible": 1, - "fieldname": "unit_of_measure_conversion", - "fieldtype": "Section Break", - "label": "Units of Measure" - }, - { - "description": "Will also apply for variants", - "fieldname": "uoms", - "fieldtype": "Table", - "label": "UOMs", - "oldfieldname": "uom_conversion_details", - "oldfieldtype": "Table", - "options": "UOM Conversion Detail" - }, - { - "collapsible": 1, - "collapsible_depends_on": "eval:doc.has_batch_no || doc.has_serial_no || doc.is_fixed_asset", - "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", - "fieldname": "serial_nos_and_batches", - "fieldtype": "Section Break", - "label": "Serial Nos and Batches" - }, - { - "default": "0", - "depends_on": "eval:doc.is_stock_item", - "fieldname": "has_batch_no", - "fieldtype": "Check", - "label": "Has Batch No", - "no_copy": 1, - "oldfieldname": "has_batch_no", - "oldfieldtype": "Select" - }, - { - "default": "0", - "depends_on": "has_batch_no", - "fieldname": "create_new_batch", - "fieldtype": "Check", - "label": "Automatically Create New Batch" - }, - { - "depends_on": "eval:doc.has_batch_no==1 && doc.create_new_batch==1", - "description": "Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.", - "fieldname": "batch_number_series", - "fieldtype": "Data", - "label": "Batch Number Series", - "translatable": 1 - }, - { - "default": "0", - "depends_on": "has_batch_no", - "fieldname": "has_expiry_date", - "fieldtype": "Check", - "label": "Has Expiry Date" - }, - { - "default": "0", - "fieldname": "retain_sample", - "fieldtype": "Check", - "label": "Retain Sample" - }, - { - "depends_on": "eval: (doc.retain_sample && doc.has_batch_no)", - "description": "Maximum sample quantity that can be retained", - "fieldname": "sample_quantity", - "fieldtype": "Int", - "label": "Max Sample Quantity" - }, - { - "fieldname": "column_break_37", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", - "fieldname": "has_serial_no", - "fieldtype": "Check", - "label": "Has Serial No", - "no_copy": 1, - "oldfieldname": "has_serial_no", - "oldfieldtype": "Select" - }, - { - "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", - "description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.", - "fieldname": "serial_no_series", - "fieldtype": "Data", - "label": "Serial Number Series" - }, - { - "collapsible": 1, - "collapsible_depends_on": "attributes", - "fieldname": "variants_section", - "fieldtype": "Section Break", - "label": "Variants" - }, - { - "default": "0", - "depends_on": "eval:!doc.variant_of", - "description": "If this item has variants, then it cannot be selected in sales orders etc.", - "fieldname": "has_variants", - "fieldtype": "Check", - "in_standard_filter": 1, - "label": "Has Variants", - "no_copy": 1 - }, - { - "default": "Item Attribute", - "depends_on": "has_variants", - "fieldname": "variant_based_on", - "fieldtype": "Select", - "label": "Variant Based On", - "options": "Item Attribute\nManufacturer" - }, - { - "depends_on": "eval:(doc.has_variants || doc.variant_of) && doc.variant_based_on==='Item Attribute'", - "fieldname": "attributes", - "fieldtype": "Table", - "hidden": 1, - "label": "Attributes", - "no_copy": 1, - "options": "Item Variant Attribute" - }, - { - "fieldname": "defaults", - "fieldtype": "Section Break", - "label": "Sales, Purchase, Accounting Defaults" - }, - { - "fieldname": "item_defaults", - "fieldtype": "Table", - "label": "Item Defaults", - "options": "Item Default" - }, - { - "collapsible": 1, - "fieldname": "purchase_details", - "fieldtype": "Section Break", - "label": "Purchase, Replenishment Details", - "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart" - }, - { - "default": "1", - "fieldname": "is_purchase_item", - "fieldtype": "Check", - "label": "Is Purchase Item" - }, - { - "fieldname": "purchase_uom", - "fieldtype": "Link", - "label": "Default Purchase Unit of Measure", - "options": "UOM" - }, - { - "default": "0.00", - "depends_on": "is_stock_item", - "fieldname": "min_order_qty", - "fieldtype": "Float", - "label": "Minimum Order Qty", - "oldfieldname": "min_order_qty", - "oldfieldtype": "Currency" - }, - { - "fieldname": "safety_stock", - "fieldtype": "Float", - "label": "Safety Stock" - }, - { - "fieldname": "purchase_details_cb", - "fieldtype": "Column Break" - }, - { - "description": "Average time taken by the supplier to deliver", - "fieldname": "lead_time_days", - "fieldtype": "Int", - "label": "Lead Time in days", - "oldfieldname": "lead_time_days", - "oldfieldtype": "Int" - }, - { - "fieldname": "last_purchase_rate", - "fieldtype": "Float", - "label": "Last Purchase Rate", - "no_copy": 1, - "oldfieldname": "last_purchase_rate", - "oldfieldtype": "Currency", - "read_only": 1 - }, - { - "default": "0", - "fieldname": "is_customer_provided_item", - "fieldtype": "Check", - "label": "Is Customer Provided Item" - }, - { - "depends_on": "eval:doc.is_customer_provided_item==1", - "fieldname": "customer", - "fieldtype": "Link", - "label": "Customer", - "options": "Customer" - }, - { - "collapsible": 1, - "fieldname": "supplier_details", - "fieldtype": "Section Break", - "label": "Supplier Details" - }, - { - "default": "0", - "fieldname": "delivered_by_supplier", - "fieldtype": "Check", - "label": "Delivered by Supplier (Drop Ship)", - "print_hide": 1 - }, - { - "fieldname": "column_break2", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "width": "50%" - }, - { - "fieldname": "supplier_items", - "fieldtype": "Table", - "label": "Supplier Items", - "options": "Item Supplier" - }, - { - "collapsible": 1, - "fieldname": "foreign_trade_details", - "fieldtype": "Section Break", - "label": "Foreign Trade Details" - }, - { - "fieldname": "country_of_origin", - "fieldtype": "Link", - "label": "Country of Origin", - "options": "Country" - }, - { - "fieldname": "column_break_59", - "fieldtype": "Column Break" - }, - { - "fieldname": "customs_tariff_number", - "fieldtype": "Link", - "label": "Customs Tariff Number", - "options": "Customs Tariff Number" - }, - { - "collapsible": 1, - "fieldname": "sales_details", - "fieldtype": "Section Break", - "label": "Sales Details", - "oldfieldtype": "Section Break", - "options": "fa fa-tag" - }, - { - "fieldname": "sales_uom", - "fieldtype": "Link", - "label": "Default Sales Unit of Measure", - "options": "UOM" - }, - { - "default": "1", - "fieldname": "is_sales_item", - "fieldtype": "Check", - "label": "Is Sales Item" - }, - { - "fieldname": "column_break3", - "fieldtype": "Column Break", - "oldfieldtype": "Column Break", - "width": "50%" - }, - { - "fieldname": "max_discount", - "fieldtype": "Float", - "label": "Max Discount (%)", - "oldfieldname": "max_discount", - "oldfieldtype": "Currency" - }, - { - "collapsible": 1, - "fieldname": "deferred_revenue", - "fieldtype": "Section Break", - "label": "Deferred Revenue" - }, - { - "depends_on": "enable_deferred_revenue", - "fieldname": "deferred_revenue_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Deferred Revenue Account", - "options": "Account" - }, - { - "default": "0", - "fieldname": "enable_deferred_revenue", - "fieldtype": "Check", - "label": "Enable Deferred Revenue" - }, - { - "fieldname": "column_break_85", - "fieldtype": "Column Break" - }, - { - "depends_on": "enable_deferred_revenue", - "fieldname": "no_of_months", - "fieldtype": "Int", - "label": "No of Months" - }, - { - "collapsible": 1, - "fieldname": "deferred_expense_section", - "fieldtype": "Section Break", - "label": "Deferred Expense" - }, - { - "depends_on": "enable_deferred_expense", - "fieldname": "deferred_expense_account", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Deferred Expense Account", - "options": "Account" - }, - { - "default": "0", - "fieldname": "enable_deferred_expense", - "fieldtype": "Check", - "label": "Enable Deferred Expense" - }, - { - "fieldname": "column_break_88", - "fieldtype": "Column Break" - }, - { - "depends_on": "enable_deferred_expense", - "fieldname": "no_of_months_exp", - "fieldtype": "Int", - "label": "No of Months" - }, - { - "collapsible": 1, - "fieldname": "customer_details", - "fieldtype": "Section Break", - "label": "Customer Details" - }, - { - "fieldname": "customer_items", - "fieldtype": "Table", - "label": "Customer Items", - "options": "Item Customer Detail" - }, - { - "collapsible": 1, - "collapsible_depends_on": "taxes", - "fieldname": "item_tax_section_break", - "fieldtype": "Section Break", - "label": "Item Tax", - "oldfieldtype": "Section Break", - "options": "fa fa-money" - }, - { - "description": "Will also apply for variants", - "fieldname": "taxes", - "fieldtype": "Table", - "label": "Taxes", - "oldfieldname": "item_tax", - "oldfieldtype": "Table", - "options": "Item Tax" - }, - { - "collapsible": 1, - "fieldname": "inspection_criteria", - "fieldtype": "Section Break", - "label": "Inspection Criteria", - "oldfieldtype": "Section Break", - "options": "fa fa-search" - }, - { - "default": "0", - "fieldname": "inspection_required_before_purchase", - "fieldtype": "Check", - "label": "Inspection Required before Purchase", - "oldfieldname": "inspection_required", - "oldfieldtype": "Select" - }, - { - "default": "0", - "fieldname": "inspection_required_before_delivery", - "fieldtype": "Check", - "label": "Inspection Required before Delivery" - }, - { - "depends_on": "eval:(doc.inspection_required_before_purchase || doc.inspection_required_before_delivery)", - "fieldname": "quality_inspection_template", - "fieldtype": "Link", - "label": "Quality Inspection Template", - "options": "Quality Inspection Template", - "print_hide": 1 - }, - { - "collapsible": 1, - "depends_on": "is_stock_item", - "fieldname": "manufacturing", - "fieldtype": "Section Break", - "label": "Manufacturing", - "oldfieldtype": "Section Break", - "options": "fa fa-cogs" - }, - { - "fieldname": "default_bom", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default BOM", - "no_copy": 1, - "oldfieldname": "default_bom", - "oldfieldtype": "Link", - "options": "BOM", - "read_only": 1 - }, - { - "default": "0", - "description": "If subcontracted to a vendor", - "fieldname": "is_sub_contracted_item", - "fieldtype": "Check", - "label": "Supply Raw Materials for Purchase", - "oldfieldname": "is_sub_contracted_item", - "oldfieldtype": "Select" - }, - { - "fieldname": "column_break_74", - "fieldtype": "Column Break" - }, - { - "fieldname": "customer_code", - "fieldtype": "Data", - "hidden": 1, - "label": "Customer Code", - "no_copy": 1, - "print_hide": 1 - }, - { - "collapsible": 1, - "fieldname": "website_section", - "fieldtype": "Section Break", - "label": "Website", - "options": "fa fa-globe" - }, - { - "default": "0", - "depends_on": "eval:!doc.variant_of", - "fieldname": "show_in_website", - "fieldtype": "Check", - "label": "Show in Website", - "search_index": 1 - }, - { - "default": "0", - "depends_on": "variant_of", - "fieldname": "show_variant_in_website", - "fieldtype": "Check", - "label": "Show in Website (Variant)", - "search_index": 1 - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "route", - "fieldtype": "Small Text", - "label": "Route", - "no_copy": 1 - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Items with higher weightage will be shown higher", - "fieldname": "weightage", - "fieldtype": "Int", - "label": "Weightage" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Show a slideshow at the top of the page", - "fieldname": "slideshow", - "fieldtype": "Link", - "label": "Slideshow", - "options": "Website Slideshow" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Item Image (if not slideshow)", - "fieldname": "website_image", - "fieldtype": "Attach", - "label": "Website Image" - }, - { - "fieldname": "thumbnail", - "fieldtype": "Data", - "label": "Thumbnail", - "read_only": 1 - }, - { - "fieldname": "cb72", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "Show \"In Stock\" or \"Not in Stock\" based on stock available in this warehouse.", - "fieldname": "website_warehouse", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Website Warehouse", - "options": "Warehouse" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "description": "List this Item in multiple groups on the website.", - "fieldname": "website_item_groups", - "fieldtype": "Table", - "label": "Website Item Groups", - "options": "Website Item Group" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "set_meta_tags", - "fieldtype": "Button", - "label": "Set Meta Tags" - }, - { - "collapsible": 1, - "collapsible_depends_on": "website_specifications", - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "sb72", - "fieldtype": "Section Break", - "label": "Website Specifications" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "copy_from_item_group", - "fieldtype": "Button", - "label": "Copy From Item Group" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "website_specifications", - "fieldtype": "Table", - "label": "Website Specifications", - "options": "Item Website Specification" - }, - { - "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", - "fieldname": "web_long_description", - "fieldtype": "Text Editor", - "label": "Website Description" - }, - { - "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.", - "fieldname": "website_content", - "fieldtype": "HTML Editor", - "label": "Website Content" - }, - { - "fieldname": "total_projected_qty", - "fieldtype": "Float", - "hidden": 1, - "label": "Total Projected Qty", - "print_hide": 1, - "read_only": 1 - }, - { - "depends_on": "eval:(!doc.is_item_from_hub)", - "fieldname": "hub_publishing_sb", - "fieldtype": "Section Break", - "label": "Hub Publishing Details" - }, - { - "default": "0", - "description": "Publish Item to hub.erpnext.com", - "fieldname": "publish_in_hub", - "fieldtype": "Check", - "label": "Publish in Hub" - }, - { - "fieldname": "hub_category_to_publish", - "fieldtype": "Data", - "label": "Hub Category to Publish", - "read_only": 1 - }, - { - "description": "Publish \"In Stock\" or \"Not in Stock\" on Hub based on stock available in this warehouse.", - "fieldname": "hub_warehouse", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Hub Warehouse", - "options": "Warehouse" - }, - { - "default": "0", - "fieldname": "synced_with_hub", - "fieldtype": "Check", - "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", - "fieldtype": "Float", - "label": "Over Delivery/Receipt Allowance (%)", - "oldfieldname": "tolerance", - "oldfieldtype": "Currency" - }, - { - "fieldname": "over_billing_allowance", - "fieldtype": "Float", - "label": "Over Billing Allowance (%)", - "depends_on": "eval:!doc.__islocal" - } - ], - "has_web_view": 1, - "icon": "fa fa-tag", - "idx": 2, - "image_field": "image", - "max_attachments": 1, - "modified": "2019-09-03 18:34:13.977931", - "modified_by": "Administrator", - "module": "Stock", - "name": "Item", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "import": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Item Manager", - "share": 1, - "write": 1 - }, - { - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock Manager" - }, - { - "email": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User" - }, - { - "read": 1, - "role": "Sales User" - }, - { - "read": 1, - "role": "Purchase User" - }, - { - "read": 1, - "role": "Maintenance User" - }, - { - "read": 1, - "role": "Accounts User" - }, - { - "read": 1, - "role": "Manufacturing User" - } - ], - "quick_entry": 1, - "search_fields": "item_name,description,item_group,customer_code", - "show_name_in_global_search": 1, - "show_preview_popup": 1, - "sort_field": "idx desc,modified desc", - "sort_order": "DESC", - "title_field": "item_name", - "track_changes": 1 - } + "allow_guest_to_view": 1, + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:item_code", + "creation": "2013-05-03 10:45:46", + "description": "A Product or a Service that is bought, sold or kept in stock.", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "name_and_description_section", + "naming_series", + "item_code", + "variant_of", + "item_name", + "item_group", + "is_item_from_hub", + "stock_uom", + "column_break0", + "disabled", + "allow_alternative_item", + "is_stock_item", + "include_item_in_manufacturing", + "opening_stock", + "valuation_rate", + "standard_rate", + "is_fixed_asset", + "auto_create_assets", + "asset_category", + "asset_naming_series", + "over_delivery_receipt_allowance", + "over_billing_allowance", + "image", + "section_break_11", + "brand", + "description", + "sb_barcodes", + "barcodes", + "inventory_section", + "shelf_life_in_days", + "end_of_life", + "default_material_request_type", + "valuation_method", + "column_break1", + "warranty_period", + "weight_per_unit", + "weight_uom", + "reorder_section", + "reorder_levels", + "unit_of_measure_conversion", + "uoms", + "serial_nos_and_batches", + "has_batch_no", + "create_new_batch", + "batch_number_series", + "has_expiry_date", + "retain_sample", + "sample_quantity", + "column_break_37", + "has_serial_no", + "serial_no_series", + "variants_section", + "has_variants", + "variant_based_on", + "attributes", + "defaults", + "item_defaults", + "purchase_details", + "is_purchase_item", + "purchase_uom", + "min_order_qty", + "safety_stock", + "purchase_details_cb", + "lead_time_days", + "last_purchase_rate", + "is_customer_provided_item", + "customer", + "supplier_details", + "delivered_by_supplier", + "column_break2", + "supplier_items", + "foreign_trade_details", + "country_of_origin", + "column_break_59", + "customs_tariff_number", + "sales_details", + "sales_uom", + "is_sales_item", + "column_break3", + "max_discount", + "deferred_revenue", + "deferred_revenue_account", + "enable_deferred_revenue", + "column_break_85", + "no_of_months", + "deferred_expense_section", + "deferred_expense_account", + "enable_deferred_expense", + "column_break_88", + "no_of_months_exp", + "customer_details", + "customer_items", + "item_tax_section_break", + "taxes", + "inspection_criteria", + "inspection_required_before_purchase", + "inspection_required_before_delivery", + "quality_inspection_template", + "manufacturing", + "default_bom", + "is_sub_contracted_item", + "column_break_74", + "customer_code", + "website_section", + "show_in_website", + "show_variant_in_website", + "route", + "weightage", + "slideshow", + "website_image", + "thumbnail", + "cb72", + "website_warehouse", + "website_item_groups", + "set_meta_tags", + "sb72", + "copy_from_item_group", + "website_specifications", + "web_long_description", + "website_content", + "total_projected_qty", + "hub_publishing_sb", + "publish_in_hub", + "hub_category_to_publish", + "hub_warehouse", + "synced_with_hub", + "manufacturers" + ], + "fields": [ + { + "fieldname": "name_and_description_section", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-flag" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "STO-ITEM-.YYYY.-", + "set_only_once": 1 + }, + { + "bold": 1, + "fieldname": "item_code", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, + { + "depends_on": "variant_of", + "description": "If item is a variant of another item then description, image, pricing, taxes etc will be set from the template unless explicitly specified", + "fieldname": "variant_of", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "in_standard_filter": 1, + "label": "Variant Of", + "options": "Item", + "read_only": 1, + "search_index": 1, + "set_only_once": 1 + }, + { + "bold": 1, + "fieldname": "item_name", + "fieldtype": "Data", + "in_global_search": 1, + "label": "Item Name", + "oldfieldname": "item_name", + "oldfieldtype": "Data", + "search_index": 1 + }, + { + "fieldname": "item_group", + "fieldtype": "Link", + "in_list_view": 1, + "in_preview": 1, + "in_standard_filter": 1, + "label": "Item Group", + "oldfieldname": "item_group", + "oldfieldtype": "Link", + "options": "Item Group", + "reqd": 1, + "search_index": 1 + }, + { + "default": "0", + "fieldname": "is_item_from_hub", + "fieldtype": "Check", + "label": "Is Item from Hub", + "read_only": 1 + }, + { + "fieldname": "stock_uom", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Unit of Measure", + "oldfieldname": "stock_uom", + "oldfieldtype": "Link", + "options": "UOM", + "reqd": 1 + }, + { + "fieldname": "column_break0", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "default": "0", + "fieldname": "allow_alternative_item", + "fieldtype": "Check", + "label": "Allow Alternative Item" + }, + { + "bold": 1, + "default": "1", + "fieldname": "is_stock_item", + "fieldtype": "Check", + "label": "Maintain Stock", + "oldfieldname": "is_stock_item", + "oldfieldtype": "Select" + }, + { + "default": "1", + "fieldname": "include_item_in_manufacturing", + "fieldtype": "Check", + "label": "Include Item In Manufacturing" + }, + { + "bold": 1, + "depends_on": "eval:(doc.__islocal&&doc.is_stock_item && !doc.has_serial_no && !doc.has_batch_no)", + "fieldname": "opening_stock", + "fieldtype": "Float", + "label": "Opening Stock" + }, + { + "depends_on": "is_stock_item", + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "label": "Valuation Rate" + }, + { + "bold": 1, + "depends_on": "eval:doc.__islocal", + "fieldname": "standard_rate", + "fieldtype": "Currency", + "label": "Standard Selling Rate" + }, + { + "default": "0", + "fieldname": "is_fixed_asset", + "fieldtype": "Check", + "label": "Is Fixed Asset", + "set_only_once": 1 + }, + { + "depends_on": "is_fixed_asset", + "fieldname": "asset_category", + "fieldtype": "Link", + "label": "Asset Category", + "options": "Asset Category" + }, + { + "depends_on": "is_fixed_asset", + "fieldname": "asset_naming_series", + "fieldtype": "Select", + "label": "Asset Naming Series" + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "in_preview": 1, + "label": "Image", + "options": "image", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "section_break_11", + "fieldtype": "Section Break", + "label": "Description" + }, + { + "fieldname": "brand", + "fieldtype": "Link", + "label": "Brand", + "oldfieldname": "brand", + "oldfieldtype": "Link", + "options": "Brand", + "print_hide": 1 + }, + { + "fieldname": "description", + "fieldtype": "Text Editor", + "in_preview": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text" + }, + { + "fieldname": "sb_barcodes", + "fieldtype": "Section Break", + "label": "Barcodes" + }, + { + "fieldname": "barcodes", + "fieldtype": "Table", + "label": "Barcodes", + "options": "Item Barcode" + }, + { + "collapsible": 1, + "collapsible_depends_on": "is_stock_item", + "depends_on": "is_stock_item", + "fieldname": "inventory_section", + "fieldtype": "Section Break", + "label": "Inventory", + "oldfieldtype": "Section Break", + "options": "fa fa-truck" + }, + { + "fieldname": "shelf_life_in_days", + "fieldtype": "Int", + "label": "Shelf Life In Days" + }, + { + "default": "2099-12-31", + "depends_on": "is_stock_item", + "fieldname": "end_of_life", + "fieldtype": "Date", + "label": "End of Life", + "oldfieldname": "end_of_life", + "oldfieldtype": "Date" + }, + { + "default": "Purchase", + "fieldname": "default_material_request_type", + "fieldtype": "Select", + "label": "Default Material Request Type", + "options": "Purchase\nMaterial Transfer\nMaterial Issue\nManufacture\nCustomer Provided" + }, + { + "depends_on": "is_stock_item", + "fieldname": "valuation_method", + "fieldtype": "Select", + "label": "Valuation Method", + "options": "\nFIFO\nMoving Average", + "set_only_once": 1 + }, + { + "depends_on": "is_stock_item", + "fieldname": "column_break1", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "width": "50%" + }, + { + "depends_on": "eval:doc.is_stock_item", + "fieldname": "warranty_period", + "fieldtype": "Data", + "label": "Warranty Period (in days)", + "oldfieldname": "warranty_period", + "oldfieldtype": "Data" + }, + { + "depends_on": "is_stock_item", + "fieldname": "weight_per_unit", + "fieldtype": "Float", + "label": "Weight Per Unit" + }, + { + "depends_on": "eval:doc.is_stock_item", + "fieldname": "weight_uom", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Weight UOM", + "options": "UOM" + }, + { + "collapsible": 1, + "depends_on": "is_stock_item", + "fieldname": "reorder_section", + "fieldtype": "Section Break", + "label": "Auto re-order", + "options": "fa fa-rss" + }, + { + "description": "Will also apply for variants unless overrridden", + "fieldname": "reorder_levels", + "fieldtype": "Table", + "label": "Reorder level based on Warehouse", + "options": "Item Reorder" + }, + { + "collapsible": 1, + "fieldname": "unit_of_measure_conversion", + "fieldtype": "Section Break", + "label": "Units of Measure" + }, + { + "description": "Will also apply for variants", + "fieldname": "uoms", + "fieldtype": "Table", + "label": "UOMs", + "oldfieldname": "uom_conversion_details", + "oldfieldtype": "Table", + "options": "UOM Conversion Detail" + }, + { + "collapsible": 1, + "collapsible_depends_on": "eval:doc.has_batch_no || doc.has_serial_no || doc.is_fixed_asset", + "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", + "fieldname": "serial_nos_and_batches", + "fieldtype": "Section Break", + "label": "Serial Nos and Batches" + }, + { + "default": "0", + "depends_on": "eval:doc.is_stock_item", + "fieldname": "has_batch_no", + "fieldtype": "Check", + "label": "Has Batch No", + "no_copy": 1, + "oldfieldname": "has_batch_no", + "oldfieldtype": "Select" + }, + { + "default": "0", + "depends_on": "has_batch_no", + "fieldname": "create_new_batch", + "fieldtype": "Check", + "label": "Automatically Create New Batch" + }, + { + "depends_on": "eval:doc.has_batch_no==1 && doc.create_new_batch==1", + "description": "Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.", + "fieldname": "batch_number_series", + "fieldtype": "Data", + "label": "Batch Number Series", + "translatable": 1 + }, + { + "default": "0", + "depends_on": "has_batch_no", + "fieldname": "has_expiry_date", + "fieldtype": "Check", + "label": "Has Expiry Date" + }, + { + "default": "0", + "fieldname": "retain_sample", + "fieldtype": "Check", + "label": "Retain Sample" + }, + { + "depends_on": "eval: (doc.retain_sample && doc.has_batch_no)", + "description": "Maximum sample quantity that can be retained", + "fieldname": "sample_quantity", + "fieldtype": "Int", + "label": "Max Sample Quantity" + }, + { + "fieldname": "column_break_37", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", + "fieldname": "has_serial_no", + "fieldtype": "Check", + "label": "Has Serial No", + "no_copy": 1, + "oldfieldname": "has_serial_no", + "oldfieldtype": "Select" + }, + { + "depends_on": "eval:doc.is_stock_item || doc.is_fixed_asset", + "description": "Example: ABCD.#####\nIf series is set and Serial No is not mentioned in transactions, then automatic serial number will be created based on this series. If you always want to explicitly mention Serial Nos for this item. leave this blank.", + "fieldname": "serial_no_series", + "fieldtype": "Data", + "label": "Serial Number Series" + }, + { + "collapsible": 1, + "collapsible_depends_on": "attributes", + "fieldname": "variants_section", + "fieldtype": "Section Break", + "label": "Variants" + }, + { + "default": "0", + "depends_on": "eval:!doc.variant_of", + "description": "If this item has variants, then it cannot be selected in sales orders etc.", + "fieldname": "has_variants", + "fieldtype": "Check", + "in_standard_filter": 1, + "label": "Has Variants", + "no_copy": 1 + }, + { + "default": "Item Attribute", + "depends_on": "has_variants", + "fieldname": "variant_based_on", + "fieldtype": "Select", + "label": "Variant Based On", + "options": "Item Attribute\nManufacturer" + }, + { + "depends_on": "eval:(doc.has_variants || doc.variant_of) && doc.variant_based_on==='Item Attribute'", + "fieldname": "attributes", + "fieldtype": "Table", + "hidden": 1, + "label": "Attributes", + "no_copy": 1, + "options": "Item Variant Attribute" + }, + { + "fieldname": "defaults", + "fieldtype": "Section Break", + "label": "Sales, Purchase, Accounting Defaults" + }, + { + "fieldname": "item_defaults", + "fieldtype": "Table", + "label": "Item Defaults", + "options": "Item Default" + }, + { + "collapsible": 1, + "fieldname": "purchase_details", + "fieldtype": "Section Break", + "label": "Purchase, Replenishment Details", + "oldfieldtype": "Section Break", + "options": "fa fa-shopping-cart" + }, + { + "default": "1", + "fieldname": "is_purchase_item", + "fieldtype": "Check", + "label": "Is Purchase Item" + }, + { + "fieldname": "purchase_uom", + "fieldtype": "Link", + "label": "Default Purchase Unit of Measure", + "options": "UOM" + }, + { + "default": "0.00", + "depends_on": "is_stock_item", + "fieldname": "min_order_qty", + "fieldtype": "Float", + "label": "Minimum Order Qty", + "oldfieldname": "min_order_qty", + "oldfieldtype": "Currency" + }, + { + "fieldname": "safety_stock", + "fieldtype": "Float", + "label": "Safety Stock" + }, + { + "fieldname": "purchase_details_cb", + "fieldtype": "Column Break" + }, + { + "description": "Average time taken by the supplier to deliver", + "fieldname": "lead_time_days", + "fieldtype": "Int", + "label": "Lead Time in days", + "oldfieldname": "lead_time_days", + "oldfieldtype": "Int" + }, + { + "fieldname": "last_purchase_rate", + "fieldtype": "Float", + "label": "Last Purchase Rate", + "no_copy": 1, + "oldfieldname": "last_purchase_rate", + "oldfieldtype": "Currency", + "read_only": 1 + }, + { + "default": "0", + "fieldname": "is_customer_provided_item", + "fieldtype": "Check", + "label": "Is Customer Provided Item" + }, + { + "depends_on": "eval:doc.is_customer_provided_item==1", + "fieldname": "customer", + "fieldtype": "Link", + "label": "Customer", + "options": "Customer" + }, + { + "collapsible": 1, + "fieldname": "supplier_details", + "fieldtype": "Section Break", + "label": "Supplier Details" + }, + { + "default": "0", + "fieldname": "delivered_by_supplier", + "fieldtype": "Check", + "label": "Delivered by Supplier (Drop Ship)", + "print_hide": 1 + }, + { + "fieldname": "column_break2", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "width": "50%" + }, + { + "fieldname": "supplier_items", + "fieldtype": "Table", + "label": "Supplier Items", + "options": "Item Supplier" + }, + { + "collapsible": 1, + "fieldname": "foreign_trade_details", + "fieldtype": "Section Break", + "label": "Foreign Trade Details" + }, + { + "fieldname": "country_of_origin", + "fieldtype": "Link", + "label": "Country of Origin", + "options": "Country" + }, + { + "fieldname": "column_break_59", + "fieldtype": "Column Break" + }, + { + "fieldname": "customs_tariff_number", + "fieldtype": "Link", + "label": "Customs Tariff Number", + "options": "Customs Tariff Number" + }, + { + "collapsible": 1, + "fieldname": "sales_details", + "fieldtype": "Section Break", + "label": "Sales Details", + "oldfieldtype": "Section Break", + "options": "fa fa-tag" + }, + { + "fieldname": "sales_uom", + "fieldtype": "Link", + "label": "Default Sales Unit of Measure", + "options": "UOM" + }, + { + "default": "1", + "fieldname": "is_sales_item", + "fieldtype": "Check", + "label": "Is Sales Item" + }, + { + "fieldname": "column_break3", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "width": "50%" + }, + { + "fieldname": "max_discount", + "fieldtype": "Float", + "label": "Max Discount (%)", + "oldfieldname": "max_discount", + "oldfieldtype": "Currency" + }, + { + "collapsible": 1, + "fieldname": "deferred_revenue", + "fieldtype": "Section Break", + "label": "Deferred Revenue" + }, + { + "depends_on": "enable_deferred_revenue", + "fieldname": "deferred_revenue_account", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Deferred Revenue Account", + "options": "Account" + }, + { + "default": "0", + "fieldname": "enable_deferred_revenue", + "fieldtype": "Check", + "label": "Enable Deferred Revenue" + }, + { + "fieldname": "column_break_85", + "fieldtype": "Column Break" + }, + { + "depends_on": "enable_deferred_revenue", + "fieldname": "no_of_months", + "fieldtype": "Int", + "label": "No of Months" + }, + { + "collapsible": 1, + "fieldname": "deferred_expense_section", + "fieldtype": "Section Break", + "label": "Deferred Expense" + }, + { + "depends_on": "enable_deferred_expense", + "fieldname": "deferred_expense_account", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Deferred Expense Account", + "options": "Account" + }, + { + "default": "0", + "fieldname": "enable_deferred_expense", + "fieldtype": "Check", + "label": "Enable Deferred Expense" + }, + { + "fieldname": "column_break_88", + "fieldtype": "Column Break" + }, + { + "depends_on": "enable_deferred_expense", + "fieldname": "no_of_months_exp", + "fieldtype": "Int", + "label": "No of Months" + }, + { + "collapsible": 1, + "fieldname": "customer_details", + "fieldtype": "Section Break", + "label": "Customer Details" + }, + { + "fieldname": "customer_items", + "fieldtype": "Table", + "label": "Customer Items", + "options": "Item Customer Detail" + }, + { + "collapsible": 1, + "collapsible_depends_on": "taxes", + "fieldname": "item_tax_section_break", + "fieldtype": "Section Break", + "label": "Item Tax", + "oldfieldtype": "Section Break", + "options": "fa fa-money" + }, + { + "description": "Will also apply for variants", + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Taxes", + "oldfieldname": "item_tax", + "oldfieldtype": "Table", + "options": "Item Tax" + }, + { + "collapsible": 1, + "fieldname": "inspection_criteria", + "fieldtype": "Section Break", + "label": "Inspection Criteria", + "oldfieldtype": "Section Break", + "options": "fa fa-search" + }, + { + "default": "0", + "fieldname": "inspection_required_before_purchase", + "fieldtype": "Check", + "label": "Inspection Required before Purchase", + "oldfieldname": "inspection_required", + "oldfieldtype": "Select" + }, + { + "default": "0", + "fieldname": "inspection_required_before_delivery", + "fieldtype": "Check", + "label": "Inspection Required before Delivery" + }, + { + "depends_on": "eval:(doc.inspection_required_before_purchase || doc.inspection_required_before_delivery)", + "fieldname": "quality_inspection_template", + "fieldtype": "Link", + "label": "Quality Inspection Template", + "options": "Quality Inspection Template", + "print_hide": 1 + }, + { + "collapsible": 1, + "depends_on": "is_stock_item", + "fieldname": "manufacturing", + "fieldtype": "Section Break", + "label": "Manufacturing", + "oldfieldtype": "Section Break", + "options": "fa fa-cogs" + }, + { + "fieldname": "default_bom", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default BOM", + "no_copy": 1, + "oldfieldname": "default_bom", + "oldfieldtype": "Link", + "options": "BOM", + "read_only": 1 + }, + { + "default": "0", + "description": "If subcontracted to a vendor", + "fieldname": "is_sub_contracted_item", + "fieldtype": "Check", + "label": "Supply Raw Materials for Purchase", + "oldfieldname": "is_sub_contracted_item", + "oldfieldtype": "Select" + }, + { + "fieldname": "column_break_74", + "fieldtype": "Column Break" + }, + { + "fieldname": "customer_code", + "fieldtype": "Data", + "hidden": 1, + "label": "Customer Code", + "no_copy": 1, + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "website_section", + "fieldtype": "Section Break", + "label": "Website", + "options": "fa fa-globe" + }, + { + "default": "0", + "depends_on": "eval:!doc.variant_of", + "fieldname": "show_in_website", + "fieldtype": "Check", + "label": "Show in Website", + "search_index": 1 + }, + { + "default": "0", + "depends_on": "variant_of", + "fieldname": "show_variant_in_website", + "fieldtype": "Check", + "label": "Show in Website (Variant)", + "search_index": 1 + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "fieldname": "route", + "fieldtype": "Small Text", + "label": "Route", + "no_copy": 1 + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "description": "Items with higher weightage will be shown higher", + "fieldname": "weightage", + "fieldtype": "Int", + "label": "Weightage" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "description": "Show a slideshow at the top of the page", + "fieldname": "slideshow", + "fieldtype": "Link", + "label": "Slideshow", + "options": "Website Slideshow" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "description": "Item Image (if not slideshow)", + "fieldname": "website_image", + "fieldtype": "Attach", + "label": "Website Image" + }, + { + "fieldname": "thumbnail", + "fieldtype": "Data", + "label": "Thumbnail", + "read_only": 1 + }, + { + "fieldname": "cb72", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "description": "Show \"In Stock\" or \"Not in Stock\" based on stock available in this warehouse.", + "fieldname": "website_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Website Warehouse", + "options": "Warehouse" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "description": "List this Item in multiple groups on the website.", + "fieldname": "website_item_groups", + "fieldtype": "Table", + "label": "Website Item Groups", + "options": "Website Item Group" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "fieldname": "set_meta_tags", + "fieldtype": "Button", + "label": "Set Meta Tags" + }, + { + "collapsible": 1, + "collapsible_depends_on": "website_specifications", + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "fieldname": "sb72", + "fieldtype": "Section Break", + "label": "Website Specifications" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "fieldname": "copy_from_item_group", + "fieldtype": "Button", + "label": "Copy From Item Group" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "fieldname": "website_specifications", + "fieldtype": "Table", + "label": "Website Specifications", + "options": "Item Website Specification" + }, + { + "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website", + "fieldname": "web_long_description", + "fieldtype": "Text Editor", + "label": "Website Description" + }, + { + "description": "You can use any valid Bootstrap 4 markup in this field. It will be shown on your Item Page.", + "fieldname": "website_content", + "fieldtype": "HTML Editor", + "label": "Website Content" + }, + { + "fieldname": "total_projected_qty", + "fieldtype": "Float", + "hidden": 1, + "label": "Total Projected Qty", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval:(!doc.is_item_from_hub)", + "fieldname": "hub_publishing_sb", + "fieldtype": "Section Break", + "label": "Hub Publishing Details" + }, + { + "default": "0", + "description": "Publish Item to hub.erpnext.com", + "fieldname": "publish_in_hub", + "fieldtype": "Check", + "label": "Publish in Hub" + }, + { + "fieldname": "hub_category_to_publish", + "fieldtype": "Data", + "label": "Hub Category to Publish", + "read_only": 1 + }, + { + "description": "Publish \"In Stock\" or \"Not in Stock\" on Hub based on stock available in this warehouse.", + "fieldname": "hub_warehouse", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Hub Warehouse", + "options": "Warehouse" + }, + { + "default": "0", + "fieldname": "synced_with_hub", + "fieldtype": "Check", + "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", + "fieldtype": "Float", + "label": "Over Delivery/Receipt Allowance (%)", + "oldfieldname": "tolerance", + "oldfieldtype": "Currency" + }, + { + "depends_on": "eval:!doc.__islocal", + "fieldname": "over_billing_allowance", + "fieldtype": "Float", + "label": "Over Billing Allowance (%)" + }, + { + "default": "0", + "depends_on": "is_fixed_asset", + "fieldname": "auto_create_assets", + "fieldtype": "Check", + "label": "Auto Create Assets on Purchase" + } + ], + "has_web_view": 1, + "icon": "fa fa-tag", + "idx": 2, + "image_field": "image", + "max_attachments": 1, + "modified": "2019-10-09 17:05:59.576119", + "modified_by": "Administrator", + "module": "Stock", + "name": "Item", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Item Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock Manager" + }, + { + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User" + }, + { + "read": 1, + "role": "Sales User" + }, + { + "read": 1, + "role": "Purchase User" + }, + { + "read": 1, + "role": "Maintenance User" + }, + { + "read": 1, + "role": "Accounts User" + }, + { + "read": 1, + "role": "Manufacturing User" + } + ], + "quick_entry": 1, + "search_fields": "item_name,description,item_group,customer_code", + "show_name_in_global_search": 1, + "show_preview_popup": 1, + "sort_field": "idx desc,modified desc", + "sort_order": "DESC", + "title_field": "item_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json index 66c33a1c9dd..90a392c1450 100644 --- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json +++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json @@ -13,6 +13,7 @@ "qty", "rate", "amount", + "is_fixed_asset", "applicable_charges", "purchase_receipt_item", "accounting_dimensions_section", @@ -119,14 +120,25 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "default": "0", + "fetch_from": "item_code.is_fixed_asset", + "fieldname": "is_fixed_asset", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Fixed Asset", + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-05-26 09:48:15.569956", + "modified": "2019-11-12 15:41:21.053462", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Item", "owner": "wasim@webnotestech.com", - "permissions": [] + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js index c9a3fd976f3..5de13525183 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js @@ -129,6 +129,10 @@ erpnext.stock.LandedCostVoucher = erpnext.stock.StockController.extend({ }, distribute_charges_based_on: function (frm) { this.set_applicable_charges_for_item(); + }, + + items_remove: () => { + this.trigger('set_applicable_charges_for_item'); } }); 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 c2c669211a4..46fdc8fc10e 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -1,545 +1,149 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2014-07-11 11:33:42.547339", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "autoname": "naming_series:", + "creation": "2014-07-11 11:33:42.547339", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "naming_series", + "company", + "purchase_receipts", + "sec_break1", + "taxes", + "purchase_receipt_items", + "get_items_from_purchase_receipts", + "items", + "section_break_9", + "total_taxes_and_charges", + "col_break1", + "distribute_charges_based_on", + "amended_from", + "sec_break2", + "landed_cost_help" + ], "fields": [ { - "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, - "options": "MAT-LCV-.YYYY.-", - "permlevel": 0, - "precision": "", - "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 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "options": "MAT-LCV-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, { - "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": 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, - "translatable": 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_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_receipts", - "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": "Purchase Receipts", - "length": 0, - "no_copy": 0, - "options": "Landed Cost Purchase Receipt", - "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 - }, + "fieldname": "purchase_receipts", + "fieldtype": "Table", + "label": "Purchase Receipts", + "options": "Landed Cost Purchase Receipt", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purchase_receipt_items", - "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": "Purchase Receipt Items", - "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": "purchase_receipt_items", + "fieldtype": "Section Break", + "label": "Purchase Receipt Items" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "get_items_from_purchase_receipts", - "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": "Get Items From Purchase Receipts", - "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": "get_items_from_purchase_receipts", + "fieldtype": "Button", + "label": "Get Items From Purchase Receipts" + }, { - "allow_bulk_edit": 0, - "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": "Purchase Receipt Items", - "length": 0, - "no_copy": 1, - "options": "Landed Cost 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 - }, + "fieldname": "items", + "fieldtype": "Table", + "label": "Purchase Receipt Items", + "no_copy": 1, + "options": "Landed Cost Item", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sec_break1", - "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 Charges", - "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 - }, + "fieldname": "sec_break1", + "fieldtype": "Section Break", + "label": "Applicable Charges" + }, { - "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": "Taxes and Charges", - "length": 0, - "no_copy": 0, - "options": "Landed Cost 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 - }, + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Taxes and Charges", + "options": "Landed Cost Taxes and Charges", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_9", - "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_9", + "fieldtype": "Section Break" + }, { - "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": "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "total_taxes_and_charges", + "fieldtype": "Currency", + "label": "Total Taxes and Charges", + "options": "Company:company:default_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": "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, - "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": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "distribute_charges_based_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": "Distribute Charges Based On", - "length": 0, - "no_copy": 0, - "options": "\nQty\nAmount", - "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": "distribute_charges_based_on", + "fieldtype": "Select", + "label": "Distribute Charges Based On", + "options": "Qty\nAmount", + "reqd": 1 + }, { - "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": 0, - "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, - "options": "Landed Cost Voucher", - "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 - }, + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Landed Cost Voucher", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sec_break2", - "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": "sec_break2", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "landed_cost_help", - "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": "Landed Cost Help", - "length": 0, - "no_copy": 0, - "options": "", - "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": "landed_cost_help", + "fieldtype": "HTML", + "label": "Landed Cost Help" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-usd", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 14:44:30.850736", - "modified_by": "Administrator", - "module": "Stock", - "name": "Landed Cost Voucher", - "name_case": "", - "owner": "Administrator", + ], + "icon": "icon-usd", + "is_submittable": 1, + "modified": "2019-10-09 13:39:36.082777", + "modified_by": "Administrator", + "module": "Stock", + "name": "Landed Cost Voucher", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 0, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 1, - "role": "Stock Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "export": 1, + "read": 1, + "report": 1, + "role": "Stock Manager", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file 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 3f370935ef1..173b394f797 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -16,16 +16,13 @@ class LandedCostVoucher(Document): if pr.receipt_document_type and pr.receipt_document: pr_items = frappe.db.sql("""select pr_item.item_code, pr_item.description, pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name, - pr_item.cost_center, pr_item.asset + pr_item.cost_center, pr_item.is_fixed_asset from `tab{doctype} Item` pr_item where parent = %s and exists(select name from tabItem where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1)) """.format(doctype=pr.receipt_document_type), pr.receipt_document, as_dict=True) for d in pr_items: - if d.asset and frappe.db.get_value("Asset", d.asset, 'docstatus') == 1: - continue - item = self.append("items") item.item_code = d.item_code item.description = d.description @@ -37,15 +34,16 @@ class LandedCostVoucher(Document): item.receipt_document_type = pr.receipt_document_type item.receipt_document = pr.receipt_document item.purchase_receipt_item = d.name + item.is_fixed_asset = d.is_fixed_asset def validate(self): self.check_mandatory() - self.validate_purchase_receipts() - self.set_total_taxes_and_charges() if not self.get("items"): self.get_items_from_purchase_receipts() else: self.validate_applicable_charges_for_item() + self.validate_purchase_receipts() + self.set_total_taxes_and_charges() def check_mandatory(self): if not self.get("purchase_receipts"): @@ -64,6 +62,7 @@ class LandedCostVoucher(Document): for item in self.get("items"): if not item.receipt_document: frappe.throw(_("Item must be added using 'Get Items from Purchase Receipts' button")) + elif item.receipt_document not in receipt_documents: frappe.throw(_("Item Row {0}: {1} {2} does not exist in above '{1}' table") .format(item.idx, item.receipt_document_type, item.receipt_document)) @@ -96,8 +95,6 @@ class LandedCostVoucher(Document): else: frappe.throw(_("Total Applicable Charges in Purchase Receipt Items table must be same as Total Taxes and Charges")) - - def on_submit(self): self.update_landed_cost() @@ -107,6 +104,9 @@ class LandedCostVoucher(Document): def update_landed_cost(self): for d in self.get("purchase_receipts"): doc = frappe.get_doc(d.receipt_document_type, d.receipt_document) + + # check if there are {qty} assets created and linked to this receipt document + self.validate_asset_qty_and_status(d.receipt_document_type, doc) # set landed cost voucher amount in pr item doc.set_landed_cost_voucher_amount() @@ -118,23 +118,42 @@ class LandedCostVoucher(Document): for item in doc.get("items"): item.db_update() + # asset rate will be updated while creating asset gl entries from PI or PY + # update latest valuation rate in serial no - self.update_rate_in_serial_no(doc) + self.update_rate_in_serial_no_for_non_asset_items(doc) # update stock & gl entries for cancelled state of PR doc.docstatus = 2 doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) doc.make_gl_entries_on_cancel(repost_future_gle=False) - # update stock & gl entries for submit state of PR doc.docstatus = 1 doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True) doc.make_gl_entries() - def update_rate_in_serial_no(self, receipt_document): + def validate_asset_qty_and_status(self, receipt_document_type, receipt_document): + for item in self.get('items'): + 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']) + 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)) + if docs: + for d in docs: + 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) + ) + + def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): - if item.serial_no: + if not item.is_fixed_asset and item.serial_no: serial_nos = get_serial_nos(item.serial_no) if serial_nos: frappe.db.sql("update `tabSerial No` set purchase_rate=%s where name in ({0})" diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index aef53ed74ba..d5914f9b28d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -6,27 +6,35 @@ frappe.provide("erpnext.stock"); frappe.ui.form.on("Purchase Receipt", { - setup: function(frm) { + setup: (frm) => { + frm.make_methods = { + 'Landed Cost Voucher': () => { + let lcv = frappe.model.get_new_doc('Landed Cost Voucher'); + lcv.company = frm.doc.company; + + let lcv_receipt = frappe.model.get_new_doc('Landed Cost Purchase Receipt'); + lcv_receipt.receipt_document_type = 'Purchase Receipt'; + lcv_receipt.receipt_document = frm.doc.name; + lcv_receipt.supplier = frm.doc.supplier; + lcv_receipt.grand_total = frm.doc.grand_total; + lcv.purchase_receipts = [lcv_receipt]; + + frappe.set_route("Form", lcv.doctype, lcv.name); + }, + } + frm.custom_make_buttons = { 'Stock Entry': 'Return', 'Purchase Invoice': 'Invoice' }; - frm.set_query("asset", "items", function() { - return { - filters: { - "purchase_receipt": frm.doc.name - } - } - }); - frm.set_query("expense_account", "items", function() { return { query: "erpnext.controllers.queries.get_expense_account", - filters: {'company': frm.doc.company} + filters: {'company': frm.doc.company } } }); - + }, onload: function(frm) { erpnext.queries.setup_queries(frm, "Warehouse", function() { @@ -57,7 +65,7 @@ frappe.ui.form.on("Purchase Receipt", { toggle_display_account_head: function(frm) { var enabled = erpnext.is_perpetual_inventory_enabled(frm.doc.company) frm.fields_dict["items"].grid.set_column_disp(["cost_center"], enabled); - }, + } }); erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend({ diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 1bfdca50ea1..56fd47e1bc1 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -281,7 +281,7 @@ class PurchaseReceipt(BuyingController): d.rejected_warehouse not in warehouse_with_no_account: warehouse_with_no_account.append(d.warehouse) - self.get_asset_gl_entry(gl_entries, expenses_included_in_valuation) + self.get_asset_gl_entry(gl_entries) # Cost center-wise amount breakup for other charges included for valuation valuation_tax = {} for tax in self.get("taxes"): @@ -335,81 +335,85 @@ class PurchaseReceipt(BuyingController): return process_gl_map(gl_entries) - def get_asset_gl_entry(self, gl_entries, expenses_included_in_valuation=None): - arbnb_account, cwip_account = None, None - - if not expenses_included_in_valuation: - expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") - - for d in self.get("items"): - asset_category = frappe.get_cached_value("Item", d.item_code, "asset_category") - cwip_enabled = is_cwip_accounting_enabled(self.company, asset_category) - - if d.is_fixed_asset and not (arbnb_account and cwip_account): - arbnb_account = self.get_company_default("asset_received_but_not_billed") - - # CWIP entry - cwip_account = get_asset_account("capital_work_in_progress_account", d.asset, - company = self.company) - - if d.is_fixed_asset and cwip_enabled: - asset_amount = flt(d.net_amount) + flt(d.item_tax_amount/self.conversion_rate) - base_asset_amount = flt(d.base_net_amount + d.item_tax_amount) - - cwip_account_currency = get_account_currency(cwip_account) - gl_entries.append(self.get_gl_dict({ - "account": cwip_account, - "against": arbnb_account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": (base_asset_amount - if cwip_account_currency == self.company_currency else asset_amount) - }, item=d)) - - # Asset received but not billed - asset_rbnb_currency = get_account_currency(arbnb_account) - gl_entries.append(self.get_gl_dict({ - "account": arbnb_account, - "against": cwip_account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "credit": base_asset_amount, - "credit_in_account_currency": (base_asset_amount - if asset_rbnb_currency == self.company_currency else asset_amount) - }, item=d)) - - if d.is_fixed_asset and flt(d.landed_cost_voucher_amount): - asset_account = (get_asset_category_account(d.asset, 'fixed_asset_account', - company = self.company) if not cwip_enabled else cwip_account) - - gl_entries.append(self.get_gl_dict({ - "account": expenses_included_in_valuation, - "against": asset_account, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(d.landed_cost_voucher_amount), - "project": d.project - }, item=d)) - - gl_entries.append(self.get_gl_dict({ - "account": asset_account, - "against": expenses_included_in_valuation, - "cost_center": d.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(d.landed_cost_voucher_amount), - "project": d.project - }, item=d)) - - if d.asset: - doc = frappe.get_doc("Asset", d.asset) - frappe.db.set_value("Asset", d.asset, "gross_purchase_amount", - doc.gross_purchase_amount + flt(d.landed_cost_voucher_amount)) - - frappe.db.set_value("Asset", d.asset, "purchase_receipt_amount", - doc.purchase_receipt_amount + flt(d.landed_cost_voucher_amount)) - + 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): + self.add_asset_gl_entries(item, gl_entries) + if flt(item.landed_cost_voucher_amount): + self.add_lcv_gl_entries(item, gl_entries) + # update assets gross amount by its valuation rate + # valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item + self.update_assets(item, item.valuation_rate) return gl_entries + + 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) + + 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) + + cwip_account_currency = get_account_currency(cwip_account) + # debit cwip account + gl_entries.append(self.get_gl_dict({ + "account": cwip_account, + "against": arbnb_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": base_asset_amount, + "debit_in_account_currency": (base_asset_amount + if cwip_account_currency == self.company_currency else asset_amount) + }, item=item)) + + asset_rbnb_currency = get_account_currency(arbnb_account) + # credit arbnb account + gl_entries.append(self.get_gl_dict({ + "account": arbnb_account, + "against": cwip_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "credit": base_asset_amount, + "credit_in_account_currency": (base_asset_amount + if asset_rbnb_currency == self.company_currency else asset_amount) + }, item=item)) + + 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): + asset_account = get_asset_category_account(asset_category=item.asset_category, \ + fieldname='fixed_asset_account', company=self.company) + else: + # This returns company's default cwip account + asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) + + gl_entries.append(self.get_gl_dict({ + "account": expenses_included_in_asset_valuation, + "against": asset_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project + }, item=item)) + + gl_entries.append(self.get_gl_dict({ + "account": asset_account, + "against": expenses_included_in_asset_valuation, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project + }, item=item)) + + def update_assets(self, item, valuation_rate): + assets = frappe.db.get_all('Asset', + filters={ 'purchase_receipt': self.name, 'item_code': item.item_code } + ) + + for asset in assets: + frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(valuation_rate)) + frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate)) def update_status(self, status): self.set_status(update=True, status = status) @@ -517,7 +521,8 @@ def make_purchase_invoice(source_name, target_doc=None): "purchase_order_item": "po_detail", "purchase_order": "purchase_order", "is_fixed_asset": "is_fixed_asset", - "asset": "asset", + "asset_location": "asset_location", + "asset_category": 'asset_category' }, "postprocess": update_item, "filter": lambda d: get_pending_qty(d)[0] <= 0 if not doc.get("is_return") else get_pending_qty(d)[0] > 0 diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index e9ddf9d9761..2afb69353f4 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -281,8 +281,8 @@ class TestPurchaseReceipt(unittest.TestCase): serial_no=serial_no, basic_rate=100, do_not_submit=True) self.assertRaises(SerialNoDuplicateError, se.submit) - def test_serialized_asset_item(self): - asset_item = "Test Serialized Asset Item" + def test_auto_asset_creation(self): + asset_item = "Test Asset Item" if not frappe.db.exists('Item', asset_item): asset_category = frappe.get_all('Asset Category') @@ -308,30 +308,18 @@ class TestPurchaseReceipt(unittest.TestCase): asset_category = doc.name item_data = make_item(asset_item, {'is_stock_item':0, - 'stock_uom': 'Box', 'is_fixed_asset': 1, 'has_serial_no': 1, - 'asset_category': asset_category, 'serial_no_series': 'ABC.###'}) + 'stock_uom': 'Box', 'is_fixed_asset': 1, 'auto_create_assets': 1, + 'asset_category': asset_category, 'asset_naming_series': 'ABC.###'}) asset_item = item_data.item_code pr = make_purchase_receipt(item_code=asset_item, qty=3) - asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name}, 'name') - asset_movement = frappe.db.get_value('Asset Movement', {'reference_name': pr.name}, 'name') - serial_nos = frappe.get_all('Serial No', {'asset': asset}, 'name') + assets = frappe.db.get_all('Asset', filters={'purchase_receipt': pr.name}) - self.assertEquals(len(serial_nos), 3) + self.assertEquals(len(assets), 3) - location = frappe.db.get_value('Serial No', serial_nos[0].name, 'location') + location = frappe.db.get_value('Asset', assets[0].name, 'location') self.assertEquals(location, "Test Location") - frappe.db.set_value("Asset", asset, "purchase_receipt", "") - frappe.db.set_value("Purchase Receipt Item", pr.items[0].name, "asset", "") - - pr.load_from_db() - - pr.cancel() - serial_nos = frappe.get_all('Serial No', {'asset': asset}, 'name') or [] - self.assertEquals(len(serial_nos), 0) - frappe.db.sql("delete from `tabAsset`") - def test_purchase_receipt_for_enable_allow_cost_center_in_entry_of_bs_account(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center accounts_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') @@ -534,8 +522,10 @@ def make_purchase_receipt(**args): received_qty = args.received_qty or qty rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty) + item_code = args.item or args.item_code or "_Test Item" + uom = args.uom or frappe.db.get_value("Item", item_code, "stock_uom") or "_Test UOM" pr.append("items", { - "item_code": args.item or args.item_code or "_Test Item", + "item_code": item_code, "warehouse": args.warehouse or "_Test Warehouse - _TC", "qty": qty, "received_qty": received_qty, @@ -545,7 +535,7 @@ def make_purchase_receipt(**args): "conversion_factor": args.conversion_factor or 1.0, "serial_no": args.serial_no, "stock_uom": args.stock_uom or "_Test UOM", - "uom": args.uom or "_Test UOM", + "uom": uom, "cost_center": args.cost_center or frappe.get_cached_value('Company', pr.company, 'cost_center'), "asset_location": args.location or "Test Location" }) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index 446a488a7e6..16ec8db335c 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -67,26 +67,26 @@ "warehouse_and_reference", "warehouse", "rejected_warehouse", - "quality_inspection", "purchase_order", "material_request", - "purchase_order_item", - "material_request_item", "column_break_40", "is_fixed_asset", - "asset", "asset_location", + "asset_category", "schedule_date", + "quality_inspection", "stock_qty", + "purchase_order_item", + "material_request_item", "section_break_45", + "allow_zero_valuation_rate", + "bom", + "col_break5", "serial_no", "batch_no", "column_break_48", "rejected_serial_no", "expense_account", - "col_break5", - "allow_zero_valuation_rate", - "bom", "include_exploded_items", "item_tax_rate", "accounting_dimensions_section", @@ -500,21 +500,6 @@ "print_hide": 1, "read_only": 1 }, - { - "depends_on": "is_fixed_asset", - "fieldname": "asset", - "fieldtype": "Link", - "label": "Asset", - "no_copy": 1, - "options": "Asset" - }, - { - "depends_on": "is_fixed_asset", - "fieldname": "asset_location", - "fieldtype": "Link", - "label": "Asset Location", - "options": "Location" - }, { "fieldname": "purchase_order", "fieldtype": "Link", @@ -553,6 +538,7 @@ "fieldtype": "Section Break" }, { + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "serial_no", "fieldtype": "Small Text", "in_list_view": 1, @@ -562,10 +548,11 @@ "oldfieldtype": "Text" }, { + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "batch_no", "fieldtype": "Link", "in_list_view": 1, - "label": "Batch No", + "label": "Batch No!", "no_copy": 1, "oldfieldname": "batch_no", "oldfieldtype": "Link", @@ -577,6 +564,7 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:!doc.is_fixed_asset", "fieldname": "rejected_serial_no", "fieldtype": "Small Text", "label": "Rejected Serial No", @@ -814,11 +802,28 @@ "fieldtype": "Data", "label": "Manufacturer Part Number", "read_only": 1 + }, + { + "depends_on": "is_fixed_asset", + "fieldname": "asset_location", + "fieldtype": "Link", + "label": "Asset Location", + "options": "Location" + }, + { + "depends_on": "is_fixed_asset", + "fetch_from": "item_code.asset_category", + "fieldname": "asset_category", + "fieldtype": "Link", + "in_preview": 1, + "label": "Asset Category", + "options": "Asset Category", + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-09-17 22:33:01.109004", + "modified": "2019-10-14 16:03:25.499557", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From 87c6718d90de2648918e9ef28a7674e67ce9e1c3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 18 Nov 2019 12:34:30 +0530 Subject: [PATCH 198/679] fix: Book valuation expense in specified account rather than expense included in valuation account (#19590) * fix: Book valuation expense in specified accout rather than expense included in valuation account * fix: Remove undefined variable * fix: Test cases * fix: Test Case --- .../purchase_invoice/purchase_invoice.py | 60 ++++++++++--------- .../purchase_invoice/test_purchase_invoice.py | 29 +++++++-- .../test_landed_cost_voucher.py | 5 +- .../purchase_receipt/purchase_receipt.py | 47 ++++++++------- .../purchase_receipt/test_purchase_receipt.py | 5 +- 5 files changed, 88 insertions(+), 58 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 4fbf9a10095..5c53d26ad12 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -714,14 +714,14 @@ class PurchaseInvoice(BuyingController): if account_currency==self.company_currency \ else tax.tax_amount_after_discount_amount, "cost_center": tax.cost_center - }, account_currency) + }, account_currency, item=tax) ) # accumulate valuation tax if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): if self.auto_accounting_for_stock and not tax.cost_center: frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) - valuation_tax.setdefault(tax.cost_center, 0) - valuation_tax[tax.cost_center] += \ + valuation_tax.setdefault(tax.name, 0) + valuation_tax[tax.name] += \ (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax: @@ -731,36 +731,38 @@ class PurchaseInvoice(BuyingController): total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = self.negative_expense_to_be_booked i = 1 - for cost_center, amount in iteritems(valuation_tax): - if i == len(valuation_tax): - applicable_amount = amount_including_divisional_loss - else: - applicable_amount = self.negative_expense_to_be_booked * (amount / total_valuation_amount) - amount_including_divisional_loss -= applicable_amount + for tax in self.get("taxes"): + if valuation_tax.get(tax.name): + if i == len(valuation_tax): + applicable_amount = amount_including_divisional_loss + else: + applicable_amount = self.negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) + amount_including_divisional_loss -= applicable_amount - gl_entries.append( - self.get_gl_dict({ - "account": self.expenses_included_in_valuation, - "cost_center": cost_center, - "against": self.supplier, - "credit": applicable_amount, - "remarks": self.remarks or "Accounting Entry for Stock" - }) - ) + gl_entries.append( + self.get_gl_dict({ + "account": tax.account_head, + "cost_center": tax.cost_center, + "against": self.supplier, + "credit": applicable_amount, + "remarks": self.remarks or _("Accounting Entry for Stock"), + }, item=tax) + ) - i += 1 + i += 1 if self.auto_accounting_for_stock and self.update_stock and valuation_tax: - for cost_center, amount in iteritems(valuation_tax): - gl_entries.append( - self.get_gl_dict({ - "account": self.expenses_included_in_valuation, - "cost_center": cost_center, - "against": self.supplier, - "credit": amount, - "remarks": self.remarks or "Accounting Entry for Stock" - }) - ) + for tax in self.get("taxes"): + if valuation_tax.get(tax.name): + gl_entries.append( + self.get_gl_dict({ + "account": tax.account_head, + "cost_center": tax.cost_center, + "against": self.supplier, + "credit": valuation_tax[tax.name], + "remarks": self.remarks or "Accounting Entry for Stock" + }, item=tax) + ) def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index b2ad4f4d512..85b11667902 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -204,19 +204,40 @@ class TestPurchaseInvoice(unittest.TestCase): pi.insert() pi.submit() - self.check_gle_for_pi(pi.name) + self.check_gle_for_pi_against_pr(pi.name) def check_gle_for_pi(self, pi): - gl_entries = frappe.db.sql("""select account, debit, credit + 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 - order by account asc""", pi, as_dict=1) + 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", 500.0, 0], - ["_Test Account Shipping Charges - TCP1", 100.0, 0], + ["_Test Account Shipping Charges - TCP1", 100.0, 0.0], + ["_Test Account VAT - TCP1", 120.0, 0] + ]) + + 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 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): diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py index fe5d3ed6df0..988cf52ed05 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py @@ -54,9 +54,10 @@ class TestLandedCostVoucher(unittest.TestCase): expected_values = { stock_in_hand_account: [800.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "Expenses Included In Valuation - TCP1": [0.0, 300.0] + "Expenses Included In Valuation - TCP1": [0.0, 50.0], + "_Test Account Customs Duty - TCP1": [0.0, 150], + "_Test Account Shipping Charges - TCP1": [0.0, 100.00] } - else: expected_values = { stock_in_hand_account: [400.0, 0.0], diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 56fd47e1bc1..0cb21d73f90 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -288,8 +288,8 @@ class PurchaseReceipt(BuyingController): if tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount): if not tax.cost_center: frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) - valuation_tax.setdefault(tax.cost_center, 0) - valuation_tax[tax.cost_center] += \ + valuation_tax.setdefault(tax.name, 0) + valuation_tax[tax.name] += \ (tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount) if negative_expense_to_be_booked and valuation_tax: @@ -297,37 +297,42 @@ class PurchaseReceipt(BuyingController): # If expenses_included_in_valuation account has been credited in against PI # and charges added via Landed Cost Voucher, # post valuation related charges on "Stock Received But Not Billed" + # introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account negative_expense_booked_in_pi = frappe.db.sql("""select name from `tabPurchase Invoice Item` pi where docstatus = 1 and purchase_receipt=%s and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=pi.parent and account=%s)""", (self.name, expenses_included_in_valuation)) - if negative_expense_booked_in_pi: - expenses_included_in_valuation = stock_rbnb - against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) total_valuation_amount = sum(valuation_tax.values()) amount_including_divisional_loss = negative_expense_to_be_booked i = 1 - for cost_center, amount in iteritems(valuation_tax): - if i == len(valuation_tax): - applicable_amount = amount_including_divisional_loss - else: - applicable_amount = negative_expense_to_be_booked * (amount / total_valuation_amount) - amount_including_divisional_loss -= applicable_amount + for tax in self.get("taxes"): + if valuation_tax.get(tax.name): - gl_entries.append( - self.get_gl_dict({ - "account": expenses_included_in_valuation, - "cost_center": cost_center, - "credit": applicable_amount, - "remarks": self.remarks or _("Accounting Entry for Stock"), - "against": against_account - }) - ) + if negative_expense_booked_in_pi: + account = stock_rbnb + else: + account = tax.account_head - i += 1 + if i == len(valuation_tax): + applicable_amount = amount_including_divisional_loss + else: + applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) + amount_including_divisional_loss -= applicable_amount + + gl_entries.append( + self.get_gl_dict({ + "account": account, + "cost_center": tax.cost_center, + "credit": applicable_amount, + "remarks": self.remarks or _("Accounting Entry for Stock"), + "against": against_account + }, item=tax) + ) + + i += 1 if warehouse_with_no_account: frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 2afb69353f4..c80b9bd04bb 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -66,14 +66,15 @@ class TestPurchaseReceipt(unittest.TestCase): expected_values = { stock_in_hand_account: [750.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "Expenses Included In Valuation - TCP1": [0.0, 250.0] + "_Test Account Shipping Charges - TCP1": [0.0, 100.0], + "_Test Account Customs Duty - TCP1": [0.0, 150.0] } else: expected_values = { stock_in_hand_account: [375.0, 0.0], fixed_asset_account: [375.0, 0.0], "Stock Received But Not Billed - TCP1": [0.0, 500.0], - "Expenses Included In Valuation - TCP1": [0.0, 250.0] + "_Test Account Shipping Charges - TCP1": [0.0, 250.0] } for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.debit) From 466702200f22ec43a40017b18439419966a471f6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 18 Nov 2019 14:55:18 +0530 Subject: [PATCH 199/679] 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 200/679] 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 c0d8233a8b37a8b78306f531a94d5bcc21f5eedb Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 18 Nov 2019 15:45:53 +0530 Subject: [PATCH 201/679] fix: Raw material qty depending on the quantity of the parent BOM --- erpnext/manufacturing/report/bom_explorer/bom_explorer.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py index 875d1152dea..48907adc5f3 100644 --- a/erpnext/manufacturing/report/bom_explorer/bom_explorer.py +++ b/erpnext/manufacturing/report/bom_explorer/bom_explorer.py @@ -14,7 +14,7 @@ def execute(filters=None): def get_data(filters, data): get_exploded_items(filters.bom, data) -def get_exploded_items(bom, data, indent=0): +def get_exploded_items(bom, data, indent=0, qty=1): exploded_items = frappe.get_all("BOM Item", filters={"parent": bom}, fields= ['qty','bom_no','qty','scrap','item_code','item_name','description','uom']) @@ -26,13 +26,13 @@ def get_exploded_items(bom, data, indent=0): 'item_name': item.item_name, 'indent': indent, 'bom': item.bom_no, - 'qty': item.qty, + 'qty': item.qty * qty, 'uom': item.uom, 'description': item.description, 'scrap': item.scrap }) if item.bom_no: - get_exploded_items(item.bom_no, data, indent=indent+1) + get_exploded_items(item.bom_no, data, indent=indent+1, qty=item.qty) def get_columns(): return [ From 6ef057a2a3225e405435f67dd9c77b11c43eaed0 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Nov 2019 15:55:32 +0530 Subject: [PATCH 202/679] 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 203/679] 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 204/679] 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 205/679] 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 206/679] 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 207/679] 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 208/679] 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 209/679] 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 a99897841536fb34508dc549a4a917ab22ee6b6a Mon Sep 17 00:00:00 2001 From: RJPvT <48353029+RJPvT@users.noreply.github.com> Date: Tue, 19 Nov 2019 10:24:38 +0100 Subject: [PATCH 210/679] fix: pending on review date (#19609) * fix: On Specific case if no item code in name * fix: pending on review date --- erpnext/projects/doctype/task/task.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 54fce8d6db5..7083d694f80 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _, throw -from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate +from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate, today from frappe.utils.nestedset import NestedSet from frappe.desk.form.assign_to import close_all_assignments, clear from frappe.utils import date_diff @@ -212,8 +212,11 @@ def set_multiple_status(names, status): task.save() def set_tasks_as_overdue(): - tasks = frappe.get_all("Task", filters={'status':['not in',['Cancelled', 'Completed']]}) + tasks = frappe.get_all("Task", filters={'status':['not in',['Cancelled', 'Closed']]}) for task in tasks: + if frappe.db.get_value("Task", task.name, "status") in 'Pending Review': + if getdate(frappe.db.get_value("Task", task.name, "review_date")) < getdate(today()): + continue frappe.get_doc("Task", task.name).update_status() @frappe.whitelist() From 3f854fce2eb6e9c15d6e5f84ad2d5d14b8e7882c Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Tue, 19 Nov 2019 15:07:30 +0530 Subject: [PATCH 211/679] feat: fetch leave approver from both employee and department approvers (#19613) * fix: fetch leave approvers from both department and employee master * fix: creaate a set of approvers --- .../doctype/department_approver/department_approver.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index 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 e13b7698139c1d6a41b22e158803216357350d50 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 19 Nov 2019 12:04:30 +0000 Subject: [PATCH 212/679] 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 213/679] 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 214/679] 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 9db9edca2c785e3a4ed784e1a3f3c38e102b662b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 19 Nov 2019 18:44:32 +0530 Subject: [PATCH 215/679] fix(expense claim): fetch outstanding documents based on party account type --- .../purchase_invoice/purchase_invoice.py | 16 ++++++------ erpnext/accounts/utils.py | 12 ++++++--- .../hr/doctype/expense_claim/expense_claim.py | 26 ------------------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5c53d26ad12..ba7ad37c8dd 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -357,7 +357,7 @@ class PurchaseInvoice(BuyingController): return if not gl_entries: gl_entries = self.get_gl_entries() - + if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -504,7 +504,7 @@ class PurchaseInvoice(BuyingController): asset_category)): expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) - + if not item.is_fixed_asset: amount = flt(item.base_net_amount, item.precision("base_net_amount")) else: @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "project": item.project }, account_currency, item=item)) - + # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") @@ -539,9 +539,9 @@ class PurchaseInvoice(BuyingController): "debit": flt(item.landed_cost_voucher_amount), "project": item.project }, item=item)) - + # update gross amount of asset bought through this document - assets = frappe.db.get_all('Asset', + assets = frappe.db.get_all('Asset', filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } ) for asset in assets: @@ -633,7 +633,7 @@ class PurchaseInvoice(BuyingController): if asset_eiiav_currency == self.company_currency else item.item_tax_amount / self.conversion_rate) }, item=item)) - + # When update stock is checked # Assets are bought through this document then it will be linked to this document if self.update_stock: @@ -655,9 +655,9 @@ class PurchaseInvoice(BuyingController): "debit": flt(item.landed_cost_voucher_amount), "project": item.project }, item=item)) - + # update gross amount of assets bought through this document - assets = frappe.db.get_all('Asset', + assets = frappe.db.get_all('Asset', filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } ) for asset in assets: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 382a89b310d..94697be02f6 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -630,7 +630,7 @@ def get_held_invoices(party_type, party): 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()', as_dict=1 ) - held_invoices = [d['name'] for d in held_invoices] + held_invoices = set([d['name'] for d in held_invoices]) return held_invoices @@ -639,14 +639,19 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters outstanding_invoices = [] precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 - if erpnext.get_party_account_type(party_type) == 'Receivable': + if account: + root_type = frappe.get_cached_value("Account", account, "root_type") + party_account_type = "Receivable" if root_type == "Asset" else "Payable" + else: + party_account_type = erpnext.get_party_account_type(party_type) + + if party_account_type == 'Receivable': dr_or_cr = "debit_in_account_currency - credit_in_account_currency" payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency" else: dr_or_cr = "credit_in_account_currency - debit_in_account_currency" payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency" - invoice = 'Sales Invoice' if erpnext.get_party_account_type(party_type) == 'Receivable' else 'Purchase Invoice' held_invoices = get_held_invoices(party_type, party) invoice_list = frappe.db.sql(""" @@ -665,7 +670,6 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters group by voucher_type, voucher_no order by posting_date, name""".format( dr_or_cr=dr_or_cr, - invoice = invoice, condition=condition or "" ), { "party_type": party_type, diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index f0036277c88..59391505fa0 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -140,32 +140,6 @@ class ExpenseClaim(AccountsController): "against": ",".join([d.default_account for d in self.expenses]), "party_type": "Employee", "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name - }) - ) - - gl_entry.append( - self.get_gl_dict({ - "account": data.advance_account, - "debit": data.allocated_amount, - "debit_in_account_currency": data.allocated_amount, - "against": self.payable_account, - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name - }) - ) - - gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "credit": data.allocated_amount, - "credit_in_account_currency": data.allocated_amount, - "against": data.advance_account, - "party_type": "Employee", - "party": self.employee, "against_voucher_type": "Employee Advance", "against_voucher": data.employee_advance }) From a85ddf2fb472ba82c5ac6c01b24ad6b2c4c25547 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 19 Nov 2019 18:47:48 +0530 Subject: [PATCH 216/679] 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 217/679] 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 218/679] 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 219/679] 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 220/679] 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 221/679] 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 222/679] 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 223/679] 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 e537cda0620f1be820afc9328857160693332d1a Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 20 Nov 2019 12:38:58 +0530 Subject: [PATCH 224/679] fix: Added conditional check for conversion factor --- erpnext/stock/doctype/stock_entry/stock_entry.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index bb85ef0c484..6c82c8b6b2c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -254,11 +254,11 @@ frappe.ui.form.on('Stock Entry', { stock_entry_type: function(frm){ frm.remove_custom_button('Bill of Materials', "Get items from"); - if (frm.doc.docstatus === 0 && ['Material Issue','Material Receipt', - 'Material Transfer','Send to Subcontractor'].includes(frm.doc.purpose)) { - frm.add_custom_button(__('Bill of Materials'), function(){ - frm.events.get_items_from_bom(frm); - }, __("Get items from")); + if (frm.doc.docstatus === 0 && + ['Material Issue', 'Material Receipt', 'Material Transfer', 'Send to Subcontractor'].includes(frm.doc.purpose)) { + frm.add_custom_button(__('Bill of Materials'), function() { + frm.events.get_items_from_bom(frm); + }, __("Get items from")); } }, @@ -449,7 +449,7 @@ frappe.ui.form.on('Stock Entry', { d.t_warehouse = values.target_warehouse; d.uom = item.stock_uom; d.stock_uom = item.stock_uom; - d.conversion_factor = 1; + d.conversion_factor = item.conversion_factor ? item.conversion_factor : 1; d.qty = item.qty; d.expense_account = item.expense_account; d.project = item.project; From ae90ea9547d934ae6ee596c72bacb5cde41731d0 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 15:24:33 +0530 Subject: [PATCH 225/679] 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 226/679] 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 227/679] 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 eed30c6d8c3a4855e6d580b66bcf85a922647a9f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Nov 2019 19:05:23 +0530 Subject: [PATCH 228/679] 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 a8318480744dac43e724a63cc0cd356198e872b1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Nov 2019 19:21:27 +0530 Subject: [PATCH 229/679] 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 c4e6c42950076d9caf1e08a5699face0b6596b5b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 21 Nov 2019 11:20:49 +0530 Subject: [PATCH 230/679] 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 231/679] 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 232/679] 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 3cf2c2b3d517af72253f662f2d27bb2e74b519ab Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 21 Nov 2019 20:47:08 +0530 Subject: [PATCH 233/679] feat: slides for onboarding wizard in ERPNext --- erpnext/public/less/erpnext.less | 2 +- .../{user_progress_utils.py => onboarding_utils.py} | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) rename erpnext/utilities/{user_progress_utils.py => onboarding_utils.py} (94%) diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less index 8ed5f1adb05..abe48685f03 100644 --- a/erpnext/public/less/erpnext.less +++ b/erpnext/public/less/erpnext.less @@ -426,7 +426,7 @@ body[data-route="pos"] { .collapse-btn { cursor: pointer; } - + @media (max-width: @screen-xs) { .page-actions { max-width: 110px; diff --git a/erpnext/utilities/user_progress_utils.py b/erpnext/utilities/onboarding_utils.py similarity index 94% rename from erpnext/utilities/user_progress_utils.py rename to erpnext/utilities/onboarding_utils.py index b7c24a71ba6..35f2b6a7edd 100644 --- a/erpnext/utilities/user_progress_utils.py +++ b/erpnext/utilities/onboarding_utils.py @@ -20,7 +20,7 @@ def create_customers(args_data): args = json.loads(args_data) defaults = frappe.defaults.get_defaults() for i in range(1,4): - customer = args.get("customer_" + str(i)) + customer = args.get("customer_name_" + str(i)) if customer: try: doc = frappe.get_doc({ @@ -58,7 +58,7 @@ def create_suppliers(args_data): args = json.loads(args_data) defaults = frappe.defaults.get_defaults() for i in range(1,4): - supplier = args.get("supplier_" + str(i)) + supplier = args.get("supplier_name_" + str(i)) if supplier: try: doc = frappe.get_doc({ @@ -76,7 +76,7 @@ def create_suppliers(args_data): def create_contact(contact, party_type, party): """Create contact based on given contact name""" - contact = contact .split(" ") + contact = contact.split(" ") contact = frappe.get_doc({ "doctype":"Contact", @@ -232,9 +232,3 @@ def create_users(args_data): emp.insert(ignore_permissions = True) # Ennumerate the setup hooks you're going to need, apart from the slides - -@frappe.whitelist() -def update_default_domain_actions_and_get_state(): - domain = frappe.get_cached_value('Company', erpnext.get_default_company(), 'domain') - update_domain_actions(domain) - return get_domain_actions_state(domain) From 150c44b350f85bf7d566a07b3eb99dda205403ff Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 22 Nov 2019 11:08:35 +0530 Subject: [PATCH 234/679] fix: asset movement ux fixes (#19641) --- erpnext/assets/doctype/asset/asset.js | 117 +++++------------- erpnext/assets/doctype/asset/asset.py | 4 +- erpnext/assets/doctype/asset/asset_list.js | 1 + .../doctype/asset_movement/asset_movement.js | 2 +- .../asset_movement/asset_movement.json | 6 +- 5 files changed, 38 insertions(+), 92 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 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 9415eedc5c4..8b6bc40cf04 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -646,7 +646,7 @@ def make_journal_entry(asset_name): return je @frappe.whitelist() -def make_asset_movement(assets): +def make_asset_movement(assets, purpose=None): import json from six import string_types @@ -657,7 +657,7 @@ def make_asset_movement(assets): frappe.throw(_('Atleast one asset has to be selected.')) asset_movement = frappe.new_doc("Asset Movement") - asset_movement.quantity = len(assets) + asset_movement.purpose = purpose prev_reference_docname = '' for asset in assets: diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 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 046137caa23d2454c63b7aabe114ad14bb9944c2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 22 Nov 2019 11:34:50 +0530 Subject: [PATCH 235/679] 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 7af153da50fbf6ab87d407f4ae9b2077873efc1c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 22 Nov 2019 11:35:14 +0530 Subject: [PATCH 236/679] fix: Multiple fixes related to landed cost accounting (#19656) --- .../purchase_invoice/purchase_invoice.py | 10 ++++++--- .../purchase_invoice/test_purchase_invoice.py | 22 +------------------ .../purchase_invoice_item.json | 4 ++-- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../landed_cost_voucher.json | 6 ++--- 5 files changed, 14 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index ba7ad37c8dd..c0023560ff1 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 237/679] 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 238/679] 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 239/679] 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 fa4299931455bccdf096329eb1f95a8af77e83d3 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Tue, 19 Nov 2019 14:19:52 +0530 Subject: [PATCH 240/679] fix: last purchase rate greater than selling price --- .../buying/doctype/purchase_order/purchase_order.py | 2 +- erpnext/buying/utils.py | 4 ++-- erpnext/stock/doctype/item/item.py | 10 ++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 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 241/679] 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 242/679] 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 243/679] 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 244/679] 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 245/679] 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 246/679] 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 247/679] 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 f37a46edea1ef8ce9e0041241c9dc95b6130e124 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 22 Nov 2019 16:32:50 +0530 Subject: [PATCH 248/679] Fixed Asset Refactor Review fixes (#19665) * fix: fixed asset item creation ux fixes * fix: auto creation of asset ux fixes * fix: [LCV] incorrect condition when checking assets linked with PR * fix: bulk update assets * refac: remove company level cwip enabling * cwip can be enabled only on category level * fix: #19649 --- .../purchase_invoice/purchase_invoice.py | 7 +- erpnext/accounts/general_ledger.py | 6 +- erpnext/assets/doctype/asset/asset.py | 18 +- erpnext/assets/doctype/asset/test_asset.py | 1004 ++++++++--------- .../doctype/asset_category/asset_category.py | 10 - .../asset_value_adjustment.json | 5 +- .../asset_value_adjustment.py | 9 +- erpnext/controllers/buying_controller.py | 13 +- .../set_cwip_and_delete_asset_settings.py | 18 +- erpnext/setup/doctype/company/company.json | 9 +- erpnext/stock/doctype/item/item.js | 9 +- .../landed_cost_voucher.py | 7 +- .../purchase_receipt/purchase_receipt.py | 14 +- 13 files changed, 562 insertions(+), 567 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c0023560ff1..3bb3df8dbd9 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 8b6bc40cf04..546f3740947 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -31,8 +31,7 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.make_asset_movement() - if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.company, - self.asset_category): + if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category): self.make_gl_entries() def before_cancel(self): @@ -99,7 +98,7 @@ class Asset(AccountsController): if not flt(self.gross_purchase_amount): frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) - if is_cwip_accounting_enabled(self.company, self.asset_category): + if is_cwip_accounting_enabled(self.asset_category): if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice): frappe.throw(_("Please create purchase receipt or purchase invoice for the item {0}"). format(self.item_code)) @@ -295,7 +294,9 @@ class Asset(AccountsController): .format(row.idx)) if not row.depreciation_start_date: - frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + if not self.available_for_use_date: + frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + row.depreciation_start_date = self.available_for_use_date if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 @@ -514,7 +515,7 @@ def update_maintenance_status(): asset.set_status('Out of Order') def make_post_gl_entry(): - if not is_cwip_accounting_enabled(self.company, self.asset_category): + if not is_cwip_accounting_enabled(self.asset_category): return assets = frappe.db.sql_list(""" select name from `tabAsset` @@ -683,12 +684,7 @@ def make_asset_movement(assets, purpose=None): if asset_movement.get('assets'): return asset_movement.as_dict() -def is_cwip_accounting_enabled(company, asset_category=None): - enable_cwip_in_company = cint(frappe.db.get_value("Company", company, "enable_cwip_accounting")) - - if enable_cwip_in_company or not asset_category: - return enable_cwip_in_company - +def is_cwip_accounting_enabled(asset_category): return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) def get_pro_rata_amt(row, depreciation_amount, from_date, to_date): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 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 249/679] 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 737d8a1e260027a16cc1e6bb5d42717372dbec1d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 25 Nov 2019 09:42:08 +0530 Subject: [PATCH 250/679] fix: Added comments --- erpnext/stock/doctype/stock_entry/stock_entry.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6c82c8b6b2c..96e74ccca1a 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -416,9 +416,12 @@ frappe.ui.form.on('Stock Entry', { "label":__("Fetch exploded BOM (including sub-assemblies)"), "default":1}, {"fieldname":"fetch", "label":__("Get Items from BOM"), "fieldtype":"Button"} ] + + //Exclude field 'Target Warehouse' in case of Material Issue if (frm.doc.purpose == 'Material Issue'){ fields.splice(2,1); } + //Exclude field 'Source Warehouse' in case of Material Receipt else if(frm.doc.purpose == 'Material Receipt'){ fields.splice(1,1); } From eefc492ff48738515eb9753ec6e0a5cd8970b203 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 10:51:27 +0530 Subject: [PATCH 251/679] 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 00677f334ed22a9dbdb869fa79f153fb0d421f51 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 25 Nov 2019 11:58:14 +0530 Subject: [PATCH 252/679] fix: user progress code cleanup --- erpnext/hooks.py | 2 - .../images/illustrations/collaboration.png | Bin 0 -> 3849 bytes .../public/images/illustrations/customer.png | Bin 0 -> 4093 bytes .../images/illustrations/letterhead.png | Bin 0 -> 1613 bytes .../public/images/illustrations/onboard.png | Bin 0 -> 2742 bytes .../public/images/illustrations/product.png | Bin 0 -> 3136 bytes .../public/images/illustrations/supplier.png | Bin 0 -> 3531 bytes erpnext/public/images/illustrations/user.png | Bin 0 -> 7887 bytes .../setup/doctype/setup_progress/__init__.py | 0 .../doctype/setup_progress/setup_progress.js | 8 - .../setup_progress/setup_progress.json | 123 -------- .../doctype/setup_progress/setup_progress.py | 63 ---- .../setup_progress/test_setup_progress.js | 23 -- .../setup_progress/test_setup_progress.py | 9 - .../doctype/setup_progress_action/__init__.py | 0 .../setup_progress_action.json | 253 --------------- .../setup_progress_action.py | 9 - erpnext/utilities/onboarding_utils.py | 25 +- erpnext/utilities/user_progress.py | 287 ------------------ 19 files changed, 9 insertions(+), 793 deletions(-) create mode 100644 erpnext/public/images/illustrations/collaboration.png create mode 100644 erpnext/public/images/illustrations/customer.png create mode 100644 erpnext/public/images/illustrations/letterhead.png create mode 100644 erpnext/public/images/illustrations/onboard.png create mode 100644 erpnext/public/images/illustrations/product.png create mode 100644 erpnext/public/images/illustrations/supplier.png create mode 100644 erpnext/public/images/illustrations/user.png delete mode 100644 erpnext/setup/doctype/setup_progress/__init__.py delete mode 100644 erpnext/setup/doctype/setup_progress/setup_progress.js delete mode 100644 erpnext/setup/doctype/setup_progress/setup_progress.json delete mode 100644 erpnext/setup/doctype/setup_progress/setup_progress.py delete mode 100644 erpnext/setup/doctype/setup_progress/test_setup_progress.js delete mode 100644 erpnext/setup/doctype/setup_progress/test_setup_progress.py delete mode 100644 erpnext/setup/doctype/setup_progress_action/__init__.py delete mode 100644 erpnext/setup/doctype/setup_progress_action/setup_progress_action.json delete mode 100644 erpnext/setup/doctype/setup_progress_action/setup_progress_action.py delete mode 100644 erpnext/utilities/user_progress.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9e74bfd2906..a88bb44adab 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -40,8 +40,6 @@ after_install = "erpnext.setup.install.after_install" boot_session = "erpnext.startup.boot.boot_session" notification_config = "erpnext.startup.notifications.get_notification_config" get_help_messages = "erpnext.utilities.activation.get_help_messages" -get_user_progress_slides = "erpnext.utilities.user_progress.get_user_progress_slides" -update_and_get_user_progress = "erpnext.utilities.user_progress_utils.update_default_domain_actions_and_get_state" leaderboards = "erpnext.startup.leaderboard.get_leaderboards" diff --git a/erpnext/public/images/illustrations/collaboration.png b/erpnext/public/images/illustrations/collaboration.png new file mode 100644 index 0000000000000000000000000000000000000000..12c67e394ccf22e12ff8e0c149139e82e73b321f GIT binary patch literal 3849 zcmZ`+WmprA*Bvd5G}1Nc7(;M$cTXCnhUA1%O2`mK4CzKtQW{}MBOu`@NkJNkA>FBf ze|{g|5ATP2&vVYb=ehUmi8VIVq9l7j1^@skb+pw@?-2Q4-6Oo~ZZhS{cR=`D#XtoB zs7oZjb|AXrTu$1i1^_@1@NO>>0Qhq!AvXa4e+d9!+a3UbWB~w7-np$N@^=G#M?Ecd z0H7kC7j(x+y|t};0RXDr{|YZ%n2PmIB=OTR&>&eOr>7;P4VgJ2x%=d&j=Bo;<=kH0 zs}On(Mr!|m7K*&MPwp8fk|oMip)AE^d7}a$M_|=<39}BExKV0BaK=}u zm$(xac-#Z>&!w7W7T+79=y@5?0q{81(00D%+jwp_cn0hjQBz<1qlxwT(5W=BXBpu@ zcpRJE+q8qmdOR@jG5aiH5FfraP@ua1yG7^ z?>$-|=pMf*?$M8I+;PE#ThUP{%J`tp>gE0CkC26WG0cudqJ8fOPC^4yau( zF|o`{Cj=>1zin8#e-8R^jz`bG3PC!X84gg;)Y%60FqY6tyK!a(M zt~!A=h$i05i7_w!@8dC&A*}Y1*86Fr3KQyW`mM5CyNpo&!9}^U9xXvL?LZp?-pOV)j2(PRzAZMk_vk81KMzs-Bu2zM< z&wbK@7KO}xXC$bUT?tAMQhjn97qdq`F1$NZ%02Z=%VM%Ur8ymu*8=ykxFVJ>kj1pO z7dL}}?}}N!YUNT0OjI(%g-2aP#%^GY%@+KR%yUFJaTJ8p11a`Xwhg}^z!N?rPOeYtk9)@{Tw_AOcZ??P^FPw9bG4z25mx*G3S7 z`|GXfs-SmVD`VuBUPbyyJ$MVtEI7FA^>LHiO!lC-F-?BA!zAnzZv_Dkd`{k|G{+He z$-Xi|BJ`b~dWSAx*k~mzTQ5b6z)Zs`a|0 zonvR@vFKi=Tki2KirCymlI|RS2MFh)FkVvk(_}b`q5KKyrgGSQ8|^$Njn(*6OcSWf zy(BgrPm>&N9f$LZjmqrXaqsd^W>G$w5?@2kLr^HtG&<`lh>)DX*{2}p%SxlB!?^I2>8omsB|23eu5nPc(9!S$;db0GM;^qhuqqL9T(OhwEr>&x?S&pK+dyl9S_= zEqLI}3wM(4%t3P}bWcHfbUBZ?UU|f+aNil36pxe)BEfd2A^gxM&7WtdZF4lBdl7%7 z;N9tdb;)2j;atw7lm=A2`DFvtu}IFNd<7XZOOKusg8;RM_;4rBO>V`orPM z8jCz)cB&qr-xHSz394Lo@mH>C-jC@5h$oHOkNARpUd0%Qkhh&u(x+C?A@gjBy0*HY zl399=vL9xgN+<>3ab18TEpgaQ%%c_SVQ2|79)lIoY5iIxQT$f;BQvz<_ENPi9w8j9 zHm=^ap5#C1d;XVi4_24}-=UMHi!4y|%NW2cP6Ks;_w+(#Er6c5Mllmfm8@;Gk7bYq zzKw}>2?LtOKY$A}EdxTccd^K$AI3v$rBbM^BdY*L^(Ap%ZnDjaRsn5&pQIX+id(G)My1I z5%y@_C-p^d|l8Fl+%erFZx^_qMO~qPU~!y3oslv>8d*K#^MmKTZmfj(>T){e&9(y? zf^3weiW&aWqD5!iCUJQS)6Y7DDoL;N5K^7{xNXY(YLdTSLGzsWrp+V=r;jjac=|UL%V3p^a(453 zuPx11SuRxY7}aLW%$pj)MRm_-u7y;XkL?3#Ru#5sw@LGVP9)~!F;~k+jI#`blDzYL z{T(uSGVi%5arVXRFk3EI3pP>+Wj9gefNtY!yW)1ceje_ri&Ux8YE@1cK+$})2FiY9O&sN-G`q9zJkrTpg}EeNJi4Mt(rzsj3f)0v!(q+W5vc z73*WOIE!T)FrE>|Qc=~qFjzwMlS+egtE#l^+CS;y z^6d?{fFNSCT7M5W>$~=HUFBXuPX?jfg>-av256MB^xK~zHl_!tkZAJXjS$sIx=${2PW!q10t`}w>YWbjzj#XqlT{Tan zp~?}&B;+*97Il~=Uy4z@R)2cfu_|>5bfz=yN!*owe6`lBH{|+_nQf&M#k-5cB3neH zJ#l{NPaX=1wB6rW*>A24Gm50OxD1Ub#i+e9?zP<*sAqoLqmv;K5dyO{#@04hDA}^q zK~I2EmX0$XN6`esPBr`MJulp>M|UM(c{D9NP3iV#tdnv@Uq2+v=E~Ener2FI%Y=Oq z0X8vAwGnyPF^^z6K=&Qr{(?P>V+Lx4-=t?BwEW_Zqg|m8UBTophP&lzFm8cY1=8tB zTDs(ipQpqudw}v%G{Z2Kz$OXxsIB4qb)^&|qQk&UiX6n7qO=pcu_V#dvqIuB?w?_n z@tNdoq^Tko?k^-mmXdWsxGIWzOdhDZF$5&{Xyo3qjFHxws%JiC{B3Pj3_Ua59Hk3k zGUmd{jr37>{zgPPaR^^qADqv3bDP4>)|Lxp1vfh_IfJ6Ve_?;+S@>4)cqAPo6rdE<|01M5&Rt2R0Cg`BTMg;ney273 zruxG&)EU=T7Q$7KAfBx&7nQ=vdgk;5KR?6jcsQ1bemt|oVNYRh$lV-+{`XD0Ba0QV zWDofb@FuUfq;MY3tMMxk-g9N~V@62B*nW{TVzR&2S;(IgAA(X#0H5uJE(re4iZ^}k z5&T@-l3V}D@3WFTSqv^ZgADgP{nl{|_SleRen#|--Zu7+p4mmLN6;$#OI$UcBCXu0 z%%);aigC=DN^cR1j?)Ls&xmF zh*f8?Op?>8=_V^K*-QIG^wQWhcshDgc^OGrQ@BqB(+4gZh9>xHwM z%d7w2a11p)zB91=cfst1i(jAv0tN^S3>0&Nd-yszc*DeAAfCV5mw#}#1klkiRIgFB GfAc@J)HmJ$ literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/customer.png b/erpnext/public/images/illustrations/customer.png new file mode 100644 index 0000000000000000000000000000000000000000..b2ddbf3bb4709407ef493926e3490336568f6543 GIT binary patch literal 4093 zcmZ`+RaDfE^Zft|EWLCr3kyiPz|!nWcS<85f=DCHlF|(>C0!yNk|HP)(v2wH-6$cb zD=7Z>KK-Bm4|ndlGjq?JGcR){LI1uw1t|+D000!48Y+gj9R44`pxfvyRjqJKAV)=A zMF40@A^T%Pc&p*I8iu+65X5ua7YzVcx2f<00PsZuz|ms>K<5Aeqi4Yf1DV?b{u6C= z6#%G7;u5`8B%T`PJ^(=3{~z!&?@~f^GEqm4@|f@?W<61?UQlPBnIrYBn=z$U_2lQ&8nAGx=2I=# zD%bZ?rB;DRaF%S9R#D?c#8=$`t5A@TXs(4yr;nh1z5I4!cKR>)L-XV_hqViLF~%tk zt|aj(+$JZNz3je^Waltfl&EA=fv}-&qTLgSuu)3ROCqc78DpxusNp?paWkIvOBprf zVxQ(uhGBaY0bZ3o#p~br#k9)IYGlCLF5>5%eDH{}nCWZ(&Qx}DZ}fTmD1Sv@ORD-vRgOVs3U7pfkO4Gr(3mH=Sb;G zQOPr3(GmJY13DwetsN+na(B~ZC%6~=pyXpBd}TkUXr$Z~De?uKh?0^>_n2gPc>jiN z2t9+NQ)D_>I${R*?o+CulapFQ>N$iLr%yjQ)?6slU4SplRa>+c$Kd%aLscD!c|U@$ zYLKAC9SVn{qvAZ1@qL5ot847ZNI@u6&LI6`cB!w+UH5VtMLx34^UpZ<`GD3=d`0QI zh)(D(Sc&`HXRS)M37rA7e6j9#@EJ1l4A$4{eRWPB*(iY6=LFd_IWAvNN00SRB~kpM z59#y%WNcTIU3>V;nCw($c_m_o6D&`r$sPqqoVkVwMSA*b9C?C@V~t#o3mWEZQ-*nI z<2y)ghe28AZ{1FU*zh-|_$>TlzbwhDy_pC}q*q|bz7q#<;o&N6ZwP<_n91_bVY}!C zxv(@=d{$y`gY;guKIP7ooIA*AGEIS*{a#jpNeW^UG|UaHdNo>(<)GesICX?KeCaAb zZTx{62H>X3O}Xh6kl%$BcR)zvOpAF@2|(-PQx#PBVd){r_D#E>v!G6$+LWUb7$S%@ zG1#dNq080c#T()3cK8Vt9-dkpfszRYaA*HgfFCl%-FEYaQUr}yWliXVey8d(1*zb8 zcmd?aQg%=@(I>B3sRE+tSftlJ2y|{nA1l5f%6tb$q-ZJUZ65p_z7xBPP=Ivp^0xj6 z9S;Rf|X*%r0(ctYLHqfLk7`%P#3*w-TOlM)X`Cv>dlanN6NRWFZ7d?(58;& zNDjG~tE{1CTnUG}dKe` zkw`fd$Vr_r5;-C1U;Ad}(z|Lk?Hvre)vNXaHFH#*VfBonuTkP;ziI~p@Z6OUggXRcY0(Ytv#U=T;;#%1HP+Xs63AYF-iB`O;uC_Y1bD$*|o0 z4C84h_M(lhIi`;8;+6XVv!Z0x+ykVs+edh5fOff%?iMf1sod{F&s6J~2=V2qztY=q z9?S;aC(%WpUXk#gWHMBdKZczcms7_&^D(=w^F zEPgrRuciKGp%p*RNuK`Z=E{f{sNHaV6K;L1+EK)eSVz{f@%I~GhFIy=G35?XSOAY**xYgk)WM?H^yZu~Jve};ea5sg zYiaPYcG1uqNxNg2Jf_%%v|kv;|}myt(*HRF|JK|y9RdVNmW5v zSS{tbYlH)S2ENMhtTrE>#Z?-~A4s>SZ$pabMy&WdVF&!LVl8{@gHG*(ae&&Ts=0Ar zb`Htow~*>#ckJBIhjV41j}gNJt#R`l7XO|@au${(j%O$vZp@_{ZcGKA#r4p_$NWcJ zq1Hvvt)~^_DhtnA*CTI8kO`Vzg@$$qG{m`6456(GEh(aLyfq!qaprhrsG%>@``9_J zRS-omtyf`+Inxle3VEOPv*7V>m;ui`Uhqwz0){U>kbu75Onv|qAky<~2-`n`C@ zl1{C%;c+Ki8I%=aQOVaIsUv|pvN)He4N@^zx68xPb_N=F%*KuUaz%svH1HIBcy`p+ zE$tLBqP2s`AJ`ceFWH83@blR(iF=nNFp~_GVzGW$Zo!~6zk^3Sqd)I+T;X<`p+DVu zZJljLbh^_dBBbn1@+_O95iQpJk21^?6Pnm2U6Kxi1?mqZkSZ}Bn+-^Q%V}szU=FYx zh`-z+bvVkPSc=4nv_s52wrBj;VUA|p-Cg@P7i(4j$syQ(H@5^+L z36=T>$xTEr?I;^w7u?4>MrvlP_fVn_X!(zG23oQxnAXm1|Pq6VRYmySp zIkizH(S_ri2pH4>uHaF>Yi@L)Lr*5X&i%Cu6((L+!6ch5Q!;I5R_5(6JF?3`Y!zp}oFa^lZL z*_fqyoyHvbd*`Wp3=)4&Txu>PJapfK+p7_wtrI?Twx706q75jtwaS3Hz*F+#6FBkZ5@+B@!B>wd03w~dj)|SF#ENO@BKnF#5P!u zPm&e5ze%4SfYBWmK@j}zc%?Pt-OheJ&(pEh!;{DJaq+mF#eYYj*dJfd$qxrJLiM&@ z66T3zNDsb_-B1O26`E4b?r5o1?&S17%KGtOjFs5nh)EV-SE>$IwE;WmjiZy|TP>mSwmBGqZ!KBAsFI2SKj8}y=$lgg0 z2RSe?a!w>2&$M4uD8n%a&DIUw>8wjGBbJ@Ad+IK^T}+B*TNZobp#8DrlD4fl}@uvymU5>f!=n{{2_{y!r4p5Xn^RtF^IX+-OB6 z!%9z5qL}Q!dncj2<65VEO0IyLawg z-}n4}Nn`tldIK$ZRA1tQ&}FWvxeh_7^R0e-TC-pGS+~%Mu_lM^^~dSd`p^>t7gXd~ zobe><@P>fkQ5QW`VgJJVgP;tSN;%bPHR6}^EL-FcZKR*9M8H>e4QjY3z%3&kJrhKw zEmhZPdPZ0(%RPH>eI(Y95$7Z#;?!p=p10|s9vYS^7Rw19c)9ETOS7u~@%vV$lZ{K9 z`7d=sTSet~|wLE`B?wdf9Tb zibM_X9)l@qfO9m^QfnuU9QFRPnLAz%2`94J(*5~4bL~>3aAWjUs**3)p_J zDA^YrqcP@_401bUU9YeL7G8Ji4kzH+Wd~5K{lh;zr_t2lUKe)mlr4yoY@h;>?LsNT zp97YnGM3u zo=L+Fhgsl85Ao-kp-%roRsna=fs2+%e;VP8!RusBi9Jca`XW~;XBzNhE4(5CPc}UK z(RoMGpr1l_ZLYV ztYDlWcf2v-9cBpcM7q{;@Ai_a-Ja9Uh<|u_^Wm}X^!$HsKV5E&su{-424j!5^|rqy zKolv05)=^;6csT>N})wiXmQkCBod88>i%Z??|c3~gPVt)vqRwjZ;&ag2){Kj|2M(N r!vW)O<82T4`}+$yyFc}@wehqU^6+-dKapX%Z2~k^@2k`+J&ybzu~uas literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/letterhead.png b/erpnext/public/images/illustrations/letterhead.png new file mode 100644 index 0000000000000000000000000000000000000000..37df6d7f6fce07fd2604c4c8387dd18869b9bc54 GIT binary patch literal 1613 zcmZ`(do>*s zFI0QlN*gjEZq5z>#U~Aj(o7+m;>!V0Y5EyR_Bs_EX;Gf*LUojXtE8bWCtFQ2>IPV% zE zAO>neRn`oW5hBZ7VXw?QZYNlZm{Bz(XXDL+X#NI4iiQD4guQVGUS){JKThOYi~i~u zUKn9?=5jid-;{E~I+I2oX>@1nx3d-XB?-)!;D$=eG#gv?eBS$e$mnw33+4y?n{T_V z@ogT8r~Xvxy{}`BEG@wX3%|C-;@ebW80<+cyOpuljrCbG#yrvWFI#N(e6GP2Of(KD z>^xLx5x>_&B@p`6?S={rG5inv+jpd%`W5J{#fCC3Sv?0&C7zK-G5!&K5#eE7AXZeN z&ufwHzN8M%5x-eY>v^>dQ*VF4zU|~+w0#uz-I%k!Wp(x4S^;Y)awYNFS5C*W^wZNV zQ3w4R%evU&4#)Jzl24bO4KJot)p+#Se_)2L2Kv5sokB-da!!?~v!`?LVxIy;ln-AT zI<`s1c*=VTjC{&9ZMf{VBj9FfOl{ubgf(X>-}&cQ@m1>A&g)s;4@nUCxCb^{zB3;i zcmA;jAzL z2HFtvu#_o8g_ls@yN8}VKn7^1=0^w8vT9#3=4RP;j9b|WN24MiXZ*edQhl53Iz_{^ zx4b8lNO!XvjIhUjG+{QYW%IK5WTmqQ`??EVX7 z!aJTj!xBlCpuZ){W>H;Iw{dwOl%;>J>{fbx#sbRDU3{LL#;bGBbkZTGSk{YZUbC&e z7sxY+ciitmbBfX}21mjp(o2B*QmPrkPlM z6i!d^QncG4(~|5b$FD%Th;UUo(`OJFRXH-2{b0{`S(yGr1RAp4k1Js4*K5&cv&C&CZ>VDdvZ+-k zwN|V&ox2%4w@P6-@ONQ24Nk5Lq?ieki)7@5$1Q= z0C5^F*93GABA5%yRQa);lW6z$D;e@tLFY$(4QBS|$&{`D<0&_E;sxb|?ouu2lB4`% zJy%=kjVHHFK5SDYZNmpZSFNjC{xpBF;^;bTnMHrR_*Wxr=Imz#GvKF-4kAWaDR|}} zdXfB!Wx&K-WdKD-&lE$J0P{UiMA=?QGR^8Y%@@ZM#3qs|anxZGV@FO^pQfPD^F_(| z`xSQm@(3Ib_R}ppH^J}KRrI$-EtL_cqt2U}e4ES7{Bb2oJLy)~{JEl@d|}cx0b6wsGb*4tW{-EP4c22v&$Fc)FJOUEyYd!?)Krs+t8DPX9_=9!lp4MwzWI_3@ zFSLi>Y7zr2+!70mio$-$Hx7sE!j7(P!CV3{94=E5^NSuo`*CD#%jF3089qHWAU@n8SJBA@8AQH^2P0h_si3D%+O(cRPiD0pg zKp+ta8DF;N{!0)U70L|T|Nnx;l2M#gp!L(iD=Li33yxs`kH^C^_wD8E4vuEvqhiAI SCTz8(L*U}*;c#m!dgyN&#IyVW literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/onboard.png b/erpnext/public/images/illustrations/onboard.png new file mode 100644 index 0000000000000000000000000000000000000000..094aa3f8ddfc1a07762df08cb3c134ae3ce19384 GIT binary patch literal 2742 zcmZ`*c{tRK7XBGSBg@RxXI};riD*z+l0kN|4-Hx@V~jnfn9n|uC=8#nM3N;2m7y#{ zc0S8cNS1_~tV5+tY7BS2=ehsf`#krLbKduy^S;k{&VMJ>!OlYXu+(7y0EDe9O`W)y z@J~R%+)U7^GU5UlZ+y`h0O~RY_q_PHwY-m|(?tOIQ<CR(+&}@q`ZWO1 z&jSGQu)-Ec1MUFiZDU~y0MvA(7Pk@zv&2RKfJo0j!E;|tM4H|o9F(*3*m(;rqz=dsjZ>x9-rOB%bZ-KU7*)R<`-wIcL7P1X~ zZj!c|N9T&Re(u+gl17?WV$h>)D|=j`(UT^s=2~;9&b#+NWPjczJ95CLs)RSC^epWD zvqcpF_g`@n!j9`*F%PzzO)xrlN{>x^3!C_u7_xT@ERbs}wXfQaADSjj|H?|w1 zb;wX>?bg3I3e$}uF~#A-Ze0Pv4l?c#wkw!HJlq*~Vm5YKCrRFo9d?wVdZ4GD0$i(U zSdoRY`C;I?;)Q61sg!Dec!zV$5;Q?RkK^i>1Ok>VWlIzxerRLzgEz2NF+=hKtNQ|K z-pN7*vX{XoTzq0>J^b0uov*XDWX7JdU@#t|@a7YFF$8;y^E-kkuS(%-gecMX@Qt8l z`F=u0;#>{xEU#9$Xuah+!4ISBmMFl^hsZtAEw$z2Nb~_3px^arez;D zx){&t2;|qtBya`O1Ac6(FWA!#|EG33KFXqgx8%Xlnyz(ed5YCczX9kkmNq=^oE^%z zx?3JJ&f=+v=xeNf_kkh4Y7(Jqf@8q1|MJo-W`dpEMPEew=wjYF0?Gu4V9$j!*aHsMR!MCV70+M zRJu> zwzDk=r-{4CD!HSSO>}=oo)|C>eEQaBpQNV~TH+V)31RWbQS_NBkW$@z!n}a7p3T%q z%}w0AVB)RV<6zg9P<9B!lNpWlN3)7xQB(GcyUoFo{K)^k$KhAqnwZNUE}+x zy5h%^m7Iz_CSK7JF&m8c4rx(4&Q`0ROjo28o4)8-1`kD;yt!p>z=T#9k8Fk>S++l_ ztF{trVxH9U!OlBg^yC1Kalq=i71)pU_BisH*RI7J%Y&VGp$>*Y?&vyEsxzW+khfC~ zyb;Z7uD5*<3-76E>4w{eDvh?a#KKQ9{jq9f z@R#~~gnK^6etwN!mt4fo2Q?dZPvt*3eFZbN^htS@dfq#>D0#T_O0+|^EX!B%g`CBS zoyyRn!DbZ$wnLWyhGB_lZdau1X+hbQqq4@4?_1KqKc`SUMs3u-`Dn#P{iHIx$R=$v z{(Jz{>DS1(k|*%p#mpmS8PVr=H>)w9Sumfr06XwLni)}qKVOv)iEm9%L; zcO`9pDIjBnuIqyic*56t3focY32D1Vm@~6~|N32*hF_J08DCwylEVI5)}cx%qfO5~ zvmR3>7bjO{FAu)E#4q?!*&bsxu6)SQOnW<&txd6SwC$VRO<)ziB^el zm%nKqg73vA4Lek@!vT-u?Udq6yazl$3M%i(%znX8#6ldQQ1m~4bNzb>p% zwNi;c1J-WJbXWd;=|p8#$C-HW#?qHIe)A`-n64r0(f9mfQzw($G13yo`Zsrjh%BDL zDPQgAdB1!$G40fEl8!CX2%51H8nnu=i$~5-4COTG# ztiiRt68v=YeyJ_FB3Bsc&maM3pIKu$2SqdXQ+MZ7KdTSCv>0AWS#S^|o9{4e9C`C< zq{4F!TS=c3Zg(m7WWc*2PUq0lPxu|GrD!AB1+;UvPE=v!ZbMl#w54R-?%h3xyL;Ua z-snsHAW@|PGfKf&{a9IZ)Z}R*0O$T z7kFFeD}2}HMEzxrgp9l(_qM7RGfockkI68W1s>)ecebGALNbidEQ>i_Acyw$)pa~6 zd(EImQZ7)UGfQUWaY+?<2b&(4eekb{x*F*kmlMjc2HM9(w1w#t?|`Wg60F6P&GJwU zE-rKVj<^b`2~{QfBC1eyXt2ZF>gYV?XnSor)PYcp%2op@XKn>@<*^#UG_SZ|K$_Tf~^d7tBVU><9Wi3{*w%5&m>3|WtjJI)eaPvy(R z_C!k93Dbl6?`-BYE2_NnyRSZ>nd7);>=ZKGNJ@U=oRQU`DH2$mJ$sxT$cENJM06kF z-6j^F(=~E39MDotk_pCI*bs+}-OPaIF$h)mD&26})jS@5!~2Ho_osu6ZMa6S?efCV zOWSScqDr2pI=A|1i~1xP$QOiC!Mm^rbvDZaPfrd_O1rGv0_(ImpYx9@KWiKGiAe2I z(uaRU4K*RII3v2TQL1qzLCXp<-2aNSJ!Q3w>CJcZMtK_u4jGQkQ4*4044qFq-gs1 zoJ)|lvu$7G8pYq_YrqUmHE2@sqYvPwm8F^dVH{p$O){F@81ES~3C3+#gP1j+K;~~! z!bNDw&W_R<|MT*A_x?tRNN4_GmvK3l`=Tik%`Opry@)t{pKu%(04NDV?r dV1$oX7)~QJ9RG03K#D5@tjz38pP5`E{~PA20BryO literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/product.png b/erpnext/public/images/illustrations/product.png new file mode 100644 index 0000000000000000000000000000000000000000..f864b7af60ef44849084225da7f601bcbb33a7b7 GIT binary patch literal 3136 zcmZ`*cRbXO|9=~2+~MpU5h6!-N4V@6cb$2Zy`6K}JEfD6aaMykQb^8@l2JK(%P5qP zkm}>ih=i~H{QdF!6J_Haevmr(jLl;PxA=GoTF};5g?DD!mI=Aa;1E0TF zuy}3I>euY64gA`_d`e_quWwkQSFSdE)A_Wa8StOMt@g})S=&GRwp#>VqTs9{Z}W{B zh$99;2ck&4T7Pyd`cf{GthsTpCV<;g&STm#iVgO{RhT@6YaGI!;kei3?{Tb|$=qr9 zqaA*KRA=&9rM?Qdd=k`ZuK&<(F4bURE^TQ-V{%n9{;!?({ZWKR-lHlZzd`p$$uGHg zvquhY%WI1Gw0Ox$f@KH4ixI7=7GtUSW3o6e?16<`Cg%FSXsWLWF?*;)Vc%&Yugy$q z*nfl$rQ&dOCzUO^0)KbkKh3f$z!}i%kFEq)PTFhO0^sAJ^q5( zrd%tfE#s5U3-ONg+REcYNExM=Qj#p$J5X()zr96D7(BDlczZP zR-XZTYGRcJZa;(Sjx69UIE$;aZjtBHi8~8Q8|fr!m6IbxXFvmA?i> zz5?!$XVHIb;A=85Kt%?LtpgMH($S!63_Yf5*HMQ`V?Si`&iyPoH2lcZRpeL z@)-dnmFyNhC)5JAdrtvrEsUq(uU|gU&*)Z{ zl)F*|#615nOa8(Nd&~!vOm~8PwwtjMEe#K^9j{F;A{p87;4;Gd%XcQVXHPqnt zl4Wnyz=0l~vSQW<`+F0{ua#W++U*nPJHaIPl11b@N?^e*KQNl!hQ7JGC^#U8GzQ7R1{;kow58UyR4JxSzYC9l zb+BINr>H7#)s2XKXBm0oZa5D49@f5X;IvE)Jd={$U1^>0R7SXtIL z*cZYqv8m4~CsdQMFr{kZXvU)X#j>B)Ap`WDy;$)oFyITlF@j$3lO~YIj*egYqROin zbY4Iiq7tpCEk7K`;@#QAld*cR`_L}oS##!w06ul zy#pFr44?C0Ws1AWQS!;4UlUViE_~$B8J44x7n@YJ?VqtNw=5+eqk(4K09`o|#D%hx zFgabnL+ymNGH$Lm^iTL7^ZlH48PQXfKRwCOCA#Cnuoijyr^aFIDA`k>viYhjKfFkY zqsBa>#Z@*opJv%vnF+A-2PF#~&kMM%9?VO}$qqeL-=-^-WE9DLiJ$kR54M)p-IpC2 z#RZFyGJajSQ7uHJi5-IhQ*VGmErnF?Q{`e$P3-ZT;)#9LcC{OqH@f!-jFF@1Xtnx6 z{u;Y`N7s+LH+31U* zD(r_7{}hv4H&o&)=Y5(~>Nh8tlkA)PY#5-%M9+F9;a(>3Ha%CqCI(vk>axH$4S(C3 z4oRpy{NWxmWs~GhJ|kl4!7~4pf}0~crV9j$(V$hZh3kv)J<9HrR1A&+Wg3<~?yM`966$E(~Z zywN)MR}jrG9Ce4{pxSr5JLRi9ljbgHQ50#&5GOX9jx-e2Rkk+yVF})Q5^*TYwCSGw zLG+6j%mu?!i=;@7vAMFMQlep;t&%b+3b4`NEK~Jllw9w&T|uv*H2hwXZ^SB5>*;x_ z9XGTN5`8J_p~2ZYmOj@fY!7U8-pgBJpTJ$B6kcxfD(x|Td!C!^wf#I|GL18-IQ$u| z&-@q9Vxpc?)MxeywI>-)vlosa_s*&TU5B+^ue6{|g%voK07 zb~9u<1v+#5SmH|q0qsg}4qc@ii5){dRJX_`%{;H~-^`(`F4RFV5Ox2JRLmmMhP}my z5Lt<#fA5tT{uU$<@~J_y7a^AqJ1(^|IGnoliO{(cIBN^&VO;LkGhN;9z&Q@1r}5i^ z0#CANe(91iAGctgxg4)D9^~+q){YagjncBJ?&Se*ub6}q^wc4b@+GO`tU~W4F5LZi zb4q$xRse@UO%~@FpQ~if)E~A|_IcAx)N$is@fYc2V(I@~y?E}~beYB8zcf(SvJi0YM7UMnD<4`5bqNJkZ=S_34u^TB5-i^iwKR2DjJG#_(eFJ zD=!BQC`^JzUd!0Xc+1OKmhNJ?+pO_xsgIQ001ot09$qdAe{vOj4$&(8_3=a zh+*35Dgb~@;1;{-$X{w$_y7P+*MA^N7o=gm5y?=Rx~gQ$lprz?5&2%aBmhulYpN(2 z`%mxYzX}3<2le0|KLo$yA#noTf_W)f7+dSdYJl$;D~OO>DuR$t1)|N}$?sRfYSezC z3hmqviits&O>wNzY2i3GL!oF)$sOW%JVNA5wJDPd^ZA5M&_hZoGjh>hWu03n8~NHk*2hd&wlrMy|FU+H2zWpoWv6G z``z+WJ(EF8Wen~0H}Xs3;mxbRiTAKmlni|<20Yz!JtqK_Lt`PQBi31*KTj2>G&1I> z$r_8|kzuaNF}c3SmAUU8)ndSwym~@E`s^v;brCB8?e}A=WU`6eHbHChvg}^_Z_}SD zFX(a@R%((zvT+Y6NkF2&yjoj*@Xs0YtLj))p9tg+Zj0)YBuJt}vx zSH{}0#Vhv>g)!`wMk=k2ew3M6BPdux%)#^_#U|uwz+rPDjNHaJ&NY(gqQ{Pq4}0d* z(6L8r)bBeN&B;3;VA(ycwdl1ebH}HvBSSwNMF9m{%yv95!s@hTJeg_Ar7;vv+OW-v z(~0^*IM*sGokWYrsD%>H;opr^v?)JJJ{&ZH?4Rn)r@LSDN2Z*`HGlBlu(7f~{bi)x zFUa%#R_Ur{%R`ywbfn64R^+RM25k4fI}>Jy*;1(7w$@;cG2A(aM;G~3`&32^OE>>J zc6yk)Vch)~b_KddY92{g#5Ip(9BQX`VBNksn-a*vFd{{Z2fY ze$;gGK{VH=1$<5{bGXX+q(%lAQA4(cZ28T>;TF`A=)IwSDXLTj@;^xS4GNOEVe7i7 zFh5kD+Zt@Y(?8$tO|Mhx&dZ)xp^H0SCZFb4LAk1I>bn z!w~#<_t%hLPiIoY*3Rtp?*k!x;iG!Qn%3-JYmI;}p2~ zWg;EBMH(ieD4j{75;_){BY!q~mHqaeD@g8wULh(%`b@1wL|||{c*a|6CMAS2=R#^) zvMpC2Yh`%6Ilpd!4sB>FRMwxAlTp?SX-$il`p3RbFT-1{XekM!B4L(WlN-?Hwmp^A zt`4`^u?r%e6rC>HbEBtpWUBH|AO;Cf@y`g_i@_-~9^03ScpQ6o+B7zTfxltJ>4;dj z{OwM>zKK}_^DO6gwvj=euW!zObn&gQ{|ntB@;ElVq<7x5wvuU{W?bZULDujTB~3m2 z>sxZ=t#KlSZtHxR$&l4leqTA7ts(agKCv>WEoC&ac5;-8?;BdrbgBm}|LFrb-R)|? zQmfpKtXsB^hv+;os+MrM+cJ>ce1uP_g|Q8Myhu05mWg7n%<+6t7?PhGP!)*lI9S@K z%5nH<|73#-LdmHcrAE`L`MAgw98fcIw_~c)4|`5^m%Ul>>?f%Q4X%DOvLLt%4o>`Y zRp6;5H4%>>{kt#k!X-8I=I%(WuMh)Wsx6z@ph>&Nq9D*_&;Pe#L=Sn7nFsuqR27^2 zr08vFs_%XJ*ZQFdQsb}qDZiH91*IW0luigv)ZI~2=y5j>xy%tG*Y;eB8#78BKEEU- zEX!*xU9<=q$$++$YauFb-kFWvJE( zj%kx@PdjRg(dFI%ww%J6sIVL#Tli?fRiGR$jm9z~TwIdc107QWyb%Z25vdM}!u5*L zB3|9EX}yi>`C|lt2l*gt7P$5o^8ss#a)w^Pc+c;Z%M6^gO}QrnNW>LSF|j4z56ft; zs={JKVN3fgjrmut=N}z?3NPkTd5+5r>+ND5+1;iML^x6GGWF5huN>P;(^fDH2w&7z zX=nGSwmiPdRm?zi%yvFE`Yk1ii}49vlkM0g@|jPGwBGzhI`JtM$3|&Mr2jQINag-Z zK|I_DL(w#7v~gwmFQliceSh;nj3L(dJ&keDiZ^Rxjlbl%0&8AB|FaN+>Y zTYBxSrpqi=!6G%|p6^ZcRIFj-70fKl^!0jHkF! ze7s=-oFN)z8Pza(B+v+G4m_A4vn5=1m)fNf=#vvhWG5<;(9H>LLJG-VRX*#0Ic%7>f z_31Z%wfiFvZzXY?Cc@v(mgHEVuiYA2f01$?w4243(R~C|d-rK)gHbQqk<@qnq1A&R7OX2F>_;_>2SXpJdrci#{#plVJ19*R(i|HHyRS zM~4%qMi;82tO}_YKuh;n)6a%krOiH=riEN(5PY{f>(LO2^WWF$9w_Y(0V(DmPJ@B4 z!&3-6=g8`(rNYX)d9h2b+K}hXQKdtv+Sr|YQxI&dSj36`dMbNz2ZC0Dp!Q0pXeV^R z$dP6=u1_bW4U88XBWykw%)EGCLpe`2cS{S9iYO`v6~&x}`Y$@rYWSJkeBiw0>m%vo zueDmOU>c1lYm(8TLX-V+8rvt@3%BII~g_tV%Rb@;ZUx(?gn8bzRPT^+^@Z>#wAZoeCWlF1|B3L;W7$BAu$CSj?{hcbLWWb5)hJ*v+0ugPOxL44gGR@$FNe zZR=!;%|8=bdF4&IEr!G8vjfc^o+T3ONJC@peNfUan0AXkvIV!X&CJ&pWJLU;5Dev( zuE%rtpe29s`b07wl}E{WAgkEmYReB}itB(a=p5HU`eR!@@h%K=$Li9kIXwG2-q5XA zzSmL7He|?eGgE57&?fC4nOoLF;@ID5o?ZLKql|45ei^PaX}N)9-_A|bXp^Kl&oQ>G z-ThE<-90Bm49`G`@W>Z5C&kKyr(H0ekr z!w#I4^PsSDkU^OWMsm3U>^R8n8|k1N{D1wl BlxP3| literal 0 HcmV?d00001 diff --git a/erpnext/public/images/illustrations/user.png b/erpnext/public/images/illustrations/user.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd7db210d481d3ad5d78277776102d327fe21a2 GIT binary patch literal 7887 zcmZ`;Rag{Iw;ezvhLD!dp+OkByN2$N5Qdfx=@1#Z96F>y8fh4jW?%pT3CRKJE=dvb z`rrF<@59~ScfPgvK5KpZ>70kP^9B4PSWci;Y)S7*7<`gBLYdhiR zM)Ve~a=cn5-7wah{1Y81?v`{`GdkHrZoG@u8eB0!pu+_>)I>)}kYiIea>|I?KwY zX#yJQ;jd&glVIL1)!RMs!wBsfTg8;Cwsh9{yG)sD(-!Vm%ea{gwR*VWS;wI{^iYfo z$-8X%c$!M;!1rXfKWF$f3->C}i`gRYmcQhAVZ(|$?m+_5Q z(0z{gT`c~rz^qd>!aZwq1wPH}-ssp44+qOQNw^^L$v2}rv?YB=!uTc=ri(!APCeD1 zHJER@RF`4DJ7mPU^F-BYvmJTZ#ZiPs^RFCi%=4wAqq?mT`BhmpqAULI^)5-f8Y*_q;mJJF zLnO$HUb}JTwMF4_v*%<4@h@rc#g28Q#~&t`E|>`Y#0){kMjN`4_ z2>+UHm@s2%gjyZ=@?=wqe=AAifKBxU6M^FFalwoW?%bI}Q}nL@PNw$}lDAD+V$Nss zNSzX7h;k#R?MEP&x8{SmJe7tOeuN#@tozh%J*8{2^brVRcA^MbC6y!kG*T|B$SCW? zwE!nIL~@rP(aK&b!ONYHI84crq$!&bwBUMQa)~82!}`(@+%v7yx}&6qKiv)?pRpb| z<`cZ9y^*9)=!eiw6VvI)t>JGA8vYKRWI@ghC?kyrzP)ynmYk?UStzXSu#Xgu4R(*A zg=gH6L%Yg_91AgI|C;I9* zwqnFnAn3J#Pa%qS6syu{m0D>Qv#`p-H!gfOnbEH2$!W8I3;t!r9^JP8tc^Fd_Fdx! ztf4#9wuyxamMeITCP$L3l#v+P(Fj2@)Oag|MZZKGJ4vJ2KA68_=(d$4YLY>^ce6*d zy-&3_ct|E4dKr`UHmSMokw4Y3vIWPXU{EXF^G&4XZc~B(HU{;-C;EZVmXU(1JT`fM zz{0X$Ps7qq#pn5eS!vI*fGx_twSGN3gHv}UO{~u$SRBe#T67}H=3C*C5dRbW&Jm$a zpL*3IcxPyRvt)Vt4gI~U)F1V+BeC7Ackp-vjNVOoVL<Dr2_|0M7O*K@>9?&wPcn=rAVwU$DKl?)rI`aK{ zaz}$@xr!qS)>Am5$q6@6jfaoSBfd6f<0`SS-tIU6vAVRX_`I(_R}b1O9v5GpHQz;f zjdf8f4eP5S^^HvW$}iqmn?r(Nem|r(UU+`)vF3Xm^#jHxl2+unSgL3b^VNS9Aj+}6 zL?8YVolb*QvZG|lnO9Bk#uaof1JO?j?8$)ysaaMNdk*|N&g;t|F~i5Z9Sl@W984VV z-Ca|}h=_lNgC0(>V3OstNwdyshS;?@$7y5gv!b4xSMq*wWXQCM!aH5>|=T+Gra5 zOA_)EVIsofW)L_(gc))`)(Te8eR{noAKuB;WdFNxzr5_Tb_YGEl;0t znz$(T6uZ|`l>I#F7?gCt!Ld5Vc`$a2Wm$41!!f!4RW_aD@2`O-YDMAcU!o0OG*dsn zF!yiSH3T&2h-cE8x!nm{4XkIt0-nRM=K<4nBu7zZ z*3M`{eF>Fsshfr3v~h3lEEI#rlx9lwQ{%hMjM{$}C|bv!=TrEC)#}hAEF2;j#yitF zw;ddedHIX@SEU+AC@Ge%!i&#=uNSU4S;7Yeqspnj(vLmw_hJr9G6et1NP*v5`I)4x zb9y-Uy)e)%`pAS85i^p!(VdZ7=m;EFhqfEtEY}E0=D3dj(YNl=6Tnax%h+S&;L}Z z6x|y0f_U~3Mgg}$JNPawCfukFskv57lb@V_oK-oH8kB*OT(G1vvakCiP|BA32;=Fz zr*&!S{1v^Y{8t00nf{dI6h*{@GZb9j9XXkqGQLejsd4idXAChPJ&9Qfp-TVJ#sZQ` zE*cGvKVElcu#By)$YLvC9phK&dgG)awJDnZ(NXDfl(y10Epyxi>Dz`njArx5mR$1# zxx4bQx&7w|06e#LS<0}O&lzyZ-A$DNLh5hRB*L68m$$JQ)@G@T26 zlj8eT3_vBmDc5zkd^;xh8Q=xJCjCzk1Knm){{X#keC&L@=^kKD!^cGi#a=XF5N0;j zqKKCFr;eMK=+i546eAN0T{Cn;NQCGU+F1Cvez3ktAW8%n#EUP|SYS5Lk;5=vHt+B{3 zyIk;|%C8fiQ2OaQ)b^i&#@>uC2nwxqLTK@{QMtg zKhPK+q^{Ovy!OUV9{VVfvjnpX6H&QeD za-E1HG8B|r9g}C|`I{uz7jQANfz{_pF88EcTt4O#T?iIXB&R(1f?A4D1pQ^3_Yje}yXo`6y4I$2&LY#XKyv$x?4eWl)elj;W*_a+|2KDiwLRig$?d$*M}9ly>X6 zcmBRRe|cyB-Ltsq!FtYHA|}V}i)O)hWbODO9XNhFW~OM;>CEAd$i;)EjF!ny)}T%b zK=n4T)*%L`fQ5wx6`99_1`Ze1(h9}#`tppO%(gV89eaDoC&#i>0)b78C30lujH6D@ z(Uq2tqd=kGdXLYhu*z* zYW5RSS8rhHBzGnc!?Kj%DdBack|=_A|z%;h=tI|5Pj8jA{! z=)~GZ%mF@g=|LZ=mx_U06%Ru~kQ<_;uPhsfWa_)OiYI)rDU58tG=xRTPCb}L#jo{7 zW8}4%odgqu;%J0R^_jNb>bJMZ^tDadO{`}!1)Uc>>0`1^H&bxOl#&&^Td3(v(x(qn z3ntEEI`)#p_|>=`tKhn29oFK%QU&pCXM)DYfS^>v)ah7Sx+&bw#3M$1s;Y&w;=l|h*JD5BGx*pmwb(*m1yFHDqjZPGuc29MUTgArQ4|jHZ zp8tkEonaI28#d-acokieaSh*dw(@4+lkb1hCen{VkPK~_Hz$M=0}gKNkc_Y-FR`61 z*+`aTqT`(r0j@enxQi}*c(Hs33-kXDyvQ0Dd9`pczst7XXoJ_Fc#-;s^C4VA7Is{z z4pa9eN>fxr_=@tm7%fBXX0VQ--JUXYt{{Hh1FRz~+J25IEBZ4Z z8TpBsHHC&_0t?TJ_9M(2B!;WdyUEH3J_o!+b8RAQ`{_s%zYO~s*@4>Cl9d^6jpzvT z(ql}z)J#U`ab3pB2aZT#Z#QVEkszR*mZ50@Kqg1b zfL{L4@|Vz;vz&o`{0x86$EmQW+d!7asMf%AV)3^=AF#@{2udZh9INS1jcR@GK~c-K zU3!&8pc+arakbElH$YeMVl)ifMQW9(H-JPrXoW^nsS}3|ml3lZ7*ezyNQa_|jp4Ok zu1D?+f7wR{8t`VLV)DonF?J6Jx(3dCS-J|@QYc(`-%zyr0Ls=gf*)%N*ycFb$Cx&r zMSMuNRIeT-x9xAx+`co)vYu#QU?^`I1c zPE=({eE9b4qN>0<+a0W9u+-ct2E^=j)xo}blh#HV$4doYvi$j&za7mKSoX=@UI$c{ zmL4XoZ;_riS%p*#*QBLprokR=rO;NN=J~#QKAlon336b}gh;@&s%yE-b@P`^P)5$( zP*bK9ZVRM0csZpz_t|aU*}}~(p0YOc{tZA|dQ_7n=De)cmxFu2LLp;Tye%Ovv9Mbw z#E5X{gM!T^(}{_~^3>^UWL&OXL&EQmhb8gx{Z9ddaSNW+6WojpK!)uRV(~#!3inuNfn4eis9Y%t{f^G|EjvLKaw3DSE?P?{f_H+1=FbT=M5Ab@lEX`)rQ8wRCOg6BCT{#0LJ@2ovZgqT8OBh1}3Gjqi6R*OlxWNGXSbv8wUbTQ$DWc2Qz(yvq3p^^G) z@Gs9S)XGl8mL5?-OLko^9J#*u-KI5OwRHc=?pyL(|w9)wLW9ow7Wgh0`eS z_p|9T+lBoojla{SDV$1Y@$uZu|R zJbtUQ)^69Q=v)D<(hO71;2h^B+C%fmY-`M>c4_fL32N;m2V^NPrDe2ZlfcnNDE(5p zZ925f4$!JXG|x?PGaK@v&`D55P%0c$)-~rhTJ=;{W9l<-42fazD-%k{k@?d}V=ahP z#6{z^g*WfP9P5YRW9B01pG!mdCZ8syrRJI8LGdfFW$LSM%f^oan()U1iqqX!NtbYm zc+u9~Atlj#qf#}V6mFu!l2#5S7=LapK_mkRSBfk&&X86@`#lnK>^QpFpj1T z?f`P5OO5*pjhFNJKP$9AQecjzrO}{)b zuEnZzu$xi?w7K4M=MJUJd)gOX^pp<)v`A9Sx&Qi&6!qd!u~5imz!Hays|cqu0?Giz zlgb%4p6Tt~za!ei;TrHMG{=3d>*v1f{GigyBe5&7Vr65(PauDmXF1Rc(OsZoQww`F ztu=hgha~JPo?0Rrno(U>+CxW0s*UJu{7+Myt^hJhr{`2$XGwV zJ+QT@miH^qKPzl8O=BGNtgRoVWcDecUA51DP8k%kNtuyqY3_qf)sog|He~1U zcJEp|J*7a!$u?a~B3z=Fm<^5d*xpNc#D{=m zdhQy@*F}(2b`hKNZ$zKrDpR-45-8I!lyP&dFCsAWgF^l=P}U*+XS{oN-AimT$X(>6 z%OqL+{Ga$YIFU}&+ri`b5#pnml>+*AeUN9QUom{{qz3(YyyHlB!I=Sc6P8Hk_+7xc z?kV~AxrvSSs}j>!F7S46erm2xiU%~1hMi1ZX1^ zD`D)?F*xK4xTCtIL5L6F-jnU9g>O?73(_CR<1B|PN7ffy*V*GGnhiOkwM}(dB;`p4 zsi9RzYEHZKkL#+UVC=~IR&k@e>9{SoFf`RC7@^xMtk>qlV>KUOJS6l0%WQ7HoYq-SD^9R~60rILLL3?zrmqcim;oA0ZL0E`#OdJD_20#}_)NdP>g&jZa3 zVu(Kmv=Hd`N{yKyqQ#lt(*vG1U7zO3-RcS^T;ISML0Ba?!EHDjVO@zM8>Zn>>l+6MjJ{sNkXlhCW&ui z@}A(=x4->1b`rI`F%{|kDO3~wYJaleqe4@^VoGfdMW}774|{;~bZLHCgu4)&+)fJf z&0AbC&6zQqx_D3Z+p;-6sHKhH|Wyi~tF zx4}@EgR(xi?%SB0w}Og8lI{wwx#dJx!KgPzvMJT4c*2PvO5xwoLKrhqi3i$H9pT@6 z+?8>BZo8VY_JWzB6@MZcr}(R6^$5fM?QTJSDrSE6wtfzhP#=eX000XJO7IB^@__}6 z1jQsp1SG*=9svPK0f8II*QftS;O=Sf;u!S*3u2vS|81iI)c=`a=;`PeVC&-m2nYz^ gcky`b3$^uf;P>=#%0HE+`9}h1sOTs+D87pNFUs@tK>z>% literal 0 HcmV?d00001 diff --git a/erpnext/setup/doctype/setup_progress/__init__.py b/erpnext/setup/doctype/setup_progress/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/setup/doctype/setup_progress/setup_progress.js b/erpnext/setup/doctype/setup_progress/setup_progress.js deleted file mode 100644 index 5c78bd50758..00000000000 --- a/erpnext/setup/doctype/setup_progress/setup_progress.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Setup Progress', { - refresh: function() { - - } -}); diff --git a/erpnext/setup/doctype/setup_progress/setup_progress.json b/erpnext/setup/doctype/setup_progress/setup_progress.json deleted file mode 100644 index 09072d4665b..00000000000 --- a/erpnext/setup/doctype/setup_progress/setup_progress.json +++ /dev/null @@ -1,123 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-08-27 21:01:42.032109", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actions_sb", - "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": "Actions", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "actions", - "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": "Actions", - "length": 0, - "no_copy": 0, - "options": "Setup Progress Action", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "modified": "2017-09-21 11:52:56.106659", - "modified_by": "Administrator", - "module": "Setup", - "name": "Setup Progress", - "name_case": "", - "owner": "Administrator", - "permissions": [ - { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 0, - "role": "All", - "set_user_permissions": 0, - "share": 1, - "submit": 0, - "write": 1 - } - ], - "quick_entry": 1, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/setup/doctype/setup_progress/setup_progress.py b/erpnext/setup/doctype/setup_progress/setup_progress.py deleted file mode 100644 index e1402f5844b..00000000000 --- a/erpnext/setup/doctype/setup_progress/setup_progress.py +++ /dev/null @@ -1,63 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe, json -from frappe.model.document import Document - -class SetupProgress(Document): - pass - -def get_setup_progress(): - if not getattr(frappe.local, "setup_progress", None): - frappe.local.setup_progress = frappe.get_doc("Setup Progress", "Setup Progress") - - return frappe.local.setup_progress - -def get_action_completed_state(action_name): - for d in get_setup_progress().actions: - if d.action_name == action_name: - return d.is_completed - -def update_action_completed_state(action_name): - action_table_doc = [d for d in get_setup_progress().actions - if d.action_name == action_name][0] - update_action(action_table_doc) - -def update_action(doc): - doctype = doc.action_doctype - docname = doc.action_document - field = doc.action_field - - if not doc.is_completed: - if doc.min_doc_count: - if frappe.db.count(doctype) >= doc.min_doc_count: - doc.is_completed = 1 - doc.save() - if docname and field: - d = frappe.get_doc(doctype, docname) - if d.get(field): - doc.is_completed = 1 - doc.save() - -def update_domain_actions(domain): - for d in get_setup_progress().actions: - domains = json.loads(d.domains) - if domains == [] or domain in domains: - update_action(d) - -def get_domain_actions_state(domain): - state = {} - for d in get_setup_progress().actions: - domains = json.loads(d.domains) - if domains == [] or domain in domains: - state[d.action_name] = d.is_completed - return state - -@frappe.whitelist() -def set_action_completed_state(action_name): - action_table_doc = [d for d in get_setup_progress().actions - if d.action_name == action_name][0] - action_table_doc.is_completed = 1 - action_table_doc.save() diff --git a/erpnext/setup/doctype/setup_progress/test_setup_progress.js b/erpnext/setup/doctype/setup_progress/test_setup_progress.js deleted file mode 100644 index 9e84e0cb154..00000000000 --- a/erpnext/setup/doctype/setup_progress/test_setup_progress.js +++ /dev/null @@ -1,23 +0,0 @@ -/* eslint-disable */ -// rename this file from _test_[name] to test_[name] to activate -// and remove above this line - -QUnit.test("test: Setup Progress", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Setup Progress - () => frappe.tests.make('Setup Progress', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/setup/doctype/setup_progress/test_setup_progress.py b/erpnext/setup/doctype/setup_progress/test_setup_progress.py deleted file mode 100644 index 89262191436..00000000000 --- a/erpnext/setup/doctype/setup_progress/test_setup_progress.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors -# See license.txt -from __future__ import unicode_literals - -import unittest - -class TestSetupProgress(unittest.TestCase): - pass diff --git a/erpnext/setup/doctype/setup_progress_action/__init__.py b/erpnext/setup/doctype/setup_progress_action/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/erpnext/setup/doctype/setup_progress_action/setup_progress_action.json b/erpnext/setup/doctype/setup_progress_action/setup_progress_action.json deleted file mode 100644 index e9abcbcd1ac..00000000000 --- a/erpnext/setup/doctype/setup_progress_action/setup_progress_action.json +++ /dev/null @@ -1,253 +0,0 @@ -{ - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2017-08-27 21:00:40.715360", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", - "fields": [ - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "action_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": "Action 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "action_doctype", - "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": "Action Doctype", - "length": 0, - "no_copy": 0, - "options": "DocType", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "action_document", - "fieldtype": "Dynamic 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": "Action Document", - "length": 0, - "no_copy": 0, - "options": "action_doctype", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "action_field", - "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": "Action Field", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "min_doc_count", - "fieldtype": "Int", - "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": "Min Doc Count", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "domains", - "fieldtype": "Code", - "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": "Domains", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_completed", - "fieldtype": "Check", - "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": "Is Completed", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2017-09-01 14:34:59.685730", - "modified_by": "Administrator", - "module": "Setup", - "name": "Setup Progress Action", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 1, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0 -} \ No newline at end of file diff --git a/erpnext/setup/doctype/setup_progress_action/setup_progress_action.py b/erpnext/setup/doctype/setup_progress_action/setup_progress_action.py deleted file mode 100644 index 24af94347e5..00000000000 --- a/erpnext/setup/doctype/setup_progress_action/setup_progress_action.py +++ /dev/null @@ -1,9 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -from frappe.model.document import Document - -class SetupProgressAction(Document): - pass diff --git a/erpnext/utilities/onboarding_utils.py b/erpnext/utilities/onboarding_utils.py index 35f2b6a7edd..7b775a7b7d9 100644 --- a/erpnext/utilities/onboarding_utils.py +++ b/erpnext/utilities/onboarding_utils.py @@ -7,19 +7,12 @@ import frappe, erpnext import json from frappe import _ from frappe.utils import flt -from erpnext.setup.doctype.setup_progress.setup_progress import update_domain_actions, get_domain_actions_state - -@frappe.whitelist() -def set_sales_target(args_data): - args = json.loads(args_data) - defaults = frappe.defaults.get_defaults() - frappe.db.set_value("Company", defaults.get("company"), "monthly_sales_target", args.get('monthly_sales_target')) @frappe.whitelist() def create_customers(args_data): args = json.loads(args_data) defaults = frappe.defaults.get_defaults() - for i in range(1,4): + for i in range(1,args.get('max_count')): customer = args.get("customer_name_" + str(i)) if customer: try: @@ -57,7 +50,7 @@ def create_letterhead(args_data): def create_suppliers(args_data): args = json.loads(args_data) defaults = frappe.defaults.get_defaults() - for i in range(1,4): + for i in range(1,args.get('max_count')): supplier = args.get("supplier_name_" + str(i)) if supplier: try: @@ -90,7 +83,7 @@ def create_contact(contact, party_type, party): def create_items(args_data): args = json.loads(args_data) defaults = frappe.defaults.get_defaults() - for i in range(1,4): + for i in range(1, args.get('max_count')): item = args.get("item_" + str(i)) if item: default_warehouse = "" @@ -141,7 +134,7 @@ def make_item_price(item, price_list_name, item_price): @frappe.whitelist() def create_program(args_data): args = json.loads(args_data) - for i in range(1,4): + for i in range(1,args.get('max_count')): if args.get("program_" + str(i)): program = frappe.new_doc("Program") program.program_code = args.get("program_" + str(i)) @@ -154,7 +147,7 @@ def create_program(args_data): @frappe.whitelist() def create_course(args_data): args = json.loads(args_data) - for i in range(1,4): + for i in range(1,args.get('max_count')): if args.get("course_" + str(i)): course = frappe.new_doc("Course") course.course_code = args.get("course_" + str(i)) @@ -167,7 +160,7 @@ def create_course(args_data): @frappe.whitelist() def create_instructor(args_data): args = json.loads(args_data) - for i in range(1,4): + for i in range(1,args.get('max_count')): if args.get("instructor_" + str(i)): instructor = frappe.new_doc("Instructor") instructor.instructor_name = args.get("instructor_" + str(i)) @@ -179,7 +172,7 @@ def create_instructor(args_data): @frappe.whitelist() def create_room(args_data): args = json.loads(args_data) - for i in range(1,4): + for i in range(1,args.get('max_count')): if args.get("room_" + str(i)): room = frappe.new_doc("Room") room.room_name = args.get("room_" + str(i)) @@ -195,7 +188,7 @@ def create_users(args_data): return args = json.loads(args_data) defaults = frappe.defaults.get_defaults() - for i in range(1,4): + for i in range(1,args.get('max_count')): email = args.get("user_email_" + str(i)) fullname = args.get("user_fullname_" + str(i)) if email: @@ -231,4 +224,4 @@ def create_users(args_data): emp.flags.ignore_mandatory = True emp.insert(ignore_permissions = True) -# Ennumerate the setup hooks you're going to need, apart from the slides +# Enumerate the setup hooks you're going to need, apart from the slides diff --git a/erpnext/utilities/user_progress.py b/erpnext/utilities/user_progress.py deleted file mode 100644 index 5cec3ca3846..00000000000 --- a/erpnext/utilities/user_progress.py +++ /dev/null @@ -1,287 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe, erpnext -from frappe import _ -from erpnext.setup.doctype.setup_progress.setup_progress import get_action_completed_state - -def get_slide_settings(): - defaults = frappe.defaults.get_defaults() - domain = frappe.get_cached_value('Company', erpnext.get_default_company(), 'domain') - company = defaults.get("company") or '' - currency = defaults.get("currency") or '' - - doc = frappe.get_doc("Setup Progress") - item = [d for d in doc.get("actions") if d.action_name == "Set Sales Target"] - - if len(item): - item = item[0] - if not item.action_document: - item.action_document = company - doc.save() - - # Initial state of slides - return [ - frappe._dict( - action_name='Add Company', - title=_("Setup Company") if domain != 'Education' else _("Setup Institution"), - help=_('Setup your ' + ('company' if domain != 'Education' else 'institution') + ' and brand.'), - # image_src="/assets/erpnext/images/illustrations/shop.jpg", - fields=[], - done_state_title=_("You added " + company), - done_state_title_route=["Form", "Company", company], - help_links=[ - { - "label": _("Chart of Accounts"), - "url": ["https://erpnext.com/docs/user/manual/en/accounts/chart-of-accounts"] - }, - { - "label": _("Opening Balances"), - "video_id": "U5wPIvEn-0c" - } - ] - ), - frappe._dict( - action_name='Set Sales Target', - domains=('Manufacturing', 'Services', 'Retail', 'Distribution'), - title=_("Set a Target"), - help=_("Set a sales goal you'd like to achieve for your company."), - fields=[ - {"fieldtype":"Currency", "fieldname":"monthly_sales_target", - "label":_("Monthly Sales Target (" + currency + ")"), "reqd":1}, - ], - submit_method="erpnext.utilities.user_progress_utils.set_sales_target", - done_state_title=_("Go to " + company), - done_state_title_route=["Form", "Company", company], - help_links=[ - { - "label": _('Learn More'), - "url": ["https://erpnext.com/docs/user/manual/en/setting-up/setting-company-sales-goal"] - } - ] - ), - frappe._dict( - action_name='Add Customers', - domains=('Manufacturing', 'Services', 'Retail', 'Distribution'), - title=_("Add Customers"), - help=_("List a few of your customers. They could be organizations or individuals."), - fields=[ - {"fieldtype":"Section Break"}, - {"fieldtype":"Data", "fieldname":"customer", "label":_("Customer"), - "placeholder":_("Customer Name")}, - {"fieldtype":"Column Break"}, - {"fieldtype":"Data", "fieldname":"customer_contact", - "label":_("Contact Name"), "placeholder":_("Contact Name")} - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_customers", - done_state_title=_("Go to Customers"), - done_state_title_route=["List", "Customer"], - help_links=[ - { - "label": _('Learn More'), - "url": ["https://erpnext.com/docs/user/manual/en/CRM/customer.html"] - } - ] - ), - - frappe._dict( - action_name='Add Letterhead', - domains=('Manufacturing', 'Services', 'Retail', 'Distribution', 'Education'), - title=_("Add Letterhead"), - help=_("Upload your letter head (Keep it web friendly as 900px by 100px)"), - fields=[ - {"fieldtype":"Attach Image", "fieldname":"letterhead", - "is_private": 0, - "align": "center" - }, - ], - mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_letterhead", - done_state_title=_("Go to Letterheads"), - done_state_title_route=["List", "Letter Head"] - ), - - frappe._dict( - action_name='Add Suppliers', - domains=('Manufacturing', 'Services', 'Retail', 'Distribution'), - icon="fa fa-group", - title=_("Your Suppliers"), - help=_("List a few of your suppliers. They could be organizations or individuals."), - fields=[ - {"fieldtype":"Section Break"}, - {"fieldtype":"Data", "fieldname":"supplier", "label":_("Supplier"), - "placeholder":_("Supplier Name")}, - {"fieldtype":"Column Break"}, - {"fieldtype":"Data", "fieldname":"supplier_contact", - "label":_("Contact Name"), "placeholder":_("Contact Name")}, - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_suppliers", - done_state_title=_("Go to Suppliers"), - done_state_title_route=["List", "Supplier"], - help_links=[ - { - "label": _('Learn More'), - "url": ["https://erpnext.com/docs/user/manual/en/buying/supplier"] - }, - { - "label": _('Customers and Suppliers'), - "video_id": "zsrrVDk6VBs" - }, - ] - ), - frappe._dict( - action_name='Add Products', - domains=['Manufacturing', 'Services', 'Retail', 'Distribution'], - icon="fa fa-barcode", - title=_("Your Products or Services"), - help=_("List your products or services that you buy or sell."), - fields=[ - {"fieldtype":"Section Break", "show_section_border": 1}, - {"fieldtype":"Data", "fieldname":"item", "label":_("Item"), - "placeholder":_("A Product")}, - {"fieldtype":"Column Break"}, - {"fieldtype":"Select", "fieldname":"item_uom", "label":_("UOM"), - "options":[_("Unit"), _("Nos"), _("Box"), _("Pair"), _("Kg"), _("Set"), - _("Hour"), _("Minute"), _("Litre"), _("Meter"), _("Gram")], - "default": _("Unit"), "static": 1}, - {"fieldtype":"Column Break"}, - {"fieldtype":"Currency", "fieldname":"item_price", "label":_("Rate"), "static": 1} - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_items", - done_state_title=_("Go to Items"), - done_state_title_route=["List", "Item"], - help_links=[ - { - "label": _("Explore Sales Cycle"), - "video_id": "1eP90MWoDQM" - }, - ] - ), - - # Education slides begin - frappe._dict( - action_name='Add Programs', - domains=("Education"), - title=_("Program"), - help=_("Example: Masters in Computer Science"), - fields=[ - {"fieldtype":"Section Break", "show_section_border": 1}, - {"fieldtype":"Data", "fieldname":"program", "label":_("Program"), "placeholder": _("Program Name")}, - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_program", - done_state_title=_("Go to Programs"), - done_state_title_route=["List", "Program"], - help_links=[ - { - "label": _("Student Application"), - "video_id": "l8PUACusN3E" - }, - ] - - ), - frappe._dict( - action_name='Add Courses', - domains=["Education"], - title=_("Course"), - help=_("Example: Basic Mathematics"), - fields=[ - {"fieldtype":"Section Break", "show_section_border": 1}, - {"fieldtype":"Data", "fieldname":"course", "label":_("Course"), "placeholder": _("Course Name")}, - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_course", - done_state_title=_("Go to Courses"), - done_state_title_route=["List", "Course"], - help_links=[ - { - "label": _('Add Students'), - "route": ["List", "Student"] - } - ] - ), - frappe._dict( - action_name='Add Instructors', - domains=["Education"], - title=_("Instructor"), - help=_("People who teach at your organisation"), - fields=[ - {"fieldtype":"Section Break", "show_section_border": 1}, - {"fieldtype":"Data", "fieldname":"instructor", "label":_("Instructor"), "placeholder": _("Instructor Name")}, - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_instructor", - done_state_title=_("Go to Instructors"), - done_state_title_route=["List", "Instructor"], - help_links=[ - { - "label": _('Student Batches'), - "route": ["List", "Student Batch"] - } - ] - ), - frappe._dict( - action_name='Add Rooms', - domains=["Education"], - title=_("Room"), - help=_("Classrooms/ Laboratories etc where lectures can be scheduled."), - fields=[ - {"fieldtype":"Section Break", "show_section_border": 1}, - {"fieldtype":"Data", "fieldname":"room", "label":_("Room")}, - {"fieldtype":"Column Break"}, - {"fieldtype":"Int", "fieldname":"room_capacity", "label":_("Room Capacity"), "static": 1}, - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_room", - done_state_title=_("Go to Rooms"), - done_state_title_route=["List", "Room"], - help_links=[] - ), - # Education slides end - - frappe._dict( - action_name='Add Users', - title=_("Add Users"), - help=_("Add users to your organization, other than yourself."), - fields=[ - {"fieldtype":"Section Break"}, - {"fieldtype":"Data", "fieldname":"user_email", "label":_("Email ID"), - "placeholder":_("user@example.com"), "options": "Email", "static": 1}, - {"fieldtype":"Column Break"}, - {"fieldtype":"Data", "fieldname":"user_fullname", - "label":_("Full Name"), "static": 1}, - ], - add_more=1, max_count=3, mandatory_entry=1, - submit_method="erpnext.utilities.user_progress_utils.create_users", - done_state_title=_("Go to Users"), - done_state_title_route=["List", "User"], - help_links=[ - { - "label": _('Learn More'), - "url": ["https://erpnext.com/docs/user/manual/en/setting-up/users-and-permissions"] - }, - { - "label": _('Users and Permissions'), - "video_id": "8Slw1hsTmUI" - }, - ] - ) - ] - -def get_user_progress_slides(): - slides = [] - slide_settings = get_slide_settings() - - domains = frappe.get_active_domains() - for s in slide_settings: - if not s.domains or any(d in domains for d in s.domains): - s.mark_as_done_method = "erpnext.setup.doctype.setup_progress.setup_progress.set_action_completed_state" - s.done = get_action_completed_state(s.action_name) or 0 - slides.append(s) - - return slides - From a077795581a389c58dafbf36b082ba02df19fd0c Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 12:01:00 +0530 Subject: [PATCH 253/679] 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 254/679] 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 cd3976f7d24a703c80858237e9f51569b9323bea Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 25 Nov 2019 12:24:34 +0530 Subject: [PATCH 255/679] Asset cancellation fix (#19671) * fix: remove asset movement mandatory fields * fix: label for reference doctype --- erpnext/assets/doctype/asset/asset.py | 25 +++------ .../doctype/asset_movement/asset_movement.js | 51 +++---------------- .../asset_movement/asset_movement.json | 15 +++--- .../doctype/asset_movement/asset_movement.py | 2 +- erpnext/controllers/buying_controller.py | 13 +++-- 5 files changed, 32 insertions(+), 74 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 546f3740947..56341ed1b1f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -125,12 +125,14 @@ class Asset(AccountsController): frappe.throw(_("Available-for-use Date should be after purchase date")) def cancel_auto_gen_movement(self): - reference_docname = self.purchase_invoice or self.purchase_receipt - movement = frappe.db.get_all('Asset Movement', filters={ 'reference_name': reference_docname, 'docstatus': 1 }) - if len(movement) > 1: + movements = frappe.db.sql( + """SELECT asm.name, asm.docstatus + FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item + WHERE asm_item.parent=asm.name and asm_item.asset=%s and asm.docstatus=1""", self.name, as_dict=1) + if len(movements) > 1: frappe.throw(_('Asset has multiple Asset Movement Entries which has to be \ cancelled manually to cancel this asset.')) - movement = frappe.get_doc('Asset Movement', movement[0].get('name')) + movement = frappe.get_doc('Asset Movement', movements[0].get('name')) movement.flags.ignore_validate = True movement.cancel() @@ -658,23 +660,10 @@ def make_asset_movement(assets, purpose=None): frappe.throw(_('Atleast one asset has to be selected.')) asset_movement = frappe.new_doc("Asset Movement") - asset_movement.purpose = purpose - prev_reference_docname = '' - + asset_movement.quantity = len(assets) for asset in assets: asset = frappe.get_doc('Asset', asset.get('name')) - # get PR/PI linked with asset - reference_docname = asset.get('purchase_receipt') if asset.get('purchase_receipt') \ - else asset.get('purchase_invoice') - # checks if all the assets are linked with a single PR/PI - if prev_reference_docname == '': - prev_reference_docname = reference_docname - elif prev_reference_docname != reference_docname: - frappe.throw(_('Assets selected should belong to same reference document.')) - asset_movement.company = asset.get('company') - asset_movement.reference_doctype = 'Purchase Receipt' if asset.get('purchase_receipt') else 'Purchase Invoice' - asset_movement.reference_name = prev_reference_docname asset_movement.append("assets", { 'asset': asset.get('name'), 'source_location': asset.get('location'), diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index 89977e29529..06d8879091c 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -31,6 +31,13 @@ frappe.ui.form.on('Asset Movement', { name: ["in", ["Purchase Receipt", "Purchase Invoice"]] } }; + }), + frm.set_query("asset", "assets", () => { + return { + filters: { + status: ["not in", ["Draft"]] + } + } }) }, @@ -76,50 +83,6 @@ frappe.ui.form.on('Asset Movement', { }); }); frm.refresh_field('assets'); - }, - - reference_name: function(frm) { - if (frm.doc.reference_name && frm.doc.reference_doctype) { - const reference_doctype = frm.doc.reference_doctype === 'Purchase Invoice' ? 'purchase_invoice' : 'purchase_receipt'; - // On selection of reference name, - // sets query to display assets linked to that reference doc - frm.set_query('asset', 'assets', function() { - return { - filters: { - [reference_doctype] : frm.doc.reference_name - } - }; - }); - - // fetches linked asset & adds to the assets table - frappe.db.get_list('Asset', { - fields: ['name', 'location', 'custodian'], - filters: { - [reference_doctype] : frm.doc.reference_name - } - }).then((docs) => { - if (docs.length == 0) { - frappe.msgprint(frappe._(`Please select ${frm.doc.reference_doctype} which has assets.`)); - frm.doc.reference_name = ''; - frm.refresh_field('reference_name'); - return; - } - frm.doc.assets = []; - docs.forEach(doc => { - frm.add_child('assets', { - asset: doc.name, - source_location: doc.location, - from_employee: doc.custodian - }); - frm.refresh_field('assets'); - }) - }).catch((err) => { - console.log(err); // eslint-disable-line - }); - } else { - // if reference is deleted then remove query - frm.set_query('asset', 'assets', () => ({ filters: {} })); - } } }); diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index e62d6844114..3472ab5d7da 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -9,12 +9,12 @@ "purpose", "column_break_4", "transaction_date", + "section_break_10", + "assets", "reference", "reference_doctype", "column_break_9", "reference_name", - "section_break_10", - "assets", "amended_from" ], "fields": [ @@ -47,6 +47,7 @@ "fieldtype": "Column Break" }, { + "collapsible": 1, "fieldname": "reference", "fieldtype": "Section Break", "label": "Reference" @@ -54,18 +55,16 @@ { "fieldname": "reference_doctype", "fieldtype": "Link", - "label": "Reference Document", + "label": "Reference Document Type", "no_copy": 1, - "options": "DocType", - "reqd": 1 + "options": "DocType" }, { "fieldname": "reference_name", "fieldtype": "Dynamic Link", "label": "Reference Document Name", "no_copy": 1, - "options": "reference_doctype", - "reqd": 1 + "options": "reference_doctype" }, { "fieldname": "amended_from", @@ -93,7 +92,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-21 14:35:51.880332", + "modified": "2019-11-23 13:28:47.256935", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 714845dfac9..4e1822b2ce5 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -22,7 +22,7 @@ class AssetMovement(Document): if company != self.company: frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company)) - if not(d.source_location or d.target_location or d.from_employee or d.to_employee): + if not (d.source_location or d.target_location or d.from_employee or d.to_employee): frappe.throw(_("Either location or employee must be required")) def validate_location(self): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3392850e963..d12643af820 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -641,7 +641,10 @@ class BuyingController(StockController): asset = frappe.get_doc('Asset', asset.name) if delete_asset and is_auto_create_enabled: # need to delete movements to delete assets otherwise throws link exists error - movements = frappe.db.get_all('Asset Movement', filters={ 'reference_name': self.name }) + movements = frappe.db.sql( + """SELECT asm.name + FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item + WHERE asm_item.parent=asm.name and asm_item.asset=%s""", asset.name, as_dict=1) for movement in movements: frappe.delete_doc('Asset Movement', movement.name, force=1) frappe.delete_doc("Asset", asset.name, force=1) @@ -652,8 +655,12 @@ class BuyingController(StockController): asset.purchase_date = self.posting_date asset.supplier = self.supplier elif self.docstatus == 2: - asset.set(field, None) - asset.supplier = None + if asset.docstatus == 0: + asset.set(field, None) + asset.supplier = None + if asset.docstatus == 1 and delete_asset: + frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\ + Please cancel the it to continue.').format(asset.name)) asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_mandatory = True From 1ed1c4e6a432214444744774ad555c8446160396 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 12:33:40 +0530 Subject: [PATCH 256/679] 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 257/679] 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 7728ae698801d5f97ef811c40b46ee68ab27acab Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 25 Nov 2019 13:23:53 +0530 Subject: [PATCH 258/679] fix: added company letter head slide --- erpnext/utilities/onboarding_utils.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/erpnext/utilities/onboarding_utils.py b/erpnext/utilities/onboarding_utils.py index 7b775a7b7d9..6c40f81b923 100644 --- a/erpnext/utilities/onboarding_utils.py +++ b/erpnext/utilities/onboarding_utils.py @@ -17,7 +17,7 @@ def create_customers(args_data): if customer: try: doc = frappe.get_doc({ - "doctype":"Customer", + "doctype": "Customer", "customer_name": customer, "customer_type": "Company", "customer_group": _("Commercial"), @@ -25,9 +25,9 @@ def create_customers(args_data): "company": defaults.get("company") }).insert() - if args.get("customer_contact_" + str(i)): - create_contact(args.get("customer_contact_" + str(i)), - "Customer", doc.name) + if args.get("customer_email_" + str(i)): + create_contact(customer, "Customer", + doc.name, args.get("customer_email_" + str(i))) except frappe.NameError: pass @@ -38,8 +38,8 @@ def create_letterhead(args_data): if letterhead: try: frappe.get_doc({ - "doctype":"Letter Head", - "content":"""

""".format(letterhead.encode('utf-8')), + "doctype": "Letter Head", + "image": letterhead, "letter_head_name": _("Standard"), "is_default": 1 }).insert() @@ -61,21 +61,22 @@ def create_suppliers(args_data): "company": defaults.get("company") }).insert() - if args.get("supplier_contact_" + str(i)): - create_contact(args.get("supplier_contact_" + str(i)), - "Supplier", doc.name) + if args.get("supplier_email_" + str(i)): + create_contact(supplier, "Supplier", + doc.name, args.get("supplier_email_" + str(i))) except frappe.NameError: pass -def create_contact(contact, party_type, party): +def create_contact(contact, party_type, party, email): """Create contact based on given contact name""" contact = contact.split(" ") contact = frappe.get_doc({ - "doctype":"Contact", - "first_name":contact[0], + "doctype": "Contact", + "first_name": contact[0], "last_name": len(contact) > 1 and contact[1] or "" }) + contact.append('email_ids', dict(email_id=email, is_primary=1)) contact.append('links', dict(link_doctype=party_type, link_name=party)) contact.insert() From 647331bbd70a8d1b7c1b47c974175bbcbb70d15e Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 25 Nov 2019 13:56:00 +0530 Subject: [PATCH 259/679] feat: add search in category --- erpnext/public/js/hub/pages/Category.vue | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/erpnext/public/js/hub/pages/Category.vue b/erpnext/public/js/hub/pages/Category.vue index 3a0e6bfab83..a1d5d729cc9 100644 --- a/erpnext/public/js/hub/pages/Category.vue +++ b/erpnext/public/js/hub/pages/Category.vue @@ -3,6 +3,12 @@ class="marketplace-page" :data-page-name="page_name" > + +
{{ page_title }}
From 35e8d1e1d7381bcbd92a50f2798c0fdb9dae4fc4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 14:02:51 +0530 Subject: [PATCH 260/679] Stock acc bal sync msg (#19676) * fix: Prefilled JV via Account Balance and Stock Value mismatch error message - Make JV button will route to Journal Entry and add rows in child table * fix: make journal entry to sync stock and account balance * fix: translated action label --- erpnext/accounts/general_ledger.py | 34 +++++++++++++++++------ erpnext/public/js/controllers/accounts.js | 4 +-- erpnext/public/js/utils.js | 16 +++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index e9703dd7907..2ba319d05e5 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -163,16 +163,32 @@ def validate_account_for_perpetual_inventory(gl_map): .format(account), StockAccountInvalidTransaction) elif account_bal != stock_bal: - error_reason = _("Account Balance ({0}) and Stock Value ({1}) is out of sync for account {2} and it's linked warehouses.").format( - account_bal, stock_bal, frappe.bold(account)) - error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(stock_bal - account_bal)) - button_text = _("Make Adjustment Entry") + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) - frappe.throw("""{0}

{1}

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

{1}

""".format(error_reason, error_resolution), + raise_exception=StockValueAndAccountBalanceOutOfSync, + title=_('Values Out Of Sync'), + primary_action={ + 'label': _('Make Journal Entry'), + 'client_action': 'erpnext.route_to_adjustment_jv', + 'args': journal_entry_args + }) def validate_cwip_accounts(gl_map): cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 3dfc8911fc4..f4eaad58dae 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -64,7 +64,7 @@ frappe.ui.form.on(cur_frm.doctype, { } }) } - } + } }); frappe.ui.form.on('Sales Invoice Payment', { @@ -355,4 +355,4 @@ cur_frm.pformat.taxes= function(doc){ out += '
'; } return out; -} +} \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 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 f2752bf38c20e872b044fbe93d97f09f9fdbed00 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 14:09:49 +0530 Subject: [PATCH 261/679] 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 262/679] 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 263/679] 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 264/679] 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 c856cb85d9e443f00ee306428ab69e4b8aa88049 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 15:08:24 +0530 Subject: [PATCH 265/679] log: Change log for v12.2.0 --- erpnext/change_log/v12/v12_2_0.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/change_log/v12/v12_2_0.md diff --git a/erpnext/change_log/v12/v12_2_0.md b/erpnext/change_log/v12/v12_2_0.md new file mode 100644 index 00000000000..0ec0eeca3df --- /dev/null +++ b/erpnext/change_log/v12/v12_2_0.md @@ -0,0 +1,14 @@ +# Version 12.2.0 Release Notes + +### Accounting + +1. Fixed Asset + - "Enable CWIP" options moved to Asset Category from Asset Settings + - Removed Asset link from Purchase Receipt Item table + - Enhanced Asset master + - Asset Movement now handles movement of multiple assets + - Introduced monthly depreciation +2. GL Entries for Landed Cost Voucher now posted directly against individual Charges account +3. Optimization of BOM Update Tool +4. Syncing of Stock and Account balance is enforced, in case of perpetual inventory +5. Rendered email template in Email Campaign From 4d12f8acab13add3bff3bdf30f8d640d60e16dfc Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 15:31:23 +0550 Subject: [PATCH 266/679] bumped to version 12.2.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index d031bc5bb17..f40b9575632 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '12.1.8' +__version__ = '12.2.0' def get_default_company(user=None): '''Get default company for user''' From 032baeac5b89794bef874828ac8cab5d0dd0edda Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 15:15:00 +0530 Subject: [PATCH 267/679] 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 268/679] 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 269/679] 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 270/679] 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 271/679] 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 272/679] 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 273/679] 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 274/679] 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 275/679] 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 276/679] 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 195893db0e18619830ae24c6d7b5222cb2df8080 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 Nov 2019 15:22:05 +0530 Subject: [PATCH 277/679] fix: conditionally run old patch for Setup Progress --- erpnext/patches/v8_9/add_setup_progress_actions.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/patches/v8_9/add_setup_progress_actions.py b/erpnext/patches/v8_9/add_setup_progress_actions.py index fe123111bce..77501073cf4 100644 --- a/erpnext/patches/v8_9/add_setup_progress_actions.py +++ b/erpnext/patches/v8_9/add_setup_progress_actions.py @@ -5,6 +5,9 @@ from frappe import _ def execute(): """Add setup progress actions""" + if not frappe.db.exists('DocType', 'Setup Progress') or not frappe.db.exists('DocType', 'Setup Progress Action'): + return + frappe.reload_doc("setup", "doctype", "setup_progress") frappe.reload_doc("setup", "doctype", "setup_progress_action") From 3f8c46058460346690d6d9b919b231bcff20710e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 Nov 2019 15:22:45 +0530 Subject: [PATCH 278/679] feat: moved slide action functions from onboarding utils to doctypes --- erpnext/buying/doctype/supplier/supplier.py | 20 ++ erpnext/selling/doctype/customer/customer.py | 34 +++ erpnext/stock/doctype/item/item.py | 48 ++++ erpnext/utilities/onboarding_utils.py | 228 ------------------- 4 files changed, 102 insertions(+), 228 deletions(-) delete mode 100644 erpnext/utilities/onboarding_utils.py diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py index b6d588ed967..62a04f37b11 100644 --- a/erpnext/buying/doctype/supplier/supplier.py +++ b/erpnext/buying/doctype/supplier/supplier.py @@ -56,3 +56,23 @@ class Supplier(TransactionBase): def after_rename(self, olddn, newdn, merge=False): if frappe.defaults.get_global_default('supp_master_name') == 'Supplier Name': frappe.db.set(self, "supplier_name", newdn) + + def create_onboarding_docs(self, args): + defaults = frappe.defaults.get_defaults() + for i in range(1, args.get('max_count')): + supplier = args.get('supplier_name_' + str(i)) + if supplier: + try: + doc = frappe.get_doc({ + 'doctype': self.doctype, + 'supplier_name': supplier, + 'supplier_group': _('Local'), + 'company': defaults.get('company') + }).insert() + + if args.get('supplier_email_' + str(i)): + from erpnext.selling.doctype.customer.customer import create_contact + create_contact(supplier, 'Supplier', + doc.name, args.get('supplier_email_' + str(i))) + except frappe.NameError: + pass \ No newline at end of file diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index a8e3ce4eae7..e1a619ea550 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -204,6 +204,40 @@ class Customer(TransactionBase): else: frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually.")) + def create_onboarding_docs(self, args): + defaults = frappe.defaults.get_defaults() + for i in range(1, args.get('max_count')): + customer = args.get('customer_name_' + str(i)) + if customer: + try: + doc = frappe.get_doc({ + 'doctype': self.doctype, + 'customer_name': customer, + 'customer_type': 'Company', + 'customer_group': _('Commercial'), + 'territory': defaults.get('country'), + 'company': defaults.get('company') + }).insert() + + if args.get('customer_email_' + str(i)): + create_contact(customer, self.doctype, + doc.name, args.get("customer_email_" + str(i))) + except frappe.NameError: + pass + +def create_contact(contact, party_type, party, email): + """Create contact based on given contact name""" + contact = contact.split(' ') + + contact = frappe.get_doc({ + 'doctype': 'Contact', + 'first_name': contact[0], + 'last_name': len(contact) > 1 and contact[1] or "" + }) + contact.append('email_ids', dict(email_id=email, is_primary=1)) + contact.append('links', dict(link_doctype=party_type, link_name=party)) + contact.insert() + @frappe.whitelist() def get_loyalty_programs(doc): ''' returns applicable loyalty programs for a customer ''' diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 164c659fe87..a2abd195572 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -884,6 +884,54 @@ class Item(WebsiteGenerator): if not enabled: frappe.msgprint(msg=_("You have to enable auto re-order in Stock Settings to maintain re-order levels."), title=_("Enable Auto Re-Order"), indicator="orange") + def create_onboarding_docs(self, args): + defaults = frappe.defaults.get_defaults() + for i in range(1, args.get('max_count')): + item = args.get('item_' + str(i)) + if item: + default_warehouse = '' + default_warehouse = frappe.db.get_value('Warehouse', filters={ + 'warehouse_name': _('Finished Goods'), + 'company': defaults.get('company_name') + }) + + try: + frappe.get_doc({ + 'doctype': self.doctype, + 'item_code': item, + 'item_name': item, + 'description': item, + 'show_in_website': 1, + 'is_sales_item': 1, + 'is_purchase_item': 1, + 'is_stock_item': 1, + 'item_group': _('Products'), + 'stock_uom': _(args.get('item_uom_' + str(i))), + 'item_defaults': [{ + 'default_warehouse': default_warehouse, + 'company': defaults.get('company_name') + }] + }).insert() + + except frappe.NameError: + pass + else: + if args.get('item_price_' + str(i)): + item_price = flt(args.get('tem_price_' + str(i))) + + price_list_name = frappe.db.get_value('Price List', {'selling': 1}) + make_item_price(item, price_list_name, item_price) + price_list_name = frappe.db.get_value('Price List', {'buying': 1}) + make_item_price(item, price_list_name, item_price) + +def make_item_price(item, price_list_name, item_price): + frappe.get_doc({ + 'doctype': 'Item Price', + 'price_list': price_list_name, + 'item_code': item, + 'price_list_rate': item_price + }).insert() + def get_timeline_data(doctype, name): '''returns timeline data based on stock ledger entry''' out = {} diff --git a/erpnext/utilities/onboarding_utils.py b/erpnext/utilities/onboarding_utils.py deleted file mode 100644 index 6c40f81b923..00000000000 --- a/erpnext/utilities/onboarding_utils.py +++ /dev/null @@ -1,228 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals -import frappe, erpnext - -import json -from frappe import _ -from frappe.utils import flt - -@frappe.whitelist() -def create_customers(args_data): - args = json.loads(args_data) - defaults = frappe.defaults.get_defaults() - for i in range(1,args.get('max_count')): - customer = args.get("customer_name_" + str(i)) - if customer: - try: - doc = frappe.get_doc({ - "doctype": "Customer", - "customer_name": customer, - "customer_type": "Company", - "customer_group": _("Commercial"), - "territory": defaults.get("country"), - "company": defaults.get("company") - }).insert() - - if args.get("customer_email_" + str(i)): - create_contact(customer, "Customer", - doc.name, args.get("customer_email_" + str(i))) - except frappe.NameError: - pass - -@frappe.whitelist() -def create_letterhead(args_data): - args = json.loads(args_data) - letterhead = args.get("letterhead") - if letterhead: - try: - frappe.get_doc({ - "doctype": "Letter Head", - "image": letterhead, - "letter_head_name": _("Standard"), - "is_default": 1 - }).insert() - except frappe.NameError: - pass - -@frappe.whitelist() -def create_suppliers(args_data): - args = json.loads(args_data) - defaults = frappe.defaults.get_defaults() - for i in range(1,args.get('max_count')): - supplier = args.get("supplier_name_" + str(i)) - if supplier: - try: - doc = frappe.get_doc({ - "doctype":"Supplier", - "supplier_name": supplier, - "supplier_group": _("Local"), - "company": defaults.get("company") - }).insert() - - if args.get("supplier_email_" + str(i)): - create_contact(supplier, "Supplier", - doc.name, args.get("supplier_email_" + str(i))) - except frappe.NameError: - pass - -def create_contact(contact, party_type, party, email): - """Create contact based on given contact name""" - contact = contact.split(" ") - - contact = frappe.get_doc({ - "doctype": "Contact", - "first_name": contact[0], - "last_name": len(contact) > 1 and contact[1] or "" - }) - contact.append('email_ids', dict(email_id=email, is_primary=1)) - contact.append('links', dict(link_doctype=party_type, link_name=party)) - contact.insert() - -@frappe.whitelist() -def create_items(args_data): - args = json.loads(args_data) - defaults = frappe.defaults.get_defaults() - for i in range(1, args.get('max_count')): - item = args.get("item_" + str(i)) - if item: - default_warehouse = "" - default_warehouse = frappe.db.get_value("Warehouse", filters={ - "warehouse_name": _("Finished Goods"), - "company": defaults.get("company_name") - }) - - try: - frappe.get_doc({ - "doctype":"Item", - "item_code": item, - "item_name": item, - "description": item, - "show_in_website": 1, - "is_sales_item": 1, - "is_purchase_item": 1, - "is_stock_item": 1, - "item_group": _("Products"), - "stock_uom": _(args.get("item_uom_" + str(i))), - "item_defaults": [{ - "default_warehouse": default_warehouse, - "company": defaults.get("company_name") - }] - }).insert() - - except frappe.NameError: - pass - else: - if args.get("item_price_" + str(i)): - item_price = flt(args.get("item_price_" + str(i))) - - price_list_name = frappe.db.get_value("Price List", {"selling": 1}) - make_item_price(item, price_list_name, item_price) - price_list_name = frappe.db.get_value("Price List", {"buying": 1}) - make_item_price(item, price_list_name, item_price) - - -def make_item_price(item, price_list_name, item_price): - frappe.get_doc({ - "doctype": "Item Price", - "price_list": price_list_name, - "item_code": item, - "price_list_rate": item_price - }).insert() - -# Education -@frappe.whitelist() -def create_program(args_data): - args = json.loads(args_data) - for i in range(1,args.get('max_count')): - if args.get("program_" + str(i)): - program = frappe.new_doc("Program") - program.program_code = args.get("program_" + str(i)) - program.program_name = args.get("program_" + str(i)) - try: - program.save() - except frappe.DuplicateEntryError: - pass - -@frappe.whitelist() -def create_course(args_data): - args = json.loads(args_data) - for i in range(1,args.get('max_count')): - if args.get("course_" + str(i)): - course = frappe.new_doc("Course") - course.course_code = args.get("course_" + str(i)) - course.course_name = args.get("course_" + str(i)) - try: - course.save() - except frappe.DuplicateEntryError: - pass - -@frappe.whitelist() -def create_instructor(args_data): - args = json.loads(args_data) - for i in range(1,args.get('max_count')): - if args.get("instructor_" + str(i)): - instructor = frappe.new_doc("Instructor") - instructor.instructor_name = args.get("instructor_" + str(i)) - try: - instructor.save() - except frappe.DuplicateEntryError: - pass - -@frappe.whitelist() -def create_room(args_data): - args = json.loads(args_data) - for i in range(1,args.get('max_count')): - if args.get("room_" + str(i)): - room = frappe.new_doc("Room") - room.room_name = args.get("room_" + str(i)) - room.seating_capacity = args.get("room_capacity_" + str(i)) - try: - room.save() - except frappe.DuplicateEntryError: - pass - -@frappe.whitelist() -def create_users(args_data): - if frappe.session.user == 'Administrator': - return - args = json.loads(args_data) - defaults = frappe.defaults.get_defaults() - for i in range(1,args.get('max_count')): - email = args.get("user_email_" + str(i)) - fullname = args.get("user_fullname_" + str(i)) - if email: - if not fullname: - fullname = email.split("@")[0] - - parts = fullname.split(" ", 1) - - user = frappe.get_doc({ - "doctype": "User", - "email": email, - "first_name": parts[0], - "last_name": parts[1] if len(parts) > 1 else "", - "enabled": 1, - "user_type": "System User" - }) - - # default roles - user.append_roles("Projects User", "Stock User", "Support Team") - user.flags.delay_emails = True - - if not frappe.db.get_value("User", email): - user.insert(ignore_permissions=True) - - # create employee - emp = frappe.get_doc({ - "doctype": "Employee", - "employee_name": fullname, - "user_id": email, - "status": "Active", - "company": defaults.get("company") - }) - emp.flags.ignore_mandatory = True - emp.insert(ignore_permissions = True) - -# Enumerate the setup hooks you're going to need, apart from the slides From e2bb82872caece74123073bacd450203acfb5967 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 26 Nov 2019 18:46:17 +0530 Subject: [PATCH 279/679] fix: moved slide json files to respective modules --- .../add_a_few_suppliers.json | 49 ++++++++++++++++ .../add_a_few_customers.json | 49 ++++++++++++++++ .../welcome_to_erpnext!.json | 22 +++++++ .../add_a_few_products_you_buy_or_sell.json | 57 +++++++++++++++++++ 4 files changed, 177 insertions(+) create mode 100644 erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json create mode 100644 erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json create mode 100644 erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json create mode 100644 erpnext/stock/setup_wizard_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json diff --git a/erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json b/erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json new file mode 100644 index 00000000000..006d139eb02 --- /dev/null +++ b/erpnext/buying/setup_wizard_slide/add_a_few_suppliers/add_a_few_suppliers.json @@ -0,0 +1,49 @@ +{ + "add_more_button": 1, + "app": "ERPNext", + "creation": "2019-11-15 14:45:32.626641", + "docstatus": 0, + "doctype": "Setup Wizard Slide", + "domains": [], + "help_links": [ + { + "label": "Supplier", + "video_id": "zsrrVDk6VBs" + } + ], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/supplier.png", + "max_count": 3, + "modified": "2019-11-26 18:26:25.498325", + "modified_by": "Administrator", + "name": "Add A Few Suppliers", + "owner": "Administrator", + "ref_doctype": "Supplier", + "slide_desc": "", + "slide_fields": [ + { + "align": "", + "fieldname": "supplier_name", + "fieldtype": "Data", + "label": "Supplier Name", + "placeholder": "", + "reqd": 1 + }, + { + "align": "", + "fieldtype": "Column Break", + "reqd": 0 + }, + { + "align": "", + "fieldname": "supplier_email", + "fieldtype": "Data", + "label": "Supplier Email", + "reqd": 1 + } + ], + "slide_order": 50, + "slide_title": "Add A Few Suppliers", + "slide_type": "Create", + "submit_method": "" +} \ No newline at end of file diff --git a/erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json b/erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json new file mode 100644 index 00000000000..a0bb6fe26df --- /dev/null +++ b/erpnext/selling/setup_wizard_slide/add_a_few_customers/add_a_few_customers.json @@ -0,0 +1,49 @@ +{ + "add_more_button": 1, + "app": "ERPNext", + "creation": "2019-11-15 14:44:10.065014", + "docstatus": 0, + "doctype": "Setup Wizard Slide", + "domains": [], + "help_links": [ + { + "label": "Customers", + "video_id": "zsrrVDk6VBs" + } + ], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/customer.png", + "max_count": 3, + "modified": "2019-11-26 18:26:15.888794", + "modified_by": "Administrator", + "name": "Add A Few Customers", + "owner": "Administrator", + "ref_doctype": "Customer", + "slide_desc": "", + "slide_fields": [ + { + "align": "", + "fieldname": "customer_name", + "fieldtype": "Data", + "label": "Customer Name", + "placeholder": "", + "reqd": 1 + }, + { + "align": "", + "fieldtype": "Column Break", + "reqd": 0 + }, + { + "align": "", + "fieldname": "customer_email", + "fieldtype": "Data", + "label": "Email ID", + "reqd": 1 + } + ], + "slide_order": 40, + "slide_title": "Add A Few Customers", + "slide_type": "Create", + "submit_method": "" +} \ No newline at end of file diff --git a/erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json b/erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json new file mode 100644 index 00000000000..1da9dd44e2b --- /dev/null +++ b/erpnext/setup/setup_wizard_slide/welcome_to_erpnext!/welcome_to_erpnext!.json @@ -0,0 +1,22 @@ +{ + "add_more_button": 0, + "app": "ERPNext", + "creation": "2019-11-26 17:01:26.671859", + "docstatus": 0, + "doctype": "Setup Wizard Slide", + "domains": [], + "help_links": [], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/onboard.png", + "max_count": 0, + "modified": "2019-11-26 17:17:29.813299", + "modified_by": "Administrator", + "name": "Welcome to ERPNext!", + "owner": "Administrator", + "slide_desc": "Setting up an ERP can be overwhelming. But don't worry, we have got your back!
\nLet's setup your company.\nThis wizard will help you onboard to ERPNext in a short time!", + "slide_fields": [], + "slide_module": "Setup", + "slide_order": 10, + "slide_title": "Welcome to ERPNext!", + "slide_type": "Information" +} \ No newline at end of file diff --git a/erpnext/stock/setup_wizard_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json b/erpnext/stock/setup_wizard_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json new file mode 100644 index 00000000000..c536f7b2cab --- /dev/null +++ b/erpnext/stock/setup_wizard_slide/add_a_few_products_you_buy_or_sell/add_a_few_products_you_buy_or_sell.json @@ -0,0 +1,57 @@ +{ + "add_more_button": 1, + "app": "ERPNext", + "creation": "2019-11-15 14:41:12.007359", + "docstatus": 0, + "doctype": "Setup Wizard Slide", + "domains": [], + "help_links": [], + "idx": 0, + "image_src": "/assets/erpnext/images/illustrations/product.png", + "max_count": 3, + "modified": "2019-11-26 18:26:35.305755", + "modified_by": "Administrator", + "name": "Add A Few Products You Buy Or Sell", + "owner": "Administrator", + "ref_doctype": "Item", + "slide_desc": "", + "slide_fields": [ + { + "align": "", + "fieldname": "item", + "fieldtype": "Data", + "label": "Item", + "placeholder": "Product Name", + "reqd": 1 + }, + { + "align": "", + "fieldtype": "Column Break", + "reqd": 1 + }, + { + "align": "", + "fieldname": "uom", + "fieldtype": "Link", + "label": "UOM", + "options": "UOM", + "reqd": 1 + }, + { + "align": "", + "fieldtype": "Column Break", + "reqd": 0 + }, + { + "align": "", + "fieldname": "item_price", + "fieldtype": "Currency", + "label": "Item Price", + "reqd": 1 + } + ], + "slide_order": 30, + "slide_title": "Add A Few Products You Buy Or Sell", + "slide_type": "Create", + "submit_method": "" +} \ No newline at end of file From 7331964803f7b36d760a3015fa5f00fd0d54d1e1 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 27 Nov 2019 14:53:58 +0530 Subject: [PATCH 280/679] fix(travis): add lib cups for updated frappe requirements --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 40afeee8d46..365eb67f3dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -63,6 +63,7 @@ install: - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf - sudo chmod o+x /usr/local/bin/wkhtmltopdf + - sudo apt-get install libcups2-dev - cd ~/frappe-bench From f0e87f7fdd8b398b23f2cb50d893b28a1bb3ed93 Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 27 Nov 2019 15:49:41 +0530 Subject: [PATCH 281/679] fix: get_batch_qty_and_serial_no() requires argument 'stock_qty' (#19693) --- 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 707bb2052c4aed8a18e2e0151fe04577a613fbd0 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 27 Nov 2019 16:27:50 +0530 Subject: [PATCH 282/679] fix: Employee dashboard disappear after switching from leave application form (#19690) --- erpnext/hr/doctype/leave_application/leave_application.js | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index 174641048b8..db3819eff2d 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -60,7 +60,6 @@ frappe.ui.form.on("Leave Application", { } } }); - $("div").remove(".form-dashboard-section"); frm.dashboard.add_section( frappe.render_template('leave_application_dashboard', { data: leave_details From c7cfc726d7ba28cc2d78668de7e019222a1882c9 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 27 Nov 2019 16:58:06 +0530 Subject: [PATCH 283/679] feat: navigate to stock ledger from batch report --- .../batch_wise_balance_history.js | 25 +++- .../batch_wise_balance_history.py | 37 +++--- .../stock/report/stock_ledger/stock_ledger.js | 10 +- .../stock/report/stock_ledger/stock_ledger.py | 110 +++++++++++++----- 4 files changed, 133 insertions(+), 49 deletions(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index b23c908e07a..23700c94ee5 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -16,6 +16,29 @@ frappe.query_reports["Batch-Wise Balance History"] = { "fieldtype": "Date", "width": "80", "default": frappe.datetime.get_today() + }, + { + "fieldname": "item", + "label": __("Item"), + "fieldtype": "Link", + "options": "Item", + "width": "80" } - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + if (column.fieldname == "Batch" && data && !!data["Batch"]) { + value = data["Batch"]; + column.link_onclick = "frappe.query_reports['Batch-Wise Balance History'].set_batch_route_to_stock_ledger(" + JSON.stringify(data) + ")"; + } + + value = default_formatter(value, row, column, data); + return value; + }, + "set_batch_route_to_stock_ledger": function (data) { + frappe.route_options = { + "batch_no": data["Batch"] + }; + + frappe.set_route("query-report", "Stock Ledger"); + } } \ No newline at end of file diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 7f7835f74ee..2c95084b813 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -2,9 +2,11 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals + import frappe from frappe import _ -from frappe.utils import flt, cint, getdate +from frappe.utils import cint, flt, getdate + def execute(filters=None): if not filters: filters = {} @@ -17,29 +19,31 @@ def execute(filters=None): data = [] for item in sorted(iwb_map): - for wh in sorted(iwb_map[item]): - for batch in sorted(iwb_map[item][wh]): - qty_dict = iwb_map[item][wh][batch] - if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty: - data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, - flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision), - flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision), - item_map[item]["stock_uom"] - ]) + if not filters.get("item") or filters.get("item") == item: + for wh in sorted(iwb_map[item]): + for batch in sorted(iwb_map[item][wh]): + qty_dict = iwb_map[item][wh][batch] + if qty_dict.opening_qty or qty_dict.in_qty or qty_dict.out_qty or qty_dict.bal_qty: + data.append([item, item_map[item]["item_name"], item_map[item]["description"], wh, batch, + flt(qty_dict.opening_qty, float_precision), flt(qty_dict.in_qty, float_precision), + flt(qty_dict.out_qty, float_precision), flt(qty_dict.bal_qty, float_precision), + item_map[item]["stock_uom"] + ]) return columns, data + def get_columns(filters): """return columns based on filters""" columns = [_("Item") + ":Link/Item:100"] + [_("Item Name") + "::150"] + [_("Description") + "::150"] + \ - [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \ - [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \ - [_("UOM") + "::90"] - + [_("Warehouse") + ":Link/Warehouse:100"] + [_("Batch") + ":Link/Batch:100"] + [_("Opening Qty") + ":Float:90"] + \ + [_("In Qty") + ":Float:80"] + [_("Out Qty") + ":Float:80"] + [_("Balance Qty") + ":Float:90"] + \ + [_("UOM") + "::90"] return columns + def get_conditions(filters): conditions = "" if not filters.get("from_date"): @@ -52,7 +56,8 @@ def get_conditions(filters): return conditions -#get all details + +# get all details def get_stock_ledger_entries(filters): conditions = get_conditions(filters) return frappe.db.sql(""" @@ -63,6 +68,7 @@ def get_stock_ledger_entries(filters): order by item_code, warehouse""" % conditions, as_dict=1) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) iwb_map = {} @@ -90,6 +96,7 @@ def get_item_warehouse_batch_map(filters, float_precision): return iwb_map + def get_item_details(filters): item_map = {} for d in frappe.db.sql("select name, item_name, description, stock_uom from tabItem", as_dict=1): diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 3fab3273b9e..df3bba5e406 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -77,7 +77,15 @@ frappe.query_reports["Stock Ledger"] = { "fieldtype": "Link", "options": "UOM" } - ] + ], + "formatter": function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); + if (column.fieldname == "out_qty" && data.out_qty < 0) { + value = "" + value + ""; + } + + return value; + }, } // $(function() { diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index db7f6ad1b9c..dd53a006b52 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -2,9 +2,11 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals + import frappe -from frappe import _ from erpnext.stock.utils import update_included_uom_in_report +from frappe import _ + def execute(filters=None): include_uom = filters.get("include_uom") @@ -36,7 +38,22 @@ def execute(filters=None): sle.update({ "qty_after_transaction": actual_qty, - "stock_value": stock_value + "stock_value": stock_value, + "in_qty": max(sle.actual_qty, 0), + "out_qty": min(sle.actual_qty, 0) + }) + + # get the name of the item that was produced using this item + if sle.voucher_type == "Stock Entry": + purpose, work_order, fg_completed_qty = frappe.db.get_value(sle.voucher_type, sle.voucher_no, ["purpose", "work_order", "fg_completed_qty"]) + + if purpose == "Manufacture" and work_order: + finished_product = frappe.db.get_value("Work Order", work_order, "item_name") + finished_qty = fg_completed_qty + + sle.update({ + "finished_product": finished_product, + "finished_qty": finished_qty, }) data.append(sle) @@ -47,53 +64,74 @@ def execute(filters=None): update_included_uom_in_report(columns, data, include_uom, conversion_factors) return columns, data + def get_columns(): columns = [ - {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95}, - {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 130}, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 150}, + {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, {"label": _("Item Name"), "fieldname": "item_name", "width": 100}, + {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 90}, + {"label": _("In Qty"), "fieldname": "in_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + {"label": _("Out Qty"), "fieldname": "out_qty", "fieldtype": "Float", "width": 80, "convertible": "qty"}, + {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"}, + {"label": _("Finished Product"), "fieldname": "finished_product", "width": 100}, + {"label": _("Finished Qty"), "fieldname": "finished_qty", "fieldtype": "Float", "width": 100, "convertible": "qty"}, + {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 150}, + {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 150}, {"label": _("Item Group"), "fieldname": "item_group", "fieldtype": "Link", "options": "Item Group", "width": 100}, {"label": _("Brand"), "fieldname": "brand", "fieldtype": "Link", "options": "Brand", "width": 100}, {"label": _("Description"), "fieldname": "description", "width": 200}, - {"label": _("Warehouse"), "fieldname": "warehouse", "fieldtype": "Link", "options": "Warehouse", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", "fieldtype": "Link", "options": "UOM", "width": 100}, - {"label": _("Qty"), "fieldname": "actual_qty", "fieldtype": "Float", "width": 50, "convertible": "qty"}, - {"label": _("Balance Qty"), "fieldname": "qty_after_transaction", "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, - "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, - "options": "Company:company:default_currency", "convertible": "rate"}, - {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, - "options": "Company:company:default_currency"}, + {"label": _("Incoming Rate"), "fieldname": "incoming_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, + {"label": _("Valuation Rate"), "fieldname": "valuation_rate", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency", "convertible": "rate"}, + {"label": _("Balance Value"), "fieldname": "stock_value", "fieldtype": "Currency", "width": 110, "options": "Company:company:default_currency"}, {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110}, {"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100}, {"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100}, - {"label": _("Serial #"), "fieldname": "serial_no", "width": 100}, + {"label": _("Serial #"), "fieldname": "serial_no", "fieldtype": "Link", "options": "Serial No", "width": 100}, {"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100}, {"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110} ] return columns + def get_stock_ledger_entries(filters, items): item_conditions_sql = '' if items: item_conditions_sql = 'and sle.item_code in ({})'\ .format(', '.join([frappe.db.escape(i) for i in items])) - return frappe.db.sql("""select concat_ws(" ", posting_date, posting_time) as date, - item_code, warehouse, actual_qty, qty_after_transaction, incoming_rate, valuation_rate, - stock_value, voucher_type, voucher_no, batch_no, serial_no, company, project, stock_value_difference - from `tabStock Ledger Entry` sle - where company = %(company)s and - posting_date between %(from_date)s and %(to_date)s - {sle_conditions} - {item_conditions_sql} - order by posting_date asc, posting_time asc, creation asc"""\ - .format( - sle_conditions=get_sle_conditions(filters), - item_conditions_sql = item_conditions_sql - ), filters, as_dict=1) + sl_entries = frappe.db.sql(""" + SELECT + concat_ws(" ", posting_date, posting_time) AS date, + item_code, + warehouse, + actual_qty, + qty_after_transaction, + incoming_rate, + valuation_rate, + stock_value, + voucher_type, + voucher_no, + batch_no, + serial_no, + company, + project, + stock_value_difference + FROM + `tabStock Ledger Entry` sle + WHERE + company = %(company)s + AND posting_date BETWEEN %(from_date)s AND %(to_date)s + {sle_conditions} + {item_conditions_sql} + ORDER BY + posting_date asc, posting_time asc, creation asc + """.format(sle_conditions=get_sle_conditions(filters), item_conditions_sql=item_conditions_sql), + filters, as_dict=1) + + return sl_entries + def get_items(filters): conditions = [] @@ -111,6 +149,7 @@ def get_items(filters): .format(" and ".join(conditions)), filters) return items + def get_item_details(items, sl_entries, include_uom): item_details = {} if not items: @@ -140,6 +179,7 @@ def get_item_details(items, sl_entries, include_uom): return item_details + def get_sle_conditions(filters): conditions = [] if filters.get("warehouse"): @@ -155,6 +195,7 @@ def get_sle_conditions(filters): return "and {}".format(" and ".join(conditions)) if conditions else "" + def get_opening_balance(filters, columns): if not (filters.item_code and filters.warehouse and filters.from_date): return @@ -166,13 +207,17 @@ def get_opening_balance(filters, columns): "posting_date": filters.from_date, "posting_time": "00:00:00" }) - row = {} - row["item_code"] = _("'Opening'") - for dummy, v in ((9, 'qty_after_transaction'), (11, 'valuation_rate'), (12, 'stock_value')): - row[v] = last_entry.get(v, 0) + + row = { + "item_code": _("'Opening'"), + "qty_after_transaction": last_entry.get("qty_after_transaction", 0), + "valuation_rate": last_entry.get("valuation_rate", 0), + "stock_value": last_entry.get("stock_value", 0) + } return row + def get_warehouse_condition(warehouse): warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) if warehouse_details: @@ -182,6 +227,7 @@ def get_warehouse_condition(warehouse): return '' + def get_item_group_condition(item_group): item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1) if item_group_details: From 2fe0c98772435ad5bd00cafe5c7cb960c491f6e9 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 28 Nov 2019 08:09:47 +0530 Subject: [PATCH 284/679] 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 312210c2b0bf2dff44c006b26ca1866ae2fe2b5b Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Tue, 13 Aug 2019 17:15:44 +0530 Subject: [PATCH 285/679] feat: create address and contact after lead creation --- erpnext/crm/doctype/lead/lead.js | 55 +++++++------- erpnext/crm/doctype/lead/lead.json | 95 +++++++++++++++++------- erpnext/crm/doctype/lead/lead.py | 115 +++++++++++++++++++++++------ 3 files changed, 187 insertions(+), 78 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js index 122e2b4eee8..0c88d2826f7 100644 --- a/erpnext/crm/doctype/lead/lead.js +++ b/erpnext/crm/doctype/lead/lead.js @@ -1,4 +1,4 @@ -// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt frappe.provide("erpnext"); @@ -7,57 +7,54 @@ cur_frm.email_field = "email_id"; erpnext.LeadController = frappe.ui.form.Controller.extend({ setup: function () { this.frm.make_methods = { + 'Customer': this.make_customer, 'Quotation': this.make_quotation, - 'Opportunity': this.create_opportunity - } - - this.frm.fields_dict.customer.get_query = function (doc, cdt, cdn) { - return { query: "erpnext.controllers.queries.customer_query" } - } + 'Opportunity': this.make_opportunity + }; this.frm.toggle_reqd("lead_name", !this.frm.doc.organization_lead); }, onload: function () { - if (cur_frm.fields_dict.lead_owner.df.options.match(/^User/)) { - cur_frm.fields_dict.lead_owner.get_query = function (doc, cdt, cdn) { - return { query: "frappe.core.doctype.user.user.user_query" } - } - } + this.frm.set_query("customer", function (doc, cdt, cdn) { + return { query: "erpnext.controllers.queries.customer_query" } + }); - if (cur_frm.fields_dict.contact_by.df.options.match(/^User/)) { - cur_frm.fields_dict.contact_by.get_query = function (doc, cdt, cdn) { - return { query: "frappe.core.doctype.user.user.user_query" } - } - } + this.frm.set_query("lead_owner", function (doc, cdt, cdn) { + return { query: "frappe.core.doctype.user.user.user_query" } + }); + + this.frm.set_query("contact_by", function (doc, cdt, cdn) { + return { query: "frappe.core.doctype.user.user.user_query" } + }); }, refresh: function () { - var doc = this.frm.doc; + let doc = this.frm.doc; erpnext.toggle_naming_series(); frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' } - if(!doc.__islocal && doc.__onload && !doc.__onload.is_customer) { - this.frm.add_custom_button(__("Customer"), this.create_customer, __('Create')); - this.frm.add_custom_button(__("Opportunity"), this.create_opportunity, __('Create')); - this.frm.add_custom_button(__("Quotation"), this.make_quotation, __('Create')); + if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) { + this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create")); + this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create")); + this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create")); } - if (!this.frm.doc.__islocal) { - frappe.contacts.render_address_and_contact(cur_frm); + if (!this.frm.is_new()) { + frappe.contacts.render_address_and_contact(this.frm); } else { - frappe.contacts.clear_address_and_contact(cur_frm); + frappe.contacts.clear_address_and_contact(this.frm); } }, - create_customer: function () { + make_customer: function () { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_customer", frm: cur_frm }) }, - create_opportunity: function () { + make_opportunity: function () { frappe.model.open_mapped_doc({ method: "erpnext.crm.doctype.lead.lead.make_opportunity", frm: cur_frm @@ -77,7 +74,7 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({ }, company_name: function () { - if (this.frm.doc.organization_lead == 1) { + if (this.frm.doc.organization_lead && !this.frm.doc.lead_name) { this.frm.set_value("lead_name", this.frm.doc.company_name); } }, @@ -85,7 +82,7 @@ erpnext.LeadController = frappe.ui.form.Controller.extend({ contact_date: function () { if (this.frm.doc.contact_date) { let d = moment(this.frm.doc.contact_date); - d.add(1, "hours"); + d.add(1, "day"); this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat)); } } diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index eb68c679ba5..d2a98b609ac 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -16,6 +16,7 @@ "col_break123", "lead_owner", "status", + "salutation", "gender", "source", "customer", @@ -29,16 +30,20 @@ "notes_section", "notes", "contact_info", - "address_desc", "address_html", + "address_title", + "address_line1", + "address_line2", + "city", + "county", + "state", + "country", + "pincode", "column_break2", "contact_html", "phone", - "salutation", "mobile_no", "fax", - "website", - "territory", "more_info", "type", "market_segment", @@ -46,6 +51,8 @@ "request_type", "column_break3", "company", + "website", + "territory", "unsubscribed", "blog_subscriber" ], @@ -73,7 +80,6 @@ "set_only_once": 1 }, { - "depends_on": "eval:!doc.organization_lead", "fieldname": "lead_name", "fieldtype": "Data", "in_global_search": 1, @@ -130,7 +136,6 @@ "search_index": 1 }, { - "depends_on": "eval:!doc.organization_lead", "fieldname": "gender", "fieldtype": "Link", "label": "Gender", @@ -218,19 +223,13 @@ }, { "collapsible": 1, + "collapsible_depends_on": "eval: doc.__islocal", "fieldname": "contact_info", "fieldtype": "Section Break", "label": "Address & Contact", "oldfieldtype": "Column Break", "options": "fa fa-map-marker" }, - { - "depends_on": "eval:doc.__islocal", - "fieldname": "address_desc", - "fieldtype": "HTML", - "label": "Address Desc", - "print_hide": 1 - }, { "fieldname": "address_html", "fieldtype": "HTML", @@ -242,14 +241,13 @@ "fieldtype": "Column Break" }, { - "depends_on": "eval:doc.organization_lead", "fieldname": "contact_html", "fieldtype": "HTML", "label": "Contact HTML", "read_only": 1 }, { - "depends_on": "eval:!doc.organization_lead", + "depends_on": "eval: doc.__islocal", "fieldname": "phone", "fieldtype": "Data", "label": "Phone", @@ -257,14 +255,14 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.organization_lead", + "depends_on": "eval: doc.__islocal", "fieldname": "salutation", "fieldtype": "Link", "label": "Salutation", "options": "Salutation" }, { - "depends_on": "eval:!doc.organization_lead", + "depends_on": "eval: doc.__islocal", "fieldname": "mobile_no", "fieldtype": "Data", "label": "Mobile No.", @@ -272,7 +270,7 @@ "oldfieldtype": "Data" }, { - "depends_on": "eval:!doc.organization_lead", + "depends_on": "eval: doc.__islocal", "fieldname": "fax", "fieldtype": "Data", "label": "Fax", @@ -361,12 +359,62 @@ "fieldname": "blog_subscriber", "fieldtype": "Check", "label": "Blog Subscriber" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "address_title", + "fieldtype": "Data", + "label": "Address Title" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "address_line1", + "fieldtype": "Data", + "label": "Address Line 1" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "address_line2", + "fieldtype": "Data", + "label": "Address Line 2" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "city", + "fieldtype": "Data", + "label": "City/Town" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "county", + "fieldtype": "Data", + "label": "County" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "pincode", + "fieldtype": "Data", + "label": "Postal Code", + "options": "Country" } ], "icon": "fa fa-user", "idx": 5, "image_field": "image", - "modified": "2019-09-19 12:49:02.536647", + "modified": "2019-09-20 12:49:02.536647", "modified_by": "Administrator", "module": "CRM", "name": "Lead", @@ -423,15 +471,6 @@ "read": 1, "report": 1, "role": "Sales User" - }, - { - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Guest", - "share": 1 } ], "search_fields": "lead_name,lead_owner,status", diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 1dae4b9fc1c..cc2badf9848 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -2,18 +2,19 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.utils import (cstr, validate_email_address, cint, comma_and, has_gravatar, now, getdate, nowdate) -from frappe.model.mapper import get_mapped_doc -from erpnext.controllers.selling_controller import SellingController -from frappe.contacts.address_and_contact import load_address_and_contact +import frappe from erpnext.accounts.party import set_taxes +from erpnext.controllers.selling_controller import SellingController +from frappe import _ +from frappe.contacts.address_and_contact import load_address_and_contact from frappe.email.inbox import link_communication_to_document +from frappe.model.mapper import get_mapped_doc +from frappe.utils import cint, comma_and, cstr, getdate, has_gravatar, nowdate, validate_email_address sender_field = "email_id" + class Lead(SellingController): def get_feed(self): return '{0}: {1}'.format(_(self.status), self.lead_name) @@ -23,15 +24,22 @@ class Lead(SellingController): self.get("__onload").is_customer = customer load_address_and_contact(self) + def before_insert(self): + self.address_doc = self.create_address() + self.contact_doc = self.create_contact() + + def after_insert(self): + self.update_links() + # after the address and contact are created, flush the field values + # to avoid inconsistent reporting in case the documents are changed + self.flush_address_and_contact_fields() + def validate(self): self.set_lead_name() self._prev = frappe._dict({ - "contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if \ - (not cint(self.get("__islocal"))) else None, - "ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if \ - (not cint(self.get("__islocal"))) else None, - "contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if \ - (not cint(self.get("__islocal"))) else None, + "contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if (not cint(self.is_new())) else None, + "ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if (not cint(self.is_new())) else None, + "contact_by": frappe.db.get_value("Lead", self.name, "contact_by") if (not cint(self.is_new())) else None, }) self.set_status() @@ -39,7 +47,7 @@ class Lead(SellingController): if self.email_id: if not self.flags.ignore_email_validation: - validate_email_address(self.email_id, True) + validate_email_address(self.email_id, throw=True) if self.email_id == self.lead_owner: frappe.throw(_("Lead Owner cannot be same as the Lead")) @@ -53,8 +61,7 @@ class Lead(SellingController): if self.contact_date and getdate(self.contact_date) < getdate(nowdate()): frappe.throw(_("Next Contact Date cannot be in the past")) - if self.ends_on and self.contact_date and\ - (self.ends_on < self.contact_date): + if self.ends_on and self.contact_date and (self.ends_on < self.contact_date): frappe.throw(_("Ends On date cannot be before Next Contact Date.")) def on_update(self): @@ -66,8 +73,7 @@ class Lead(SellingController): "starts_on": self.contact_date, "ends_on": self.ends_on or "", "subject": ('Contact ' + cstr(self.lead_name)), - "description": ('Contact ' + cstr(self.lead_name)) + \ - (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') + "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') }, force) def check_email_id_is_unique(self): @@ -81,8 +87,7 @@ class Lead(SellingController): .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) def on_trash(self): - frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", - self.name) + frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) self.delete_events() @@ -115,10 +120,74 @@ class Lead(SellingController): self.lead_name = self.company_name + def create_address(self): + address_fields = ["address_title", "address_line1", "address_line2", + "city", "county", "state", "country", "pincode"] + info_fields = ["email_id", "phone", "fax"] + + # do not create an address if no fields are available, + # skipping country since the system auto-sets it from system defaults + if not any([self.get(field) for field in address_fields if field != "country"]): + return + + address = frappe.new_doc("Address") + address.update({addr_field: self.get(addr_field) for addr_field in address_fields}) + address.update({info_field: self.get(info_field) for info_field in info_fields}) + address.insert() + + return address + + def create_contact(self): + names = self.lead_name.split(" ") + if len(names) > 1: + first_name, last_name = names[0], " ".join(names[1:]) + else: + first_name, last_name = self.lead_name, None + + contact_fields = ["email_id", "salutation", "gender", "phone", "mobile_no"] + + contact = frappe.new_doc("Contact") + contact.update({contact_field: self.get(contact_field) for contact_field in contact_fields}) + contact.update({ + "first_name": first_name, + "last_name": last_name + }) + contact.insert() + + return contact + + def update_links(self): + # update address links + if self.address_doc: + self.address_doc.append("links", { + "link_doctype": "Lead", + "link_name": self.name, + "link_title": self.lead_name + }) + self.address_doc.save() + + # update contact links + if self.contact_doc: + self.contact_doc.append("links", { + "link_doctype": "Lead", + "link_name": self.name, + "link_title": self.lead_name + }) + self.contact_doc.save() + + def flush_address_and_contact_fields(self): + fields = ['address_line1', 'address_line2', 'address_title', 'city', 'country', + 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'salutation', 'state'] + + for field in fields: + self.set(field, None) + + @frappe.whitelist() def make_customer(source_name, target_doc=None): return _make_customer(source_name, target_doc) + def _make_customer(source_name, target_doc=None, ignore_permissions=False): def set_missing_values(source, target): if source.company_name: @@ -143,6 +212,7 @@ def _make_customer(source_name, target_doc=None, ignore_permissions=False): return doclist + @frappe.whitelist() def make_opportunity(source_name, target_doc=None): def set_missing_values(source, target): @@ -164,6 +234,7 @@ def make_opportunity(source_name, target_doc=None): return target_doc + @frappe.whitelist() def make_quotation(source_name, target_doc=None): def set_missing_values(source, target): @@ -205,7 +276,8 @@ def _set_missing_values(source, target): @frappe.whitelist() def get_lead_details(lead, posting_date=None, company=None): - if not lead: return {} + if not lead: + return {} from erpnext.accounts.party import set_address_details out = frappe._dict() @@ -231,6 +303,7 @@ def get_lead_details(lead, posting_date=None, company=None): return out + @frappe.whitelist() def make_lead_from_communication(communication, ignore_communication_links=False): """ raise a issue from email """ @@ -267,4 +340,4 @@ def get_lead_with_phone_number(number): lead = leads[0].name if leads else None - return lead \ No newline at end of file + return lead From f9e2bfcc2940c90868941cfd84548f8e301c4d49 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Thu, 22 Aug 2019 15:03:34 +0530 Subject: [PATCH 286/679] fix: conditionally set lead title as organization or person --- erpnext/crm/doctype/lead/lead.json | 12 ++++++++++-- erpnext/crm/doctype/lead/lead.py | 7 +++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index d2a98b609ac..88a562f720b 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -54,7 +54,8 @@ "website", "territory", "unsubscribed", - "blog_subscriber" + "blog_subscriber", + "title" ], "fields": [ { @@ -409,6 +410,13 @@ "fieldtype": "Data", "label": "Postal Code", "options": "Country" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "print_hide": 1 } ], "icon": "fa fa-user", @@ -477,5 +485,5 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", - "title_field": "lead_name" + "title_field": "title" } \ No newline at end of file diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index cc2badf9848..9e5fdc0e120 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -36,6 +36,7 @@ class Lead(SellingController): def validate(self): self.set_lead_name() + self.set_title() self._prev = frappe._dict({ "contact_date": frappe.db.get_value("Lead", self.name, "contact_date") if (not cint(self.is_new())) else None, "ends_on": frappe.db.get_value("Lead", self.name, "ends_on") if (not cint(self.is_new())) else None, @@ -120,6 +121,12 @@ class Lead(SellingController): self.lead_name = self.company_name + def set_title(self): + if self.organization_lead: + self.title = self.company_name + else: + self.title = self.lead_name + def create_address(self): address_fields = ["address_title", "address_line1", "address_line2", "city", "county", "state", "country", "pincode"] From 75da5af900cb49e509a63433e89830cbb58bd561 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Thu, 22 Aug 2019 15:38:44 +0530 Subject: [PATCH 287/679] fix: set missing values --- erpnext/crm/doctype/lead/lead.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index 9e5fdc0e120..bd0c742aa2d 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -145,6 +145,9 @@ class Lead(SellingController): return address def create_contact(self): + if not self.lead_name: + self.set_lead_name() + names = self.lead_name.split(" ") if len(names) > 1: first_name, last_name = names[0], " ".join(names[1:]) From 69e3868a9dfcaf02c61defac58df8d39cbf1cb51 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Fri, 6 Sep 2019 13:38:15 +0530 Subject: [PATCH 288/679] patch: set title for old leads --- erpnext/patches.txt | 3 ++- erpnext/patches/v12_0/set_lead_title_field.py | 11 +++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/set_lead_title_field.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 07b646b0f82..0495c027523 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.set_lead_title_field diff --git a/erpnext/patches/v12_0/set_lead_title_field.py b/erpnext/patches/v12_0/set_lead_title_field.py new file mode 100644 index 00000000000..86e00038f6c --- /dev/null +++ b/erpnext/patches/v12_0/set_lead_title_field.py @@ -0,0 +1,11 @@ +import frappe + + +def execute(): + frappe.reload_doc("crm", "doctype", "lead") + frappe.db.sql(""" + UPDATE + `tabLead` + SET + title = IF(organization_lead = 1, company_name, lead_name) + """) From fd46fef857b578515db6d12245d8f01895dc00b1 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Fri, 6 Sep 2019 14:10:20 +0530 Subject: [PATCH 289/679] fix: add designation to Lead --- erpnext/crm/doctype/lead/lead.json | 153 +++++++++++++++-------------- erpnext/crm/doctype/lead/lead.py | 4 +- 2 files changed, 82 insertions(+), 75 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 88a562f720b..c8e9fbc463e 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -17,6 +17,7 @@ "lead_owner", "status", "salutation", + "designation", "gender", "source", "customer", @@ -136,6 +137,13 @@ "reqd": 1, "search_index": 1 }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "salutation", + "fieldtype": "Link", + "label": "Salutation", + "options": "Salutation" + }, { "fieldname": "gender", "fieldtype": "Link", @@ -237,6 +245,56 @@ "label": "Address HTML", "read_only": 1 }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "address_title", + "fieldtype": "Data", + "label": "Address Title" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "address_line1", + "fieldtype": "Data", + "label": "Address Line 1" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "address_line2", + "fieldtype": "Data", + "label": "Address Line 2" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "city", + "fieldtype": "Data", + "label": "City/Town" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "county", + "fieldtype": "Data", + "label": "County" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "state", + "fieldtype": "Data", + "label": "State" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "country", + "fieldtype": "Link", + "label": "Country", + "options": "Country" + }, + { + "depends_on": "eval: doc.__islocal", + "fieldname": "pincode", + "fieldtype": "Data", + "label": "Postal Code", + "options": "Country" + }, { "fieldname": "column_break2", "fieldtype": "Column Break" @@ -255,13 +313,6 @@ "oldfieldname": "contact_no", "oldfieldtype": "Data" }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "salutation", - "fieldtype": "Link", - "label": "Salutation", - "options": "Salutation" - }, { "depends_on": "eval: doc.__islocal", "fieldname": "mobile_no", @@ -278,22 +329,6 @@ "oldfieldname": "fax", "oldfieldtype": "Data" }, - { - "fieldname": "website", - "fieldtype": "Data", - "label": "Website", - "oldfieldname": "website", - "oldfieldtype": "Data" - }, - { - "fieldname": "territory", - "fieldtype": "Link", - "label": "Territory", - "oldfieldname": "territory", - "oldfieldtype": "Link", - "options": "Territory", - "print_hide": 1 - }, { "collapsible": 1, "fieldname": "more_info", @@ -349,6 +384,22 @@ "options": "Company", "remember_last_selected_value": 1 }, + { + "fieldname": "website", + "fieldtype": "Data", + "label": "Website", + "oldfieldname": "website", + "oldfieldtype": "Data" + }, + { + "fieldname": "territory", + "fieldtype": "Link", + "label": "Territory", + "oldfieldname": "territory", + "oldfieldtype": "Link", + "options": "Territory", + "print_hide": 1 + }, { "default": "0", "fieldname": "unsubscribed", @@ -361,62 +412,18 @@ "fieldtype": "Check", "label": "Blog Subscriber" }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "address_title", - "fieldtype": "Data", - "label": "Address Title" - }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "address_line1", - "fieldtype": "Data", - "label": "Address Line 1" - }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "address_line2", - "fieldtype": "Data", - "label": "Address Line 2" - }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "city", - "fieldtype": "Data", - "label": "City/Town" - }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "county", - "fieldtype": "Data", - "label": "County" - }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "state", - "fieldtype": "Data", - "label": "State" - }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "country", - "fieldtype": "Link", - "label": "Country", - "options": "Country" - }, - { - "depends_on": "eval: doc.__islocal", - "fieldname": "pincode", - "fieldtype": "Data", - "label": "Postal Code", - "options": "Country" - }, { "fieldname": "title", "fieldtype": "Data", "hidden": 1, "label": "Title", "print_hide": 1 + }, + { + "fieldname": "designation", + "fieldtype": "Link", + "label": "Designation", + "options": "Designation" } ], "icon": "fa fa-user", diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index bd0c742aa2d..c0416092b1a 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -154,7 +154,7 @@ class Lead(SellingController): else: first_name, last_name = self.lead_name, None - contact_fields = ["email_id", "salutation", "gender", "phone", "mobile_no"] + contact_fields = ["email_id", "salutation", "gender", "designation", "phone", "mobile_no"] contact = frappe.new_doc("Contact") contact.update({contact_field: self.get(contact_field) for contact_field in contact_fields}) @@ -187,7 +187,7 @@ class Lead(SellingController): def flush_address_and_contact_fields(self): fields = ['address_line1', 'address_line2', 'address_title', 'city', 'country', - 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'salutation', 'state'] + 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'state'] for field in fields: self.set(field, None) From c59ac36378fb720b6bd8562c004ae53fbed1beb7 Mon Sep 17 00:00:00 2001 From: Rohan Bansal Date: Wed, 18 Sep 2019 13:46:22 +0530 Subject: [PATCH 290/679] fix: use new Contact schema --- erpnext/crm/doctype/lead/lead.json | 9 +++++++++ erpnext/crm/doctype/lead/lead.py | 31 ++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index c8e9fbc463e..2219307caf4 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -486,6 +486,15 @@ "read": 1, "report": 1, "role": "Sales User" + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Guest", + "share": 1 } ], "search_fields": "lead_name,lead_owner,status", diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py index c0416092b1a..6645c4d0019 100644 --- a/erpnext/crm/doctype/lead/lead.py +++ b/erpnext/crm/doctype/lead/lead.py @@ -80,8 +80,8 @@ class Lead(SellingController): def check_email_id_is_unique(self): if self.email_id: # validate email is unique - duplicate_leads = frappe.db.sql_list("""select name from tabLead - where email_id=%s and name!=%s""", (self.email_id, self.name)) + duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) + duplicate_leads = [lead.name for lead in duplicate_leads] if duplicate_leads: frappe.throw(_("Email Address must be unique, already exists for {0}") @@ -154,13 +154,28 @@ class Lead(SellingController): else: first_name, last_name = self.lead_name, None - contact_fields = ["email_id", "salutation", "gender", "designation", "phone", "mobile_no"] - contact = frappe.new_doc("Contact") - contact.update({contact_field: self.get(contact_field) for contact_field in contact_fields}) contact.update({ "first_name": first_name, - "last_name": last_name + "last_name": last_name, + "salutation": self.salutation, + "gender": self.gender, + "designation": self.designation, + "email_ids": [ + { + "email_id": self.email_id, + "is_primary": 1 + } + ], + "phone_nos": [ + { + "phone": self.phone, + "is_primary": 1 + }, + { + "phone": self.mobile_no, + } + ] }) contact.insert() @@ -186,8 +201,8 @@ class Lead(SellingController): self.contact_doc.save() def flush_address_and_contact_fields(self): - fields = ['address_line1', 'address_line2', 'address_title', 'city', 'country', - 'county', 'fax', 'mobile_no', 'phone', 'pincode', 'state'] + fields = ['address_line1', 'address_line2', 'address_title', + 'city', 'county', 'country', 'fax', 'pincode', 'state'] for field in fields: self.set(field, None) From 236140d94ccf4b836933b88023f3c3b70535349b Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 28 Nov 2019 16:36:24 +0530 Subject: [PATCH 291/679] 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 1511154aa894fe83b2a7612e6c527d81550f340e Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 28 Nov 2019 16:43:39 +0530 Subject: [PATCH 292/679] fix: removed stock value and account balance out of sync validation (#19729) * fix: revert value out of sync feature * fix: removed stock value and account balance out of sync validation --- erpnext/accounts/general_ledger.py | 47 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 2ba319d05e5..feb598a2e51 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -162,33 +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: - 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 f51ccbf5d4e85719aa6bb7488977933dc5da0476 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 16:44:46 +0530 Subject: [PATCH 293/679] chore: Added Quick Stock Balance to Stock Module (#19726) - 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 2512962f55e355be4e17ff6ea53efe91c89d921d Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 28 Nov 2019 16:46:58 +0530 Subject: [PATCH 294/679] fix: handle None case for get_shipping_amount_from_rules (#19723) --- 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 aff678e376f4d5d9623b7c80eeb93b8b6e9e50d3 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 18:20:48 +0530 Subject: [PATCH 295/679] fix: Changed type of column 'serial_no' in Stock Ledger Entry (#19702) --- .../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 bcd02069ea2e7ee9d208ee7ac4aef6bcade16e5f Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 18:23:45 +0530 Subject: [PATCH 296/679] fix: Validation for Suppliers in SO to PO (#19686) - 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 707b83940aeaa60289dc8f8f029faf9363234720 Mon Sep 17 00:00:00 2001 From: Saqib Date: Thu, 28 Nov 2019 18:27:17 +0530 Subject: [PATCH 297/679] fix: due date before posting date for items added to cart yesterday (#19681) --- 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 9f3276046ff584843cac3f1a140dbc17f9b45d8e Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 28 Nov 2019 18:29:33 +0530 Subject: [PATCH 298/679] fix: Account type in Handling Difference in Inventory account (#19674) * fix: Account type in Handling Difference in Inventory account * fix: Add Stock Adjustment account * fix: Rename account to stock adjustment --- .../ae_uae_chart_template_standard.json | 435 +++++++++--------- 1 file changed, 218 insertions(+), 217 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json index 8856c8cc90f..a8afb55df6f 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json @@ -1,465 +1,466 @@ { - "country_code": "ae", - "name": "U.A.E - Chart of Accounts", + "country_code": "ae", + "name": "U.A.E - Chart of Accounts", "tree": { "Assets": { "Current Assets": { "Accounts Receivable": { "Corporate Credit Cards": { "account_type": "Receivable" - }, + }, "Other Receivable": { "Accrued Rebates Due from Suppliers": { "account_type": "Receivable" - }, + }, "Accrued Income from Suppliers": { "account_type": "Receivable" - }, + }, "Other Debtors": { "account_type": "Receivable" - }, + }, "account_type": "Receivable" - }, + }, "Post Dated Cheques Received": { "account_type": "Receivable" - }, + }, "Staff Receivable": { "account_type": "Receivable" - }, + }, "Trade Receivable": { "account_type": "Receivable" - }, + }, "Trade in Opening Fees": { "account_type": "Receivable" - }, + }, "account_type": "Receivable" - }, + }, "Cash in Hand & Banks": { "Banks": { - "Bank Margin On LC & LG": {}, - "Banks Blocked Deposits": {}, - "Banks Call Deposit Accounts": {}, + "Bank Margin On LC & LG": {}, + "Banks Blocked Deposits": {}, + "Banks Call Deposit Accounts": {}, "Banks Current Accounts": { "account_type": "Bank" - }, + }, "account_type": "Bank" - }, + }, "Cash in Hand": { "Cash in Safe": { "Main Safe": { "account_type": "Cash" - }, + }, "Main Safe - Foreign Currency": { "account_type": "Cash" } - }, + }, "Petty Cash": { "Petty Cash - Administration": { "account_type": "Cash" - }, + }, "Petty Cash - Others": { "account_type": "Cash" } - }, + }, "account_type": "Cash" - }, + }, "Cash in Transit": { "Credit Cards": { "Gateway Credit Cards": { "account_type": "Bank" - }, + }, "Manual Visa & Master Cards": { "account_type": "Bank" - }, + }, "PayPal Account": { "account_type": "Bank" - }, + }, "Visa & Master Credit Cards": { "account_type": "Bank" } } } - }, + }, "Inventory": { "Consigned Stock": { - "Handling Difference in Inventory": { - "account_type": "Stock Adjustment" - }, + "Handling Difference in Inventory": {}, "Items Delivered to Customs on temporary Base": {} - }, + }, "Stock in Hand": { "account_type": "Stock" } - }, + }, "Preliminary and Preoperating Expenses": { "Preoperating Expenses": {} - }, + }, "Prepayments & Deposits": { "Deposits": { - "Deposit - Office Rent": {}, - "Deposit Others": {}, - "Deposit to Immigration (Visa)": {}, + "Deposit - Office Rent": {}, + "Deposit Others": {}, + "Deposit to Immigration (Visa)": {}, "Deposits - Customs": {} - }, + }, "Prepaid Taxes": { - "Sales Taxes Receivables": {}, + "Sales Taxes Receivables": {}, "Withholding Tax Receivables": {} - }, + }, "Prepayments": { - "Other Prepayments": {}, - "PrePaid Advertisement Expenses": {}, - "Prepaid Bank Guarantee": {}, - "Prepaid Consultancy Fees": {}, - "Prepaid Employees Housing": {}, - "Prepaid Finance charge for Loans": {}, - "Prepaid Legal Fees": {}, - "Prepaid License Fees": {}, - "Prepaid Life Insurance": {}, - "Prepaid Maintenance": {}, - "Prepaid Medical Insurance": {}, - "Prepaid Office Rent": {}, - "Prepaid Other Insurance": {}, - "Prepaid Schooling Fees": {}, - "Prepaid Site Hosting Fees": {}, + "Other Prepayments": {}, + "PrePaid Advertisement Expenses": {}, + "Prepaid Bank Guarantee": {}, + "Prepaid Consultancy Fees": {}, + "Prepaid Employees Housing": {}, + "Prepaid Finance charge for Loans": {}, + "Prepaid Legal Fees": {}, + "Prepaid License Fees": {}, + "Prepaid Life Insurance": {}, + "Prepaid Maintenance": {}, + "Prepaid Medical Insurance": {}, + "Prepaid Office Rent": {}, + "Prepaid Other Insurance": {}, + "Prepaid Schooling Fees": {}, + "Prepaid Site Hosting Fees": {}, "Prepaid Sponsorship Fees": {} } } - }, + }, "Long Term Assets": { "Fixed Assets": { "Accumulated Depreciation": { "Acc. Depreciation of Motor Vehicles": { "account_type": "Accumulated Depreciation" - }, + }, "Acc. Deprn.Computer Hardware & Software": { "account_type": "Accumulated Depreciation" - }, + }, "Acc.Deprn.of Furniture & Office Equipment": { "account_type": "Accumulated Depreciation" - }, + }, "Amortisation on Leasehold Improvement": { "account_type": "Accumulated Depreciation" - }, + }, "account_type": "Accumulated Depreciation" - }, + }, "Fixed Assets (Cost Price)": { "Computer Hardware & Software": { "account_type": "Fixed Asset" - }, + }, "Furniture and Equipment": { "account_type": "Fixed Asset" - }, - "Leasehold Improvement": {}, + }, + "Leasehold Improvement": {}, "Motor Vehicles": { "account_type": "Fixed Asset" - }, - "Work In Progress": {}, + }, + "Work In Progress": {}, "account_type": "Fixed Asset" } - }, + }, "Intangible Assets": { - "Computer Card Renewal": {}, - "Disposal of Outlets": {}, + "Computer Card Renewal": {}, + "Disposal of Outlets": {}, "Registration of Trademarks": {} - }, - "Intercompany Accounts": {}, + }, + "Intercompany Accounts": {}, "Investments": { "Investments in Subsidiaries": {} } - }, + }, "root_type": "Asset" - }, + }, "Closing And Temporary Accounts": { "Closing Accounts": { "Closing Account": {} - }, + }, "root_type": "Liability" - }, + }, "Expenses": { "Commercial Expenses": { - "Consultancy Fees": {}, + "Consultancy Fees": {}, "Provision for Doubtful Debts": {} - }, + }, "Cost of Sale": { "Cost Of Goods Sold": { - "Cost Of Goods Sold I/C Sales": {}, + "Cost Of Goods Sold I/C Sales": {}, "Cost of Goods Sold in Trading": { "account_type": "Cost of Goods Sold" - }, + }, "account_type": "Cost of Goods Sold" - }, + }, "Expenses Included In Valuation": { "account_type": "Expenses Included In Valuation" + }, + "Stock Adjustment": { + "account_type": "Stock Adjustment" } - }, + }, "Depreciation": { "Depreciation & Amortization": { - "Amortization on Leasehold Improvement": {}, + "Amortization on Leasehold Improvement": {}, "Depreciation Of Computer Hard & Soft": { "account_type": "Depreciation" - }, + }, "Depreciation Of Furniture & Office Equipment\n\t\t\t": { "account_type": "Depreciation" - }, + }, "Depreciation Of Motor Vehicles": { "account_type": "Depreciation" } } - }, + }, "Direct Expenses": { "Financial Charges": { - "Air Miles Card Charges": {}, - "Amex Credit Cards Charges": {}, - "Bank Finance & Loan Charges": {}, - "Credit Card Charges": {}, - "Credit Card Swipe Charges": {}, + "Air Miles Card Charges": {}, + "Amex Credit Cards Charges": {}, + "Bank Finance & Loan Charges": {}, + "Credit Card Charges": {}, + "Credit Card Swipe Charges": {}, "PayPal Charges": {} } - }, + }, "MISC Charges": { "Other Charges": { "Capital Loss": { - "Disposal of Business Branch": {}, - "Loss On Fixed Assets Disposal": {}, + "Disposal of Business Branch": {}, + "Loss On Fixed Assets Disposal": {}, "Loss on Difference on Exchange": {} - }, + }, "Other Non Operating Exp": { "Other Non Operating Expenses": {} - }, + }, "Previous Year Adjustments": { "Previous Year Adjustments Account": {} - }, + }, "Royalty Fees": { "Royalty to Parent Co.": {} - }, + }, "Tax / Zakat Expenses": { "Income Tax": { "account_type": "Tax" - }, - "Zakat": {}, + }, + "Zakat": {}, "account_type": "Tax" } } - }, + }, "Share Resources": { "Share Resource Expenses Account": {} - }, + }, "Store Operating Expenses": { "Selling, General & Admin Expenses": { "Advertising Expenses": { "Other - Advertising Expenses": {} - }, + }, "Bank & Finance Charges": { "Other Bank Charges": {} - }, + }, "Communications": { - "Courier": {}, - "Others - Communication": {}, - "Telephone": {}, + "Courier": {}, + "Others - Communication": {}, + "Telephone": {}, "Web Site Hosting Fees": {} - }, + }, "Office & Various Expenses": { - "Cleaning": {}, - "Conveyance Expenses": {}, - "Gifts & Donations": {}, - "Insurance": {}, - "Kitchen and Buffet Expenses": {}, - "Maintenance": {}, - "Others - Office Various Expenses": {}, - "Security & Guard": {}, - "Stationary From Suppliers": {}, - "Stationary Out Of Stock": {}, - "Subscriptions": {}, - "Training": {}, + "Cleaning": {}, + "Conveyance Expenses": {}, + "Gifts & Donations": {}, + "Insurance": {}, + "Kitchen and Buffet Expenses": {}, + "Maintenance": {}, + "Others - Office Various Expenses": {}, + "Security & Guard": {}, + "Stationary From Suppliers": {}, + "Stationary Out Of Stock": {}, + "Subscriptions": {}, + "Training": {}, "Vehicle Expenses": {} - }, + }, "Personnel Cost": { - "Basic Salary": {}, - "End Of Service Indemnity": {}, - "Housing Allowance": {}, - "Leave Salary": {}, - "Leave Ticket": {}, - "Life Insurance": {}, - "Medical Insurance": {}, - "Personnel Cost Others": {}, - "Sales Commission": {}, - "Staff School Allowances": {}, - "Transportation Allowance": {}, - "Uniform": {}, + "Basic Salary": {}, + "End Of Service Indemnity": {}, + "Housing Allowance": {}, + "Leave Salary": {}, + "Leave Ticket": {}, + "Life Insurance": {}, + "Medical Insurance": {}, + "Personnel Cost Others": {}, + "Sales Commission": {}, + "Staff School Allowances": {}, + "Transportation Allowance": {}, + "Uniform": {}, "Visa Expenses": {} - }, + }, "Professional & Legal Fees": { - "Audit Fees": {}, - "Legal fees": {}, - "Others - Professional Fees": {}, - "Sponsorship Fees": {}, + "Audit Fees": {}, + "Legal fees": {}, + "Others - Professional Fees": {}, + "Sponsorship Fees": {}, "Trade License Fees": {} - }, + }, "Provision & Write Off": { - "Amortisation of Preoperating Expenses": {}, - "Cash Shortage": {}, - "Others - Provision & Write off": {}, - "Write Off Inventory": {}, + "Amortisation of Preoperating Expenses": {}, + "Cash Shortage": {}, + "Others - Provision & Write off": {}, + "Write Off Inventory": {}, "Write Off Receivables & Payables": {} - }, + }, "Rent Expenses": { - "Office Rent": {}, + "Office Rent": {}, "Warehouse Rent": {} - }, + }, "Travel Expenses": { - "Air tickets": {}, - "Hotel": {}, - "Meals": {}, - "Others": {}, + "Air tickets": {}, + "Hotel": {}, + "Meals": {}, + "Others": {}, "Per Diem": {} - }, + }, "Utilities": { - "Other Utility Cahrges": {}, + "Other Utility Cahrges": {}, "Water & Electricity": {} } } - }, + }, "root_type": "Expense" - }, + }, "Liabilities": { "Current Liabilities": { "Accounts Payable": { "Payables": { "Advance Payable to Suppliers": { "account_type": "Payable" - }, + }, "Consigned Payable": { "account_type": "Payable" - }, + }, "Other Payable": { "account_type": "Payable" - }, + }, "Post Dated Cheques Paid": { "account_type": "Payable" - }, - "Staff Payable": {}, + }, + "Staff Payable": {}, "Suppliers Price Protection": { "account_type": "Payable" - }, + }, "Trade Payable": { "account_type": "Payable" - }, + }, "account_type": "Payable" } - }, + }, "Accruals & Provisions": { "Accruals": { "Accrued Personnel Cost": { - "Accrued - Commissions": {}, - "Accrued - Leave Salary": {}, - "Accrued - Leave Tickets": {}, - "Accrued - Salaries": {}, - "Accrued Other Personnel Cost": {}, - "Accrued Salaries Increment": {}, + "Accrued - Commissions": {}, + "Accrued - Leave Salary": {}, + "Accrued - Leave Tickets": {}, + "Accrued - Salaries": {}, + "Accrued Other Personnel Cost": {}, + "Accrued Salaries Increment": {}, "Accrued-Staff Bonus": {} } - }, + }, "Accrued Expenses": { "Accrued Other Expenses": { - "Accrued - Audit Fees": {}, - "Accrued - Office Rent": {}, - "Accrued - Sponsorship": {}, - "Accrued - Telephone": {}, - "Accrued - Utilities": {}, + "Accrued - Audit Fees": {}, + "Accrued - Office Rent": {}, + "Accrued - Sponsorship": {}, + "Accrued - Telephone": {}, + "Accrued - Utilities": {}, "Accrued Others": {} } - }, + }, "Other Current Liabilities": { - "Accrued Dubai Customs": {}, - "Deferred income": {}, + "Accrued Dubai Customs": {}, + "Deferred income": {}, "Shipping & Handling": {} - }, + }, "Provisions": { "Tax Payables": { - "Income Tax Payable": {}, - "Sales Tax Payable": {}, + "Income Tax Payable": {}, + "Sales Tax Payable": {}, "Withholding Tax Payable": {} } - }, + }, "Short Term Loan": {} - }, + }, "Duties and Taxes": { - "account_type": "Tax", + "account_type": "Tax", "is_group": 1 - }, + }, "Reservations & Credit Notes": { "Credit Notes": { - "Credit Notes to Customers": {}, + "Credit Notes to Customers": {}, "Reservations": {} } - }, + }, "Stock Liabilities": { "Stock Received But Not Billed": { "account_type": "Stock Received But Not Billed" } - }, + }, "Unearned Income": {} - }, + }, "Long Term Liabilities": { "Long Term Loans & Provisions": {} - }, + }, "root_type": "Liability" - }, + }, "Revenue": { "Direct Revenue": { "Other Direct Revenue": { "Other Revenue - Operating": { - "Advertising Income": {}, - "Branding Income": {}, - "Early Setmt Margin from Suppliers": {}, - "Marketing Rebate from Suppliers": {}, - "Rebate from Suppliers": {}, - "Service Income": {}, + "Advertising Income": {}, + "Branding Income": {}, + "Early Setmt Margin from Suppliers": {}, + "Marketing Rebate from Suppliers": {}, + "Rebate from Suppliers": {}, + "Service Income": {}, "Space Rental Income": {} } } - }, + }, "Indirect Revenue": { "Other Indirect Revenue": { - "Capital Gain": {}, - "Excess In Till": {}, - "Gain On Difference Of Exchange": {}, - "Management Consultancy Fees": {}, + "Capital Gain": {}, + "Excess In Till": {}, + "Gain On Difference Of Exchange": {}, + "Management Consultancy Fees": {}, "Other Income": {} - }, + }, "Other Revenue - Non Operating": { - "Interest Revenue": {}, - "Interest from FD": {}, - "Products Listing Fees from Suppliers": {}, + "Interest Revenue": {}, + "Interest from FD": {}, + "Products Listing Fees from Suppliers": {}, "Trade Opening Fees from suppliers": {} } - }, + }, "Sales": { "Sales from Other Regions": { "Sales from Other Region": {} - }, + }, "Sales of same region": { - "Management Consultancy Fees 1": {}, - "Sales Account": {}, + "Management Consultancy Fees 1": {}, + "Sales Account": {}, "Sales of I/C": {} } - }, + }, "root_type": "Income" - }, + }, "Share Holder Equity": { "Capital": { - "Contributed Capital": {}, - "Share Capital": {}, - "Shareholders Current A/c": {}, - "Sub Ordinated Loan": {}, + "Contributed Capital": {}, + "Share Capital": {}, + "Shareholders Current A/c": {}, + "Sub Ordinated Loan": {}, "Treasury Stocks": {} - }, + }, "Retained Earnings": { - "Current Year Results": {}, - "Dividends Paid": {}, + "Current Year Results": {}, + "Dividends Paid": {}, "Previous Years Results": {} - }, - "account_type": "Equity", + }, + "account_type": "Equity", "root_type": "Equity" } } From 5c2ba0ef9c6e6424af95b841dade3e0b873184aa Mon Sep 17 00:00:00 2001 From: Joseph Marie Alba <54699674+erpjosephalba@users.noreply.github.com> Date: Thu, 28 Nov 2019 21:03:20 +0800 Subject: [PATCH 299/679] Fix: Logic bug (#19692) account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] not in ('', 1)] - if accounts[d]['is_group'] not in ('', 1) is wrong because it returns false even if the account is a group. - should be if accounts[d]['is_group'] not in ('', 0) However, the correction provided here: account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1] is more consistent with the prior statement that extracts ledger (and not group) accounts. account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1] --- .../chart_of_accounts_importer/chart_of_accounts_importer.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 9bf5887b38f..34070b01aea 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -185,7 +185,8 @@ def validate_account_types(accounts): return _("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)) account_types_for_group = ["Bank", "Cash", "Stock"] - account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] not in ('', 1)] + # fix logic bug + account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1] missing = list(set(account_types_for_group) - set(account_groups)) if missing: From 7cdde9364502a8bd28a744119a96b48a494d05b6 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 28 Nov 2019 18:37:51 +0530 Subject: [PATCH 300/679] fix: Validation for parent cost center (#19664) * fix: Validation for parent cost center * fix: Minor modification in condition * fix: Update test cases for invalid cost center creation --- .../doctype/cost_center/cost_center.py | 7 +++++++ .../doctype/cost_center/test_cost_center.py | 18 ++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py index 584e11c53f6..0294e78111c 100644 --- a/erpnext/accounts/doctype/cost_center/cost_center.py +++ b/erpnext/accounts/doctype/cost_center/cost_center.py @@ -18,6 +18,7 @@ class CostCenter(NestedSet): def validate(self): self.validate_mandatory() + self.validate_parent_cost_center() def validate_mandatory(self): if self.cost_center_name != self.company and not self.parent_cost_center: @@ -25,6 +26,12 @@ class CostCenter(NestedSet): elif self.cost_center_name == self.company and self.parent_cost_center: frappe.throw(_("Root cannot have a parent cost center")) + def validate_parent_cost_center(self): + if self.parent_cost_center: + if not frappe.db.get_value('Cost Center', self.parent_cost_center, 'is_group'): + frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format( + frappe.bold(self.parent_cost_center))) + def convert_group_to_ledger(self): if self.check_if_child_exists(): frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes")) diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py index c4fad753756..8f23d906760 100644 --- a/erpnext/accounts/doctype/cost_center/test_cost_center.py +++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py @@ -1,12 +1,26 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals - - +import unittest import frappe + test_records = frappe.get_test_records('Cost Center') +class TestCostCenter(unittest.TestCase): + def test_cost_center_creation_against_child_node(self): + if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}): + frappe.get_doc(test_records[1]).insert() + + cost_center = frappe.get_doc({ + 'doctype': 'Cost Center', + 'cost_center_name': '_Test Cost Center 3', + 'parent_cost_center': '_Test Cost Center 2 - _TC', + 'is_group': 0, + 'company': '_Test Company' + }) + + self.assertRaises(frappe.ValidationError, cost_center.save) def create_cost_center(**args): args = frappe._dict(args) From f3b393f5e0e4cd7e0864be1c9791301c980bd841 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Thu, 28 Nov 2019 18:52:16 +0530 Subject: [PATCH 301/679] fix: UOM was not fetching in purchase invoice (#19732) * 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 2597817cdeb025caa1f4d4fb7fee49831761778a Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 28 Nov 2019 18:57:42 +0530 Subject: [PATCH 302/679] fix: add new routes to handle category wise search --- erpnext/public/js/hub/PageContainer.vue | 2 +- erpnext/public/js/hub/pages/Category.vue | 2 +- erpnext/public/js/hub/pages/Home.vue | 2 +- erpnext/public/js/hub/pages/Search.vue | 15 +++++++++++---- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/erpnext/public/js/hub/PageContainer.vue b/erpnext/public/js/hub/PageContainer.vue index f151add8d5a..54c359766d3 100644 --- a/erpnext/public/js/hub/PageContainer.vue +++ b/erpnext/public/js/hub/PageContainer.vue @@ -24,7 +24,7 @@ import NotFound from './pages/NotFound.vue'; function get_route_map() { const read_only_routes = { 'marketplace/home': Home, - 'marketplace/search/:keyword': Search, + 'marketplace/search/:category/:keyword': Search, 'marketplace/category/:category': Category, 'marketplace/item/:item': Item, 'marketplace/seller/:seller': Seller, diff --git a/erpnext/public/js/hub/pages/Category.vue b/erpnext/public/js/hub/pages/Category.vue index a1d5d729cc9..057fe8bc617 100644 --- a/erpnext/public/js/hub/pages/Category.vue +++ b/erpnext/public/js/hub/pages/Category.vue @@ -67,7 +67,7 @@ export default { }, set_search_route() { - frappe.set_route('marketplace', 'search', this.search_value); + frappe.set_route('marketplace', 'search', this.category, this.search_value); }, } } diff --git a/erpnext/public/js/hub/pages/Home.vue b/erpnext/public/js/hub/pages/Home.vue index 353656957df..aaeaa7eb7c1 100644 --- a/erpnext/public/js/hub/pages/Home.vue +++ b/erpnext/public/js/hub/pages/Home.vue @@ -98,7 +98,7 @@ export default { }, set_search_route() { - frappe.set_route('marketplace', 'search', this.search_value); + frappe.set_route('marketplace', 'search', 'All', this.search_value); }, } } diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue index 5118a814e07..2a6088925fd 100644 --- a/erpnext/public/js/hub/pages/Search.vue +++ b/erpnext/public/js/hub/pages/Search.vue @@ -29,8 +29,10 @@ export default { return { page_name: frappe.get_route()[1], items: [], - search_value: frappe.get_route()[2], + category: frappe.get_route()[2], + search_value: frappe.get_route()[3], item_id_fieldname: 'name', + filters: {}, // Constants search_placeholder: __('Search for anything ...'), @@ -40,7 +42,7 @@ export default { computed: { page_title() { return this.items.length - ? __(`Results for "${this.search_value}"`) + ? __(`Results for ${this.search_value} in category ${this.category}`) : __('No Items found.'); } }, @@ -49,14 +51,19 @@ export default { }, methods: { get_items() { - hub.call('get_items', { keyword: this.search_value }) + if (this.category !== 'All') { + this.filters['hub_category']=this.category; + } + hub.call('get_items', { keyword: this.search_value, + filters: this.filters + }) .then((items) => { this.items = items; }) }, set_route_and_get_items() { - frappe.set_route('marketplace', 'search', this.search_value); + frappe.set_route('marketplace', 'search', this.category, this.search_value); this.get_items(); }, From 8f48896261615f2ea9880e3ccb9d6d538e59d053 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 19:39:49 +0530 Subject: [PATCH 303/679] fix: Permission issue in Stock Entry (#19738) --- 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 9d6d95c4a20f5ef6ef6a366473c461de934a91d7 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Fri, 29 Nov 2019 13:26:52 +0530 Subject: [PATCH 304/679] fix: valuation of "finished good" item in purchase receipt (#19268) * 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 8b599f2bc9c9e6b2dad7c1c6e5c67d6404c273dc Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 30 Oct 2019 01:52:03 +0530 Subject: [PATCH 305/679] fix: capacity planning back --- .../doctype/job_card/job_card.json | 1288 ++++------------- .../doctype/job_card/job_card.py | 122 +- .../manufacturing_settings.json | 715 ++------- .../doctype/work_order/work_order.js | 1 + .../doctype/work_order/work_order.py | 66 +- .../doctype/workstation/workstation.json | 583 ++------ .../doctype/workstation/workstation.py | 15 +- .../workstation/workstation_dashboard.py | 8 +- erpnext/patches.txt | 3 +- .../set_production_capacity_in_workstation.py | 8 + 10 files changed, 747 insertions(+), 2062 deletions(-) create mode 100644 erpnext/patches/v12_0/set_production_capacity_in_workstation.py diff --git a/erpnext/manufacturing/doctype/job_card/job_card.json b/erpnext/manufacturing/doctype/job_card/job_card.json index 39c5cce313b..2c217028d06 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.json +++ b/erpnext/manufacturing/doctype/job_card/job_card.json @@ -1,1071 +1,299 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "PO-JOB.#####", - "beta": 0, - "creation": "2018-07-09 17:23:29.518745", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "autoname": "naming_series:", + "creation": "2018-07-09 17:23:29.518745", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "work_order", + "bom_no", + "workstation", + "operation", + "column_break_4", + "posting_date", + "company", + "for_quantity", + "wip_warehouse", + "timing_detail", + "employee", + "time_logs", + "section_break_13", + "total_completed_qty", + "total_time_in_mins", + "column_break_15", + "section_break_8", + "items", + "more_information", + "operation_id", + "transferred_qty", + "requested_qty", + "project", + "remarks", + "column_break_20", + "status", + "job_started", + "started_time", + "amended_from" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "work_order", - "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": "Work Order", - "length": 0, - "no_copy": 0, - "options": "Work Order", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "work_order", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Work Order", + "options": "Work Order", + "read_only": 1, + "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_if_empty": 0, - "fieldname": "bom_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": "BOM No", - "length": 0, - "no_copy": 0, - "options": "BOM", - "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": "bom_no", + "fieldtype": "Link", + "label": "BOM No", + "options": "BOM", + "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": "workstation", - "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": "Workstation", - "length": 0, - "no_copy": 0, - "options": "Workstation", - "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": "workstation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Workstation", + "options": "Workstation", + "reqd": 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": "operation", - "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": "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "operation", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Operation", + "options": "Operation", + "reqd": 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_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, - "default": "Today", - "fetch_if_empty": 0, - "fieldname": "posting_date", - "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": "Posting Date", - "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 - }, + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "label": "Posting Date" + }, { - "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, - "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", + "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, - "fetch_if_empty": 0, - "fieldname": "for_quantity", - "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": "For 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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "for_quantity", + "fieldtype": "Float", + "in_list_view": 1, + "label": "For Quantity", + "reqd": 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": "wip_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": "WIP 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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "wip_warehouse", + "fieldtype": "Link", + "label": "WIP Warehouse", + "options": "Warehouse", + "reqd": 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": "timing_detail", - "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": "Timing Detail", - "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": "timing_detail", + "fieldtype": "Section Break", + "label": "Timing Detail" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "employee", - "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": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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": "employee", + "fieldtype": "Link", + "label": "Employee", + "options": "Employee" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "time_logs", - "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": "Time Logs", - "length": 0, - "no_copy": 0, - "options": "Job Card Time Log", - "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": "time_logs", + "fieldtype": "Table", + "label": "Time Logs", + "options": "Job Card Time Log" + }, { - "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_13", - "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_13", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_completed_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 Completed Qty", - "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": "total_completed_qty", + "fieldtype": "Float", + "label": "Total Completed Qty", + "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_break_15", - "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_15", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_time_in_mins", - "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 Time in Mins", - "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": "total_time_in_mins", + "fieldtype": "Float", + "label": "Total Time in Mins", + "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": "section_break_8", - "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": "Raw Materials", - "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_8", + "fieldtype": "Section Break", + "label": "Raw Materials" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 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, - "options": "Job Card 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 - }, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "options": "Job Card Item", + "read_only": 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_information", - "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, - "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 - }, + "collapsible": 1, + "fieldname": "more_information", + "fieldtype": "Section Break", + "label": "More Information" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "operation_id", - "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": "Operation ID", - "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": "operation_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Operation ID", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "transferred_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": "Transferred Qty", - "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": "transferred_qty", + "fieldtype": "Float", + "label": "Transferred Qty", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "fetch_if_empty": 0, - "fieldname": "requested_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 Qty", - "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": "requested_qty", + "fieldtype": "Float", + "label": "Requested Qty", + "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": "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, - "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": "project", + "fieldtype": "Link", + "label": "Project", + "options": "Project" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "remarks", - "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": "Remarks", - "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": "remarks", + "fieldtype": "Small Text", + "label": "Remarks" + }, { - "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_20", - "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_20", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Open", - "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": 0, - "in_standard_filter": 0, - "label": "Status", - "length": 0, - "no_copy": 1, - "options": "Open\nWork In Progress\nMaterial Transferred\nSubmitted\nCancelled\nCompleted", - "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": "Open", + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Open\nWork In Progress\nMaterial Transferred\nSubmitted\nCancelled\nCompleted", + "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": "job_started", - "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": "Job Started", - "length": 0, - "no_copy": 1, - "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 - }, + "default": "0", + "fieldname": "job_started", + "fieldtype": "Check", + "hidden": 1, + "label": "Job Started", + "no_copy": 1, + "print_hide": 1, + "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": "started_time", - "fieldtype": "Datetime", - "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": "Started Time", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "started_time", + "fieldtype": "Datetime", + "hidden": 1, + "label": "Started Time", + "no_copy": 1, + "print_hide": 1, + "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": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Job Card", - "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 + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Job Card", + "print_hide": 1, + "read_only": 1 + }, + { + "default": "PO-JOB.#####", + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "PO-JOB.#####", + "reqd": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-03-10 17:38:37.499871", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Job Card", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "modified": "2019-10-30 01:49:19.606178", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Job Card", + "owner": "Administrator", "permissions": [ { - "amend": 1, - "cancel": 1, - "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": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, + "submit": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing Manager", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "operation", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "operation", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py index 9d2e620e584..e8787ed7db0 100644 --- a/erpnext/manufacturing/doctype/job_card/job_card.py +++ b/erpnext/manufacturing/doctype/job_card/job_card.py @@ -4,10 +4,16 @@ from __future__ import unicode_literals import frappe +import datetime from frappe import _ -from frappe.utils import flt, time_diff_in_hours, get_datetime from frappe.model.mapper import get_mapped_doc from frappe.model.document import Document +from frappe.utils import (flt, cint, time_diff_in_hours, get_datetime, getdate, + get_time, add_to_date, time_diff, add_days, get_datetime_str) + +from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations + +class OverlapError(frappe.ValidationError): pass class JobCard(Document): def validate(self): @@ -26,7 +32,7 @@ class JobCard(Document): data = self.get_overlap_for(d) if data: frappe.throw(_("Row {0}: From Time and To Time of {1} is overlapping with {2}") - .format(d.idx, self.name, data.name)) + .format(d.idx, self.name, data.name), OverlapError) if d.from_time and d.to_time: d.time_in_mins = time_diff_in_hours(d.to_time, d.from_time) * 60 @@ -35,27 +41,120 @@ class JobCard(Document): if d.completed_qty: self.total_completed_qty += d.completed_qty - def get_overlap_for(self, args): - existing = frappe.db.sql("""select jc.name as name from + def get_overlap_for(self, args, check_next_available_slot=False): + production_capacity = 1 + + if self.workstation: + production_capacity = frappe.get_cached_value("Workstation", + self.workstation, 'production_capacity') or 1 + validate_overlap_for = " and jc.workstation = %(workstation)s " + + if self.employee: + # override capacity for employee + production_capacity = 1 + validate_overlap_for = " and jc.employee = %(employee)s " + + extra_cond = '' + if check_next_available_slot: + extra_cond = " or (%(from_time)s <= jctl.from_time and %(to_time)s <= jctl.to_time)" + + existing = frappe.db.sql("""select jc.name as name, jctl.to_time from `tabJob Card Time Log` jctl, `tabJob Card` jc where jctl.parent = jc.name and ( (%(from_time)s > jctl.from_time and %(from_time)s < jctl.to_time) or (%(to_time)s > jctl.from_time and %(to_time)s < jctl.to_time) or - (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time)) - and jctl.name!=%(name)s - and jc.name!=%(parent)s - and jc.docstatus < 2 - and jc.employee = %(employee)s """, + (%(from_time)s <= jctl.from_time and %(to_time)s >= jctl.to_time) {0} + ) + and jctl.name != %(name)s and jc.name != %(parent)s and jc.docstatus < 2 {1} + order by jctl.to_time desc limit 1""".format(extra_cond, validate_overlap_for), { "from_time": args.from_time, "to_time": args.to_time, "name": args.name or "No Name", "parent": args.parent or "No Name", - "employee": self.employee + "employee": self.employee, + "workstation": self.workstation }, as_dict=True) + if existing and production_capacity > len(existing): + return + return existing[0] if existing else None + def schedule_time_logs(self, row): + row.remaining_time_in_mins = row.time_in_mins + while row.remaining_time_in_mins > 0: + args = frappe._dict({ + "from_time": row.planned_start_time, + "to_time": row.planned_end_time + }) + + self.validate_overlap_for_workstation(args, row) + self.check_workstation_time(row) + + def validate_overlap_for_workstation(self, args, row): + # get the last record based on the to time from the job card + data = self.get_overlap_for(args, check_next_available_slot=True) + if data: + row.planned_start_time = get_datetime(data.to_time + get_mins_between_operations()) + + def check_workstation_time(self, row): + workstation_doc = frappe.get_cached_doc("Workstation", self.workstation) + if (not workstation_doc.working_hours or + cint(frappe.db.get_single_value("Manufacturing Settings", "allow_overtime"))): + row.remaining_time_in_mins -= time_diff_in_minutes(row.planned_end_time, + row.planned_start_time) + + self.update_time_logs(row) + return + + start_date = getdate(row.planned_start_time) + start_time = get_time(row.planned_start_time) + + new_start_date = workstation_doc.validate_workstation_holiday(start_date) + + if new_start_date != start_date: + row.planned_start_time = datetime.datetime.combine(new_start_date, start_time) + start_date = new_start_date + + total_idx = len(workstation_doc.working_hours) + + for i, time_slot in enumerate(workstation_doc.working_hours): + workstation_start_time = datetime.datetime.combine(start_date, get_time(time_slot.start_time)) + workstation_end_time = datetime.datetime.combine(start_date, get_time(time_slot.end_time)) + + if (get_datetime(row.planned_start_time) >= workstation_start_time and + get_datetime(row.planned_start_time) <= workstation_end_time): + time_in_mins = time_diff_in_minutes(workstation_end_time, row.planned_start_time) + + # If remaining time fit in workstation time logs else split hours as per workstation time + if time_in_mins > row.remaining_time_in_mins: + row.planned_end_time = add_to_date(row.planned_start_time, + minutes=row.remaining_time_in_mins) + row.remaining_time_in_mins = 0 + else: + row.planned_end_time = add_to_date(row.planned_start_time, minutes=time_in_mins) + row.remaining_time_in_mins -= time_in_mins + + self.update_time_logs(row) + + if total_idx != (i+1) and row.remaining_time_in_mins > 0: + row.planned_start_time = datetime.datetime.combine(start_date, + get_time(workstation_doc.working_hours[i+1].start_time)) + + if row.remaining_time_in_mins > 0: + start_date = add_days(start_date, 1) + row.planned_start_time = datetime.datetime.combine(start_date, + get_time(workstation_doc.working_hours[0].start_time)) + + def update_time_logs(self, row): + self.append("time_logs", { + "from_time": row.planned_start_time, + "to_time": row.planned_end_time, + "completed_qty": 0, + "time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time), + }) + def get_required_items(self): if not self.get('work_order'): return @@ -251,3 +350,6 @@ def make_stock_entry(source_name, target_doc=None): }, target_doc, set_missing_values) return doclist + +def time_diff_in_minutes(string_ed_date, string_st_date): + return time_diff(string_ed_date, string_st_date).total_seconds() / 60 \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json index 461b9ab3dfa..86fa7a8901a 100644 --- a/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json +++ b/erpnext/manufacturing/doctype/manufacturing_settings/manufacturing_settings.json @@ -1,585 +1,178 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2014-11-27 14:12:07.542534", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 0, - "engine": "InnoDB", + "creation": "2014-11-27 14:12:07.542534", + "doctype": "DocType", + "document_type": "Document", + "engine": "InnoDB", + "field_order": [ + "raw_materials_consumption_section", + "material_consumption", + "column_break_3", + "backflush_raw_materials_based_on", + "capacity_planning", + "disable_capacity_planning", + "allow_overtime", + "allow_production_on_holidays", + "column_break_5", + "capacity_planning_for_days", + "mins_between_operations", + "section_break_6", + "default_wip_warehouse", + "default_fg_warehouse", + "column_break_11", + "default_scrap_warehouse", + "over_production_for_sales_and_work_order_section", + "overproduction_percentage_for_sales_order", + "column_break_16", + "overproduction_percentage_for_work_order", + "other_settings_section", + "update_bom_costs_automatically" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "capacity_planning", - "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": "Capacity Planning", - "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": "capacity_planning", + "fieldtype": "Section Break", + "label": "Capacity Planning" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Disables creation of time logs against Work Orders. Operations shall not be tracked against Work Order", - "fieldname": "disable_capacity_planning", - "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": "Disable Capacity Planning and Time Tracking", - "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 - }, + "default": "0", + "depends_on": "eval:!doc.disable_capacity_planning", + "description": "Plan time logs outside Workstation Working Hours.", + "fieldname": "allow_overtime", + "fieldtype": "Check", + "label": "Allow Overtime" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Plan time logs outside Workstation Working Hours.", - "fieldname": "allow_overtime", - "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": "Allow Overtime", - "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 - }, + "default": "0", + "depends_on": "eval:!doc.disable_capacity_planning", + "fieldname": "allow_production_on_holidays", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Allow Production on Holidays" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "allow_production_on_holidays", - "fieldtype": "Check", - "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": "Allow Production on Holidays", - "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 - }, + "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": "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 - }, + "default": "30", + "depends_on": "eval:!doc.disable_capacity_planning", + "description": "Try planning operations for X days in advance.", + "fieldname": "capacity_planning_for_days", + "fieldtype": "Int", + "label": "Capacity Planning For (Days)" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "30", - "description": "Try planning operations for X days in advance.", - "fieldname": "capacity_planning_for_days", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Capacity Planning For (Days)", - "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 - }, + "depends_on": "eval:!doc.disable_capacity_planning", + "description": "Default 10 mins", + "fieldname": "mins_between_operations", + "fieldtype": "Int", + "label": "Time Between Operations (in mins)" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Default 10 mins", - "fieldname": "mins_between_operations", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Time Between Operations (in mins)", - "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_6", + "fieldtype": "Section Break", + "label": "Default Warehouses for Production" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_6", - "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": "overproduction_percentage_for_sales_order", + "fieldtype": "Percent", + "label": "Overproduction Percentage For Sales Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "overproduction_percentage_for_sales_order", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Overproduction Percentage For Sales Order", - "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": "overproduction_percentage_for_work_order", + "fieldtype": "Percent", + "label": "Overproduction Percentage For Work Order" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "overproduction_percentage_for_work_order", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Overproduction Percentage For Work Order", - "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 - }, + "default": "BOM", + "fieldname": "backflush_raw_materials_based_on", + "fieldtype": "Select", + "label": "Backflush Raw Materials Based On", + "options": "BOM\nMaterial Transferred for Manufacture" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "BOM", - "fieldname": "backflush_raw_materials_based_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": "Backflush Raw Materials Based On", - "length": 0, - "no_copy": 0, - "options": "BOM\nMaterial Transferred for Manufacture", - "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 - }, + "default": "0", + "description": "Allow multiple Material Consumption against a Work Order", + "fieldname": "material_consumption", + "fieldtype": "Check", + "label": "Allow Multiple Material Consumption" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Allow multiple Material Consumption against a Work Order", - "fieldname": "material_consumption", - "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": "Allow Multiple Material Consumption", - "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 - }, + "default": "0", + "description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.", + "fieldname": "update_bom_costs_automatically", + "fieldtype": "Check", + "label": "Update BOM Cost Automatically" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Update BOM cost automatically via Scheduler, based on latest valuation rate / price list rate / last purchase rate of raw materials.", - "fieldname": "update_bom_costs_automatically", - "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": "Update BOM Cost Automatically", - "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_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "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": "default_wip_warehouse", + "fieldtype": "Link", + "label": "Default Work In Progress Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_wip_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": "Default Work In Progress 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 - }, + "fieldname": "default_fg_warehouse", + "fieldtype": "Link", + "label": "Default Finished Goods Warehouse", + "options": "Warehouse" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "default_fg_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": "Default Finished Goods 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 + "default": "0", + "fieldname": "disable_capacity_planning", + "fieldtype": "Check", + "label": "Disable Capacity Planning" + }, + { + "fieldname": "default_scrap_warehouse", + "fieldtype": "Link", + "label": "Default Scrap Warehouse", + "options": "Warehouse" + }, + { + "fieldname": "over_production_for_sales_and_work_order_section", + "fieldtype": "Section Break", + "label": "Over Production for Sales and Work Order" + }, + { + "fieldname": "raw_materials_consumption_section", + "fieldtype": "Section Break", + "label": "Raw Materials Consumption" + }, + { + "fieldname": "column_break_16", + "fieldtype": "Column Break" + }, + { + "fieldname": "other_settings_section", + "fieldtype": "Section Break", + "label": "Other Settings" + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-wrench", - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 1, - "istable": 0, - "max_attachments": 0, - "menu_index": 0, - "modified": "2018-05-28 00:46:25.310621", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Manufacturing Settings", - "name_case": "", - "owner": "Administrator", + ], + "icon": "icon-wrench", + "issingle": 1, + "modified": "2019-11-26 13:10:45.569341", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Manufacturing Settings", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Manufacturing Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "read": 1, + "role": "Manufacturing Manager", + "share": 1, "write": 1 } - ], - "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 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 107c79b89bc..5c721c723d2 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -551,6 +551,7 @@ erpnext.work_order = { if (!r.exe) { frm.set_value("wip_warehouse", r.message.wip_warehouse); frm.set_value("fg_warehouse", r.message.fg_warehouse); + frm.set_value("scrap_warehouse", r.message.scrap_warehouse); } } }); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2c16bbe90c5..84f570b07af 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -12,7 +12,7 @@ from erpnext.manufacturing.doctype.bom.bom import validate_bom_no, get_bom_items from dateutil.relativedelta import relativedelta from erpnext.stock.doctype.item.item import validate_end_of_life from erpnext.manufacturing.doctype.workstation.workstation import WorkstationHolidayError -from erpnext.projects.doctype.timesheet.timesheet import OverlapError +from erpnext.manufacturing.doctype.job_card.job_card import OverlapError from erpnext.stock.doctype.stock_entry.stock_entry import get_additional_costs from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.stock.stock_balance import get_planned_qty, update_bin_qty @@ -260,12 +260,50 @@ class WorkOrder(Document): self.update_reserved_qty_for_production() def create_job_card(self): - for row in self.operations: + manufacturing_settings_doc = frappe.get_doc("Manufacturing Settings") + + enable_capacity_planning = not cint(manufacturing_settings_doc.disable_capacity_planning) + plan_days = cint(manufacturing_settings_doc.capacity_planning_for_days) or 30 + + for i, row in enumerate(self.operations): + self.set_operation_start_end_time(i, row) + if not row.workstation: frappe.throw(_("Row {0}: select the workstation against the operation {1}") .format(row.idx, row.operation)) - create_job_card(self, row, auto_create=True) + original_start_time = row.planned_start_time + job_card_doc = create_job_card(self, row, + enable_capacity_planning=enable_capacity_planning, auto_create=True) + + if enable_capacity_planning and job_card_doc: + row.planned_start_time = job_card_doc.time_logs[0].from_time + row.planned_end_time = job_card_doc.time_logs[-1].to_time + + if date_diff(row.planned_start_time, original_start_time) > plan_days: + frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") + .format(plan_days, row.operation)) + + row.db_update() + + planned_end_date = self.operations and self.operations[-1].planned_end_time + if planned_end_date: + self.db_set("planned_end_date", planned_end_date) + + def set_operation_start_end_time(self, idx, row): + """Set start and end time for given operation. If first operation, set start as + `planned_start_date`, else add time diff to end time of earlier operation.""" + if idx==0: + # first operation at planned_start date + row.planned_start_time = self.planned_start_date + else: + row.planned_start_time = get_datetime(self.operations[idx-1].planned_end_time)\ + + get_mins_between_operations() + + row.planned_end_time = get_datetime(row.planned_start_time) + relativedelta(minutes = row.time_in_mins) + + if row.planned_start_time == row.planned_end_time: + frappe.throw(_("Capacity Planning Error, planned start time can not be same as end time")) def validate_cancel(self): if self.status == "Stopped": @@ -327,9 +365,8 @@ class WorkOrder(Document): """Fetch operations from BOM and set in 'Work Order'""" self.set('operations', []) - if not self.bom_no \ - or cint(frappe.db.get_single_value("Manufacturing Settings", "disable_capacity_planning")): - return + if not self.bom_no: + return if self.use_multi_level_bom: bom_list = frappe.get_doc("BOM", self.bom_no).traverse_tree() @@ -681,11 +718,13 @@ def make_stock_entry(work_order_id, purpose, qty=None): @frappe.whitelist() def get_default_warehouse(): - wip_warehouse = frappe.db.get_single_value("Manufacturing Settings", - "default_wip_warehouse") - fg_warehouse = frappe.db.get_single_value("Manufacturing Settings", - "default_fg_warehouse") - return {"wip_warehouse": wip_warehouse, "fg_warehouse": fg_warehouse} + doc = frappe.get_cached_doc("Manufacturing Settings") + + return { + "wip_warehouse": doc.default_wip_warehouse, + "fg_warehouse": doc.default_fg_warehouse, + "scrap_warehouse": doc.default_scrap_warehouse + } @frappe.whitelist() def stop_unstop(work_order, status): @@ -721,7 +760,7 @@ def make_job_card(work_order, operation, workstation, qty=0): if row: return create_job_card(work_order, row, qty) -def create_job_card(work_order, row, qty=0, auto_create=False): +def create_job_card(work_order, row, qty=0, enable_capacity_planning=False, auto_create=False): doc = frappe.new_doc("Job Card") doc.update({ 'work_order': work_order.name, @@ -741,6 +780,9 @@ def create_job_card(work_order, row, qty=0, auto_create=False): if auto_create: doc.flags.ignore_mandatory = True + if enable_capacity_planning: + doc.schedule_time_logs(row) + doc.insert() frappe.msgprint(_("Job card {0} created").format(doc.name)) diff --git a/erpnext/manufacturing/doctype/workstation/workstation.json b/erpnext/manufacturing/doctype/workstation/workstation.json index dca9891277f..d130391cece 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.json +++ b/erpnext/manufacturing/doctype/workstation/workstation.json @@ -1,466 +1,159 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:workstation_name", - "beta": 0, - "creation": "2013-01-10 16:34:17", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:workstation_name", + "creation": "2013-01-10 16:34:17", + "doctype": "DocType", + "document_type": "Setup", + "field_order": [ + "workstation_name", + "production_capacity", + "column_break_3", + "over_heads", + "hour_rate_electricity", + "hour_rate_consumable", + "column_break_11", + "hour_rate_rent", + "hour_rate_labour", + "hour_rate", + "working_hours_section", + "holiday_list", + "working_hours", + "workstaion_description", + "description" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "description_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": "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, - "unique": 0 - }, + "fieldname": "workstation_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Workstation Name", + "oldfieldname": "workstation_name", + "oldfieldtype": "Data", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "workstation_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": "Workstation Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "workstation_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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text", - "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": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "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, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text", + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Text", "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "over_heads", - "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": "Operating Costs", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section 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, - "unique": 0 - }, + "fieldname": "over_heads", + "fieldtype": "Section Break", + "label": "Operating Costs", + "oldfieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_rate_electricity", - "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": "Electricity Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_electricity", - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "bold": 1, + "description": "per hour", + "fieldname": "hour_rate_electricity", + "fieldtype": "Currency", + "label": "Electricity Cost", + "oldfieldname": "hour_rate_electricity", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_rate_consumable", - "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": "Consumable Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_consumable", - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "bold": 1, + "description": "per hour", + "fieldname": "hour_rate_consumable", + "fieldtype": "Currency", + "label": "Consumable Cost", + "oldfieldname": "hour_rate_consumable", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "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, - "unique": 0 - }, + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_rate_rent", - "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": "Rent Cost", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_rent", - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "bold": 1, + "description": "per hour", + "fieldname": "hour_rate_rent", + "fieldtype": "Currency", + "label": "Rent Cost", + "oldfieldname": "hour_rate_rent", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Wages per hour", - "fieldname": "hour_rate_labour", - "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": "Wages", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_rate_labour", - "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": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "bold": 1, + "description": "Wages per hour", + "fieldname": "hour_rate_labour", + "fieldtype": "Currency", + "label": "Wages", + "oldfieldname": "hour_rate_labour", + "oldfieldtype": "Currency" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "per hour", - "fieldname": "hour_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": "Net Hour Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "hour_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, - "unique": 0 - }, + "description": "per hour", + "fieldname": "hour_rate", + "fieldtype": "Currency", + "label": "Net Hour Rate", + "oldfieldname": "hour_rate", + "oldfieldtype": "Currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "working_hours_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": "Working Hours", - "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": "working_hours_section", + "fieldtype": "Section Break", + "label": "Working Hours" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "working_hours", - "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": "Working Hours", - "length": 0, - "no_copy": 0, - "options": "Workstation Working Hour", - "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": "working_hours", + "fieldtype": "Table", + "label": "Working Hours", + "options": "Workstation Working Hour" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "fieldname": "holiday_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": "Holiday List", - "length": 0, - "no_copy": 0, - "options": "Holiday List", - "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": "holiday_list", + "fieldtype": "Link", + "label": "Holiday List", + "options": "Holiday List" + }, + { + "default": "1", + "fieldname": "production_capacity", + "fieldtype": "Int", + "label": "Production Capacity", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "collapsible": 1, + "fieldname": "workstaion_description", + "fieldtype": "Section Break", + "label": "Description" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "icon-wrench", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2017-07-18 22:28:50.163219", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Workstation", - "owner": "Administrator", + ], + "icon": "icon-wrench", + "idx": 1, + "modified": "2019-11-26 12:39:19.742052", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Workstation", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 1, - "sort_order": "ASC", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 1, + "show_name_in_global_search": 1, + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/workstation/workstation.py b/erpnext/manufacturing/doctype/workstation/workstation.py index f6ab72ade49..3512e590458 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation.py +++ b/erpnext/manufacturing/doctype/workstation/workstation.py @@ -4,7 +4,9 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, cint, getdate, formatdate, comma_and, time_diff_in_seconds, to_timedelta +from erpnext.support.doctype.issue.issue import get_holidays +from frappe.utils import (flt, cint, getdate, formatdate, + comma_and, time_diff_in_seconds, to_timedelta, add_days) from frappe.model.document import Document from dateutil.parser import parse @@ -43,6 +45,17 @@ class Workstation(Document): where parent = %s and workstation = %s""", (self.hour_rate, bom_no[0], self.name)) + def validate_workstation_holiday(self, schedule_date, skip_holiday_list_check=False): + if not skip_holiday_list_check and (not self.holiday_list or + cint(frappe.db.get_single_value("Manufacturing Settings", "allow_production_on_holidays"))): + return schedule_date + + if schedule_date in tuple(get_holidays(self.holiday_list)): + schedule_date = add_days(schedule_date, 1) + self.validate_workstation_holiday(schedule_date, skip_holiday_list_check=True) + + return schedule_date + @frappe.whitelist() def get_default_holiday_list(): return frappe.get_cached_value('Company', frappe.defaults.get_user_default("Company"), "default_holiday_list") diff --git a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py index 9e0d1d17394..7f0124b5030 100644 --- a/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py +++ b/erpnext/manufacturing/doctype/workstation/workstation_dashboard.py @@ -6,8 +6,12 @@ def get_data(): 'fieldname': 'workstation', 'transactions': [ { - 'label': _('Manufacture'), - 'items': ['BOM', 'Routing', 'Work Order', 'Job Card', 'Operation', 'Timesheet'] + 'label': _('Master'), + 'items': ['BOM', 'Routing', 'Operation'] + }, + { + 'label': _('Transaction'), + 'items': ['Work Order', 'Job Card', 'Timesheet'] } ] } diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 07b646b0f82..daedca7a28c 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.set_production_capacity_in_workstation \ No newline at end of file diff --git a/erpnext/patches/v12_0/set_production_capacity_in_workstation.py b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py new file mode 100644 index 00000000000..bae1e28deb9 --- /dev/null +++ b/erpnext/patches/v12_0/set_production_capacity_in_workstation.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("manufacturing", "doctype", "workstation") + + frappe.db.sql(""" UPDATE `tabWorkstation` + SET production_capacity = 1 """) \ No newline at end of file From bf8fe06f86e513edc52a59da851180c7e48ebeb3 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 29 Nov 2019 14:14:11 +0530 Subject: [PATCH 306/679] fix: please specify customer error in sales invoice if patient is blank --- .../doctype/sales_invoice/sales_invoice.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 3c852106635..2ea74f6d854 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -789,22 +789,21 @@ frappe.ui.form.on('Sales Invoice', { method: "frappe.client.get_value", args:{ doctype: "Patient", - filters: {"name": frm.doc.patient}, + filters: { + "name": frm.doc.patient + }, fieldname: "customer" }, - callback:function(patient_customer) { - if(patient_customer){ - frm.set_value("customer", patient_customer.message.customer); - frm.refresh_fields(); + callback:function(r) { + if(r && r.message.customer){ + frm.set_value("customer", r.message.customer); } } }); } - else{ - frm.set_value("customer", ''); - } } }, + refresh: function(frm) { if (frappe.boot.active_domains.includes("Healthcare")){ frm.set_df_property("patient", "hidden", 0); From 92b9d7383d246ceca8447a1afe7e9712279b76a7 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 29 Nov 2019 14:22:52 +0530 Subject: [PATCH 307/679] 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 8287c3d05a67065c1e494d3679fc2e9c6ab13cb0 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 29 Nov 2019 15:52:29 +0530 Subject: [PATCH 308/679] feat(marketplace): unpublish item from hub --- erpnext/hub_node/api.py | 19 ++++++++++++++++++- erpnext/public/js/hub/pages/Item.vue | 16 +++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py index 0d01c67650b..b0b00b2b469 100644 --- a/erpnext/hub_node/api.py +++ b/erpnext/hub_node/api.py @@ -70,7 +70,7 @@ def map_fields(items): field_mappings = get_field_mappings() table_fields = [d.fieldname for d in frappe.get_meta('Item').get_table_fields()] - hub_seller_name = frappe.db.get_value('Marketplace Settings' , 'Marketplace Settings', 'hub_seller_name') + hub_seller_name = frappe.db.get_value('Marketplace Settings', 'Marketplace Settings', 'hub_seller_name') for item in items: for fieldname in table_fields: @@ -147,6 +147,23 @@ def publish_selected_items(items_to_publish): except Exception as e: frappe.log_error(message=e, title='Hub Sync Error') +@frappe.whitelist() +def unpublish_item(item): + ''' Remove item listing from the marketplace ''' + item = json.loads(item) + + item_code = item.get('item_code') + frappe.db.set_value('Item', item_code, 'publish_in_hub', 0) + + item = map_fields([item])[0] + + try: + connection = get_hub_connection() + connection.set_value('Hub Item', item.get('name'), 'published', 0) + + except Exception as e: + frappe.log_error(message=e, title='Hub Sync Error') + @frappe.whitelist() def get_unregistered_users(): settings = frappe.get_single('Marketplace Settings') diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue index 841d0046db8..285f6969268 100644 --- a/erpnext/public/js/hub/pages/Item.vue +++ b/erpnext/public/js/hub/pages/Item.vue @@ -300,7 +300,21 @@ export default { }, unpublish_item() { - frappe.msgprint(__('This feature is under development...')); + let me = this; + frappe.confirm(__(`Unpublish ${this.item.item_name}?`), function () { + frappe.call( + 'erpnext.hub_node.api.unpublish_item', + { + item: me.item + } + ) + .then((r) => { + frappe.set_route(`marketplace/home`); + frappe.show_alert(__('Item listing removed')) + + }) + + }) } } } From 45f57f273ca8d1367686cbbfeeb3d57669a9629d Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 26 Nov 2019 15:13:23 +0530 Subject: [PATCH 309/679] 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 c51dd2989eb6319bddd9a1c0e0750b1905da6a2b Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 26 Nov 2019 16:12:29 +0530 Subject: [PATCH 310/679] 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 20c31dd579f369b260777574994ad06a4f6c8d56 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 29 Nov 2019 16:55:10 +0530 Subject: [PATCH 311/679] fix: tax templates from all companies fetching in receipt (#19682) * fix: tax templates from all companies fetching in receipt * Update purchase_receipt.js --- .../stock/doctype/purchase_receipt/purchase_receipt.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js index d5914f9b28d..6b5e40e6288 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js @@ -34,6 +34,12 @@ frappe.ui.form.on("Purchase Receipt", { filters: {'company': frm.doc.company } } }); + + frm.set_query("taxes_and_charges", function() { + return { + filters: {'company': frm.doc.company } + } + }); }, onload: function(frm) { @@ -296,4 +302,4 @@ var validate_sample_quantity = function(frm, cdt, cdn) { } }); } -}; \ No newline at end of file +}; From 06a6fa4cfc88cbe6c05abe0000826b78ddecdfbf Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 29 Nov 2019 16:56:27 +0530 Subject: [PATCH 312/679] feat: Accounts Payable report based on payment terms (#19672) --- 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 14906f2c2e6..41989bf863e 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -318,7 +318,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 e3c05290c8f2aa58bfa7ab83723b1967dffbf1e2 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Fri, 29 Nov 2019 17:00:09 +0530 Subject: [PATCH 313/679] fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req (#19691) --- .../clinical_procedure_template.py | 5 +++-- .../doctype/inpatient_record/inpatient_record.py | 11 ++++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py index 141329b3db1..7cec3622001 100644 --- a/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py +++ b/erpnext/healthcare/doctype/clinical_procedure_template/clinical_procedure_template.py @@ -63,10 +63,11 @@ def updating_rate(self): item_code=%s""",(self.template, self.rate, self.item)) def create_item_from_template(doc): + disabled = 1 + if(doc.is_billable == 1): disabled = 0 - else: - disabled = 1 + #insert item item = frappe.get_doc({ "doctype": "Item", diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py index c107cd73350..835b38bedf8 100644 --- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py +++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import today, now_datetime +from frappe.utils import today, now_datetime, getdate from frappe.model.document import Document from frappe.desk.reportview import get_match_cond @@ -15,11 +15,20 @@ class InpatientRecord(Document): frappe.db.set_value("Patient", self.patient, "inpatient_record", self.name) def validate(self): + self.validate_dates() self.validate_already_scheduled_or_admitted() if self.status == "Discharged": frappe.db.set_value("Patient", self.patient, "inpatient_status", None) frappe.db.set_value("Patient", self.patient, "inpatient_record", None) + def validate_dates(self): + if (getdate(self.scheduled_date) < getdate(today())) or \ + (getdate(self.admitted_datetime) < getdate(today())): + frappe.throw(_("Scheduled and Admitted dates can not be less than today")) + if (getdate(self.expected_discharge) < getdate(self.scheduled_date)) or \ + (getdate(self.discharge_date) < getdate(self.scheduled_date)): + frappe.throw(_("Expected and Discharge dates cannot be less than Admission Schedule date")) + def validate_already_scheduled_or_admitted(self): query = """ select name, status From 750b3a594671151d7edeba770a9a5d5906085a73 Mon Sep 17 00:00:00 2001 From: Prasad Ramesh Date: Fri, 29 Nov 2019 17:30:47 +0530 Subject: [PATCH 315/679] fix: Button label case (#19706) Json -> JSON --- 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 96827682807..ce559218cbd 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -52,7 +52,7 @@ frappe.query_reports["GSTR-1"] = { ], onload: function (report) { - report.page.add_inner_button(__("Download as Json"), function () { + report.page.add_inner_button(__("Download as JSON"), function () { var filters = report.get_values(); const args = { From 0de066c3b190be6f8eac06f547feba0c0750226a Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Fri, 29 Nov 2019 13:02:17 +0100 Subject: [PATCH 316/679] feat(regional): Add master data to DATEV Export (#18755) * Add master data to export * add SQL statements to get customers and suppliers * make data category a string * fix SQL error * fix SQL errors * unique column names * add encoding of constants * get customer primary address and contact * fix typo * fix typo * binary response * add filename * add filecontent * rename account columns * exclude account groups * use compression, close file before transfer * fix StringIO * add basic tests * fix assertion, merge test methods * fix indentation * relative import of constants * fix path * import os * Add default currency to test company * root accounts with parent = null * move account-related things to setup() * add: test headers * company and filters become class properties * add: test csv creation * (fix): add missing account * (fix): remove wrong space * add items to sales invoice * refactor: create test data * fix: create cost center * fix: doctype Accoutn * fix: make sure account belongs to company * fix: remove customer group and territory, save on a new line * create default warehouses * fix: make Item myself * fix: item defaults are a list * fix: use my own warehouse * fix: use my own expense account * fix: let you take care of the Sales Invoice Item * fix: import zipfile * add TODOs * fix: workaround for pandas bug * SQL: utf-8 everywhere to make conversion in tests unnecessary * tests: zipfile must be encoded string * fix(tests): invalid start byte * fix(test): give is_zipfile() the file-like object it expects * fix(test): fix encoding of colums * fix(get_transactions): as_dict is 1 by default * fix(tests): allow empty data * refactor: rename columns in get_account_names * fix(pandas): keep sorting columns * fix: "lineterminator" must be a string * fix(test): check if cost center exists * fix: credit limit became a child table * fix: save company after creation * insert instead of save * tests: setup_fiscal_year * fix(test): import cstr * fix(tests): fiscal year * fix: can't concat str to bytes * fix: make csv-encoding work for py2 and py3 * fix(test): use frappe.as_unicode instead of unicode * fix: use BytesIO instead of StringIO for py3 compatibility * fix(tests): use BytesIO instead of StringIO for py3 compatibility --- erpnext/regional/report/datev/datev.py | 470 +++++++--------- .../regional/report/datev/datev_constants.py | 512 ++++++++++++++++++ erpnext/regional/report/datev/test_datev.py | 244 +++++++++ 3 files changed, 961 insertions(+), 265 deletions(-) create mode 100644 erpnext/regional/report/datev/datev_constants.py create mode 100644 erpnext/regional/report/datev/test_datev.py diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index ee8735fb1ff..bd70639ef28 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -10,17 +10,26 @@ Provide a report and downloadable CSV according to the German DATEV format. from __future__ import unicode_literals import datetime import json +import zlib +import zipfile +import six +from six import BytesIO from six import string_types import frappe from frappe import _ import pandas as pd +from .datev_constants import DataCategory +from .datev_constants import Transactions +from .datev_constants import DebtorsCreditors +from .datev_constants import AccountNames +from .datev_constants import QUERY_REPORT_COLUMNS def execute(filters=None): """Entry point for frappe.""" validate(filters) - result = get_gl_entries(filters, as_dict=0) - columns = get_columns() + result = get_transactions(filters, as_dict=0) + columns = QUERY_REPORT_COLUMNS return columns, result @@ -41,65 +50,8 @@ def validate(filters): except frappe.DoesNotExistError: frappe.throw(_('Please create DATEV Settings for Company {}.').format(filters.get('company'))) -def get_columns(): - """Return the list of columns that will be shown in query report.""" - columns = [ - { - "label": "Umsatz (ohne Soll/Haben-Kz)", - "fieldname": "Umsatz (ohne Soll/Haben-Kz)", - "fieldtype": "Currency", - }, - { - "label": "Soll/Haben-Kennzeichen", - "fieldname": "Soll/Haben-Kennzeichen", - "fieldtype": "Data", - }, - { - "label": "Kontonummer", - "fieldname": "Kontonummer", - "fieldtype": "Data", - }, - { - "label": "Gegenkonto (ohne BU-Schlüssel)", - "fieldname": "Gegenkonto (ohne BU-Schlüssel)", - "fieldtype": "Data", - }, - { - "label": "Belegdatum", - "fieldname": "Belegdatum", - "fieldtype": "Date", - }, - { - "label": "Buchungstext", - "fieldname": "Buchungstext", - "fieldtype": "Text", - }, - { - "label": "Beleginfo - Art 1", - "fieldname": "Beleginfo - Art 1", - "fieldtype": "Data", - }, - { - "label": "Beleginfo - Inhalt 1", - "fieldname": "Beleginfo - Inhalt 1", - "fieldtype": "Data", - }, - { - "label": "Beleginfo - Art 2", - "fieldname": "Beleginfo - Art 2", - "fieldtype": "Data", - }, - { - "label": "Beleginfo - Inhalt 2", - "fieldname": "Beleginfo - Inhalt 2", - "fieldtype": "Data", - } - ] - return columns - - -def get_gl_entries(filters, as_dict): +def get_transactions(filters, as_dict=1): """ Get a list of accounting entries. @@ -111,7 +63,7 @@ def get_gl_entries(filters, as_dict): as_dict -- return as list of dicts [0,1] """ gl_entries = frappe.db.sql(""" - select + SELECT /* either debit or credit amount; always positive */ case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)', @@ -132,7 +84,7 @@ def get_gl_entries(filters, as_dict): gl.against_voucher_type as 'Beleginfo - Art 2', gl.against_voucher as 'Beleginfo - Inhalt 2' - from `tabGL Entry` gl + FROM `tabGL Entry` gl /* Statistisches Konto (Debitoren/Kreditoren) */ left join `tabParty Account` pa @@ -155,15 +107,127 @@ def get_gl_entries(filters, as_dict): left join `tabAccount` acc_against_pa on pa.account = acc_against_pa.name - where gl.company = %(company)s - and DATE(gl.posting_date) >= %(from_date)s - and DATE(gl.posting_date) <= %(to_date)s - order by 'Belegdatum', gl.voucher_no""", filters, as_dict=as_dict) + WHERE gl.company = %(company)s + AND DATE(gl.posting_date) >= %(from_date)s + AND DATE(gl.posting_date) <= %(to_date)s + ORDER BY 'Belegdatum', gl.voucher_no""", filters, as_dict=as_dict, as_utf8=1) return gl_entries -def get_datev_csv(data, filters): +def get_customers(filters): + """ + Get a list of Customers. + + Arguments: + filters -- dict of filters to be passed to the sql query + """ + return frappe.db.sql(""" + SELECT + + acc.account_number as 'Konto', + cus.customer_name as 'Name (Adressatentyp Unternehmen)', + case cus.customer_type when 'Individual' then 1 when 'Company' then 2 else 0 end as 'Adressatentyp', + adr.address_line1 as 'Straße', + adr.pincode as 'Postleitzahl', + adr.city as 'Ort', + UPPER(country.code) as 'Land', + adr.address_line2 as 'Adresszusatz', + con.email_id as 'E-Mail', + coalesce(con.mobile_no, con.phone) as 'Telefon', + cus.website as 'Internet', + cus.tax_id as 'Steuernummer', + ccl.credit_limit as 'Kreditlimit (Debitor)' + + FROM `tabParty Account` par + + left join `tabAccount` acc + on acc.name = par.account + + left join `tabCustomer` cus + on cus.name = par.parent + + left join `tabAddress` adr + on adr.name = cus.customer_primary_address + + left join `tabCountry` country + on country.name = adr.country + + left join `tabContact` con + on con.name = cus.customer_primary_contact + + left join `tabCustomer Credit Limit` ccl + on ccl.parent = cus.name + and ccl.company = par.company + + WHERE par.company = %(company)s + AND par.parenttype = 'Customer'""", filters, as_dict=1, as_utf8=1) + + +def get_suppliers(filters): + """ + Get a list of Suppliers. + + Arguments: + filters -- dict of filters to be passed to the sql query + """ + return frappe.db.sql(""" + SELECT + + acc.account_number as 'Konto', + sup.supplier_name as 'Name (Adressatentyp Unternehmen)', + case sup.supplier_type when 'Individual' then '1' when 'Company' then '2' else '0' end as 'Adressatentyp', + adr.address_line1 as 'Straße', + adr.pincode as 'Postleitzahl', + adr.city as 'Ort', + UPPER(country.code) as 'Land', + adr.address_line2 as 'Adresszusatz', + con.email_id as 'E-Mail', + coalesce(con.mobile_no, con.phone) as 'Telefon', + sup.website as 'Internet', + sup.tax_id as 'Steuernummer', + case sup.on_hold when 1 then sup.release_date else null end as 'Zahlungssperre bis' + + FROM `tabParty Account` par + + left join `tabAccount` acc + on acc.name = par.account + + left join `tabSupplier` sup + on sup.name = par.parent + + left join `tabDynamic Link` dyn_adr + on dyn_adr.link_name = sup.name + and dyn_adr.link_doctype = 'Supplier' + and dyn_adr.parenttype = 'Address' + + left join `tabAddress` adr + on adr.name = dyn_adr.parent + and adr.is_primary_address = '1' + + left join `tabCountry` country + on country.name = adr.country + + left join `tabDynamic Link` dyn_con + on dyn_con.link_name = sup.name + and dyn_con.link_doctype = 'Supplier' + and dyn_con.parenttype = 'Contact' + + left join `tabContact` con + on con.name = dyn_con.parent + and con.is_primary_contact = '1' + + WHERE par.company = %(company)s + AND par.parenttype = 'Supplier'""", filters, as_dict=1, as_utf8=1) + + +def get_account_names(filters): + return frappe.get_list("Account", + fields=["account_number as Konto", "name as Kontenbeschriftung"], + filters={"company": filters.get("company"), "is_group": "0"}) + + +def get_datev_csv(data, filters, csv_class): """ Fill in missing columns and return a CSV in DATEV Format. @@ -174,7 +238,46 @@ def get_datev_csv(data, filters): Arguments: data -- array of dictionaries filters -- dict + csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS """ + header = get_header(filters, csv_class) + + empty_df = pd.DataFrame(columns=csv_class.COLUMNS) + data_df = pd.DataFrame.from_records(data) + + result = empty_df.append(data_df, sort=True) + + if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: + result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) + + if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: + result['Sprach-ID'] = 'de-DE' + + header = ';'.join(header).encode('latin_1') + data = result.to_csv( + # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 + sep=str(';'), + # European decimal seperator + decimal=',', + # Windows "ANSI" encoding + encoding='latin_1', + # format date as DDMM + date_format='%d%m', + # Windows line terminator + line_terminator='\r\n', + # Do not number rows + index=False, + # Use all columns defined above + columns=csv_class.COLUMNS + ) + + if not six.PY2: + data = data.encode('latin_1') + + return header + b'\r\n' + data + + +def get_header(filters, csv_class): header = [ # A = DATEV format # DTVF = created by DATEV software, @@ -185,18 +288,8 @@ def get_datev_csv(data, filters): # 510 = 5.10, # 720 = 7.20 "510", - # C = Data category - # 21 = Transaction batch (Buchungsstapel), - # 67 = Buchungstextkonstanten, - # 16 = Debitors/Creditors, - # 20 = Account names (Kontenbeschriftungen) - "21", - # D = Format name - # Buchungsstapel, - # Buchungstextkonstanten, - # Debitoren/Kreditoren, - # Kontenbeschriftungen - "Buchungsstapel", + csv_class.DATA_CATEGORY, + csv_class.FORMAT_NAME, # E = Format version (regarding format name) "", # F = Generated on @@ -224,16 +317,17 @@ def get_datev_csv(data, filters): # P = Transaction batch end date (YYYYMMDD) frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd"), # Q = Description (for example, "January - February 2019 Transactions") - "{} - {} Buchungsstapel".format( - frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"), - frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy") + "{} - {} {}".format( + frappe.utils.formatdate(filters.get('from_date'), "MMMM yyyy"), + frappe.utils.formatdate(filters.get('to_date'), "MMMM yyyy"), + csv_class.FORMAT_NAME ), # R = Diktatkürzel "", # S = Buchungstyp # 1 = Transaction batch (Buchungsstapel), # 2 = Annual financial statement (Jahresabschluss) - "1", + "1" if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else "", # T = Rechnungslegungszweck "", # U = Festschreibung @@ -241,185 +335,8 @@ def get_datev_csv(data, filters): # V = Kontoführungs-Währungskennzeichen des Geldkontos frappe.get_value("Company", filters.get("company"), "default_currency") ] - columns = [ - # All possible columns must tbe listed here, because DATEV requires them to - # be present in the CSV. - # --- - # Umsatz - "Umsatz (ohne Soll/Haben-Kz)", - "Soll/Haben-Kennzeichen", - "WKZ Umsatz", - "Kurs", - "Basis-Umsatz", - "WKZ Basis-Umsatz", - # Konto/Gegenkonto - "Kontonummer", - "Gegenkonto (ohne BU-Schlüssel)", - "BU-Schlüssel", - # Datum - "Belegdatum", - # Belegfelder - "Belegfeld 1", - "Belegfeld 2", - # Weitere Felder - "Skonto", - "Buchungstext", - # OPOS-Informationen - "Postensperre", - "Diverse Adressnummer", - "Geschäftspartnerbank", - "Sachverhalt", - "Zinssperre", - # Digitaler Beleg - "Beleglink", - # Beleginfo - "Beleginfo - Art 1", - "Beleginfo - Inhalt 1", - "Beleginfo - Art 2", - "Beleginfo - Inhalt 2", - "Beleginfo - Art 3", - "Beleginfo - Inhalt 3", - "Beleginfo - Art 4", - "Beleginfo - Inhalt 4", - "Beleginfo - Art 5", - "Beleginfo - Inhalt 5", - "Beleginfo - Art 6", - "Beleginfo - Inhalt 6", - "Beleginfo - Art 7", - "Beleginfo - Inhalt 7", - "Beleginfo - Art 8", - "Beleginfo - Inhalt 8", - # Kostenrechnung - "Kost 1 - Kostenstelle", - "Kost 2 - Kostenstelle", - "Kost-Menge", - # Steuerrechnung - "EU-Land u. UStID", - "EU-Steuersatz", - "Abw. Versteuerungsart", - # L+L Sachverhalt - "Sachverhalt L+L", - "Funktionsergänzung L+L", - # Funktion Steuerschlüssel 49 - "BU 49 Hauptfunktionstyp", - "BU 49 Hauptfunktionsnummer", - "BU 49 Funktionsergänzung", - # Zusatzinformationen - "Zusatzinformation - Art 1", - "Zusatzinformation - Inhalt 1", - "Zusatzinformation - Art 2", - "Zusatzinformation - Inhalt 2", - "Zusatzinformation - Art 3", - "Zusatzinformation - Inhalt 3", - "Zusatzinformation - Art 4", - "Zusatzinformation - Inhalt 4", - "Zusatzinformation - Art 5", - "Zusatzinformation - Inhalt 5", - "Zusatzinformation - Art 6", - "Zusatzinformation - Inhalt 6", - "Zusatzinformation - Art 7", - "Zusatzinformation - Inhalt 7", - "Zusatzinformation - Art 8", - "Zusatzinformation - Inhalt 8", - "Zusatzinformation - Art 9", - "Zusatzinformation - Inhalt 9", - "Zusatzinformation - Art 10", - "Zusatzinformation - Inhalt 10", - "Zusatzinformation - Art 11", - "Zusatzinformation - Inhalt 11", - "Zusatzinformation - Art 12", - "Zusatzinformation - Inhalt 12", - "Zusatzinformation - Art 13", - "Zusatzinformation - Inhalt 13", - "Zusatzinformation - Art 14", - "Zusatzinformation - Inhalt 14", - "Zusatzinformation - Art 15", - "Zusatzinformation - Inhalt 15", - "Zusatzinformation - Art 16", - "Zusatzinformation - Inhalt 16", - "Zusatzinformation - Art 17", - "Zusatzinformation - Inhalt 17", - "Zusatzinformation - Art 18", - "Zusatzinformation - Inhalt 18", - "Zusatzinformation - Art 19", - "Zusatzinformation - Inhalt 19", - "Zusatzinformation - Art 20", - "Zusatzinformation - Inhalt 20", - # Mengenfelder LuF - "Stück", - "Gewicht", - # Forderungsart - "Zahlweise", - "Forderungsart", - "Veranlagungsjahr", - "Zugeordnete Fälligkeit", - # Weitere Felder - "Skontotyp", - # Anzahlungen - "Auftragsnummer", - "Buchungstyp", - "USt-Schlüssel (Anzahlungen)", - "EU-Land (Anzahlungen)", - "Sachverhalt L+L (Anzahlungen)", - "EU-Steuersatz (Anzahlungen)", - "Erlöskonto (Anzahlungen)", - # Stapelinformationen - "Herkunft-Kz", - # Technische Identifikation - "Buchungs GUID", - # Kostenrechnung - "Kost-Datum", - # OPOS-Informationen - "SEPA-Mandatsreferenz", - "Skontosperre", - # Gesellschafter und Sonderbilanzsachverhalt - "Gesellschaftername", - "Beteiligtennummer", - "Identifikationsnummer", - "Zeichnernummer", - # OPOS-Informationen - "Postensperre bis", - # Gesellschafter und Sonderbilanzsachverhalt - "Bezeichnung SoBil-Sachverhalt", - "Kennzeichen SoBil-Buchung", - # Stapelinformationen - "Festschreibung", - # Datum - "Leistungsdatum", - "Datum Zuord. Steuerperiode", - # OPOS-Informationen - "Fälligkeit", - # Konto/Gegenkonto - "Generalumkehr (GU)", - # Steuersatz für Steuerschlüssel - "Steuersatz", - "Land" - ] + return header - empty_df = pd.DataFrame(columns=columns) - data_df = pd.DataFrame.from_records(data) - - result = empty_df.append(data_df) - result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) - - header = ';'.join(header).encode('latin_1') - data = result.to_csv( - sep=b';', - # European decimal seperator - decimal=',', - # Windows "ANSI" encoding - encoding='latin_1', - # format date as DDMM - date_format='%d%m', - # Windows line terminator - line_terminator=b'\r\n', - # Do not number rows - index=False, - # Use all columns defined above - columns=columns - ) - - return header + b'\r\n' + data @frappe.whitelist() def download_datev_csv(filters=None): @@ -438,8 +355,31 @@ def download_datev_csv(filters=None): filters = json.loads(filters) validate(filters) - data = get_gl_entries(filters, as_dict=1) - frappe.response['result'] = get_datev_csv(data, filters) - frappe.response['doctype'] = 'EXTF_Buchungsstapel' - frappe.response['type'] = 'csv' + # This is where my zip will be written + zip_buffer = BytesIO() + # This is my zip file + datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + + transactions = get_transactions(filters) + transactions_csv = get_datev_csv(transactions, filters, csv_class=Transactions) + datev_zip.writestr('EXTF_Buchungsstapel.csv', transactions_csv) + + account_names = get_account_names(filters) + account_names_csv = get_datev_csv(account_names, filters, csv_class=AccountNames) + datev_zip.writestr('EXTF_Kontenbeschriftungen.csv', account_names_csv) + + customers = get_customers(filters) + customers_csv = get_datev_csv(customers, filters, csv_class=DebtorsCreditors) + datev_zip.writestr('EXTF_Kunden.csv', customers_csv) + + suppliers = get_suppliers(filters) + suppliers_csv = get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) + datev_zip.writestr('EXTF_Lieferanten.csv', suppliers_csv) + + # You must call close() before exiting your program or essential records will not be written. + datev_zip.close() + + frappe.response['filecontent'] = zip_buffer.getvalue() + frappe.response['filename'] = 'DATEV.zip' + frappe.response['type'] = 'binary' diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/report/datev/datev_constants.py new file mode 100644 index 00000000000..1c9bd23ee12 --- /dev/null +++ b/erpnext/regional/report/datev/datev_constants.py @@ -0,0 +1,512 @@ +# coding: utf-8 +"""Constants used in datev.py.""" + +TRANSACTION_COLUMNS = [ + # All possible columns must tbe listed here, because DATEV requires them to + # be present in the CSV. + # --- + # Umsatz + "Umsatz (ohne Soll/Haben-Kz)", + "Soll/Haben-Kennzeichen", + "WKZ Umsatz", + "Kurs", + "Basis-Umsatz", + "WKZ Basis-Umsatz", + # Konto/Gegenkonto + "Kontonummer", + "Gegenkonto (ohne BU-Schlüssel)", + "BU-Schlüssel", + # Datum + "Belegdatum", + # Belegfelder + "Belegfeld 1", + "Belegfeld 2", + # Weitere Felder + "Skonto", + "Buchungstext", + # OPOS-Informationen + "Postensperre", + "Diverse Adressnummer", + "Geschäftspartnerbank", + "Sachverhalt", + "Zinssperre", + # Digitaler Beleg + "Beleglink", + # Beleginfo + "Beleginfo - Art 1", + "Beleginfo - Inhalt 1", + "Beleginfo - Art 2", + "Beleginfo - Inhalt 2", + "Beleginfo - Art 3", + "Beleginfo - Inhalt 3", + "Beleginfo - Art 4", + "Beleginfo - Inhalt 4", + "Beleginfo - Art 5", + "Beleginfo - Inhalt 5", + "Beleginfo - Art 6", + "Beleginfo - Inhalt 6", + "Beleginfo - Art 7", + "Beleginfo - Inhalt 7", + "Beleginfo - Art 8", + "Beleginfo - Inhalt 8", + # Kostenrechnung + "Kost 1 - Kostenstelle", + "Kost 2 - Kostenstelle", + "Kost-Menge", + # Steuerrechnung + "EU-Land u. UStID", + "EU-Steuersatz", + "Abw. Versteuerungsart", + # L+L Sachverhalt + "Sachverhalt L+L", + "Funktionsergänzung L+L", + # Funktion Steuerschlüssel 49 + "BU 49 Hauptfunktionstyp", + "BU 49 Hauptfunktionsnummer", + "BU 49 Funktionsergänzung", + # Zusatzinformationen + "Zusatzinformation - Art 1", + "Zusatzinformation - Inhalt 1", + "Zusatzinformation - Art 2", + "Zusatzinformation - Inhalt 2", + "Zusatzinformation - Art 3", + "Zusatzinformation - Inhalt 3", + "Zusatzinformation - Art 4", + "Zusatzinformation - Inhalt 4", + "Zusatzinformation - Art 5", + "Zusatzinformation - Inhalt 5", + "Zusatzinformation - Art 6", + "Zusatzinformation - Inhalt 6", + "Zusatzinformation - Art 7", + "Zusatzinformation - Inhalt 7", + "Zusatzinformation - Art 8", + "Zusatzinformation - Inhalt 8", + "Zusatzinformation - Art 9", + "Zusatzinformation - Inhalt 9", + "Zusatzinformation - Art 10", + "Zusatzinformation - Inhalt 10", + "Zusatzinformation - Art 11", + "Zusatzinformation - Inhalt 11", + "Zusatzinformation - Art 12", + "Zusatzinformation - Inhalt 12", + "Zusatzinformation - Art 13", + "Zusatzinformation - Inhalt 13", + "Zusatzinformation - Art 14", + "Zusatzinformation - Inhalt 14", + "Zusatzinformation - Art 15", + "Zusatzinformation - Inhalt 15", + "Zusatzinformation - Art 16", + "Zusatzinformation - Inhalt 16", + "Zusatzinformation - Art 17", + "Zusatzinformation - Inhalt 17", + "Zusatzinformation - Art 18", + "Zusatzinformation - Inhalt 18", + "Zusatzinformation - Art 19", + "Zusatzinformation - Inhalt 19", + "Zusatzinformation - Art 20", + "Zusatzinformation - Inhalt 20", + # Mengenfelder LuF + "Stück", + "Gewicht", + # Forderungsart + "Zahlweise", + "Forderungsart", + "Veranlagungsjahr", + "Zugeordnete Fälligkeit", + # Weitere Felder + "Skontotyp", + # Anzahlungen + "Auftragsnummer", + "Buchungstyp", + "USt-Schlüssel (Anzahlungen)", + "EU-Land (Anzahlungen)", + "Sachverhalt L+L (Anzahlungen)", + "EU-Steuersatz (Anzahlungen)", + "Erlöskonto (Anzahlungen)", + # Stapelinformationen + "Herkunft-Kz", + # Technische Identifikation + "Buchungs GUID", + # Kostenrechnung + "Kost-Datum", + # OPOS-Informationen + "SEPA-Mandatsreferenz", + "Skontosperre", + # Gesellschafter und Sonderbilanzsachverhalt + "Gesellschaftername", + "Beteiligtennummer", + "Identifikationsnummer", + "Zeichnernummer", + # OPOS-Informationen + "Postensperre bis", + # Gesellschafter und Sonderbilanzsachverhalt + "Bezeichnung SoBil-Sachverhalt", + "Kennzeichen SoBil-Buchung", + # Stapelinformationen + "Festschreibung", + # Datum + "Leistungsdatum", + "Datum Zuord. Steuerperiode", + # OPOS-Informationen + "Fälligkeit", + # Konto/Gegenkonto + "Generalumkehr (GU)", + # Steuersatz für Steuerschlüssel + "Steuersatz", + "Land" +] + +DEBTOR_CREDITOR_COLUMNS = [ + # All possible columns must tbe listed here, because DATEV requires them to + # be present in the CSV. + # Columns "Leerfeld" have been replaced with "Leerfeld #" to not confuse pandas + # --- + "Konto", + "Name (Adressatentyp Unternehmen)", + "Unternehmensgegenstand", + "Name (Adressatentyp natürl. Person)", + "Vorname (Adressatentyp natürl. Person)", + "Name (Adressatentyp keine Angabe)", + "Adressatentyp", + "Kurzbezeichnung", + "EU-Land", + "EU-USt-IdNr.", + "Anrede", + "Titel/Akad. Grad", + "Adelstitel", + "Namensvorsatz", + "Adressart", + "Straße", + "Postfach", + "Postleitzahl", + "Ort", + "Land", + "Versandzusatz", + "Adresszusatz", + "Abweichende Anrede", + "Abw. Zustellbezeichnung 1", + "Abw. Zustellbezeichnung 2", + "Kennz. Korrespondenzadresse", + "Adresse gültig von", + "Adresse gültig bis", + "Telefon", + "Bemerkung (Telefon)", + "Telefon Geschäftsleitung", + "Bemerkung (Telefon GL)", + "E-Mail", + "Bemerkung (E-Mail)", + "Internet", + "Bemerkung (Internet)", + "Fax", + "Bemerkung (Fax)", + "Sonstige", + "Bemerkung (Sonstige)", + "Bankleitzahl 1", + "Bankbezeichnung 1", + "Bankkonto-Nummer 1", + "Länderkennzeichen 1", + "IBAN 1", + "Leerfeld 1", + "SWIFT-Code 1", + "Abw. Kontoinhaber 1", + "Kennz. Haupt-Bankverb. 1", + "Bankverb. 1 Gültig von", + "Bankverb. 1 Gültig bis", + "Bankleitzahl 2", + "Bankbezeichnung 2", + "Bankkonto-Nummer 2", + "Länderkennzeichen 2", + "IBAN 2", + "Leerfeld 2", + "SWIFT-Code 2", + "Abw. Kontoinhaber 2", + "Kennz. Haupt-Bankverb. 2", + "Bankverb. 2 gültig von", + "Bankverb. 2 gültig bis", + "Bankleitzahl 3", + "Bankbezeichnung 3", + "Bankkonto-Nummer 3", + "Länderkennzeichen 3", + "IBAN 3", + "Leerfeld 3", + "SWIFT-Code 3", + "Abw. Kontoinhaber 3", + "Kennz. Haupt-Bankverb. 3", + "Bankverb. 3 gültig von", + "Bankverb. 3 gültig bis", + "Bankleitzahl 4", + "Bankbezeichnung 4", + "Bankkonto-Nummer 4", + "Länderkennzeichen 4", + "IBAN 4", + "Leerfeld 4", + "SWIFT-Code 4", + "Abw. Kontoinhaber 4", + "Kennz. Haupt-Bankverb. 4", + "Bankverb. 4 Gültig von", + "Bankverb. 4 Gültig bis", + "Bankleitzahl 5", + "Bankbezeichnung 5", + "Bankkonto-Nummer 5", + "Länderkennzeichen 5", + "IBAN 5", + "Leerfeld 5", + "SWIFT-Code 5", + "Abw. Kontoinhaber 5", + "Kennz. Haupt-Bankverb. 5", + "Bankverb. 5 gültig von", + "Bankverb. 5 gültig bis", + "Leerfeld 6", + "Briefanrede", + "Grußformel", + "Kundennummer", + "Steuernummer", + "Sprache", + "Ansprechpartner", + "Vertreter", + "Sachbearbeiter", + "Diverse-Konto", + "Ausgabeziel", + "Währungssteuerung", + "Kreditlimit (Debitor)", + "Zahlungsbedingung", + "Fälligkeit in Tagen (Debitor)", + "Skonto in Prozent (Debitor)", + "Kreditoren-Ziel 1 (Tage)", + "Kreditoren-Skonto 1 (%)", + "Kreditoren-Ziel 2 (Tage)", + "Kreditoren-Skonto 2 (%)", + "Kreditoren-Ziel 3 Brutto (Tage)", + "Kreditoren-Ziel 4 (Tage)", + "Kreditoren-Skonto 4 (%)", + "Kreditoren-Ziel 5 (Tage)", + "Kreditoren-Skonto 5 (%)", + "Mahnung", + "Kontoauszug", + "Mahntext 1", + "Mahntext 2", + "Mahntext 3", + "Kontoauszugstext", + "Mahnlimit Betrag", + "Mahnlimit %", + "Zinsberechnung", + "Mahnzinssatz 1", + "Mahnzinssatz 2", + "Mahnzinssatz 3", + "Lastschrift", + "Verfahren", + "Mandantenbank", + "Zahlungsträger", + "Indiv. Feld 1", + "Indiv. Feld 2", + "Indiv. Feld 3", + "Indiv. Feld 4", + "Indiv. Feld 5", + "Indiv. Feld 6", + "Indiv. Feld 7", + "Indiv. Feld 8", + "Indiv. Feld 9", + "Indiv. Feld 10", + "Indiv. Feld 11", + "Indiv. Feld 12", + "Indiv. Feld 13", + "Indiv. Feld 14", + "Indiv. Feld 15", + "Abweichende Anrede (Rechnungsadresse)", + "Adressart (Rechnungsadresse)", + "Straße (Rechnungsadresse)", + "Postfach (Rechnungsadresse)", + "Postleitzahl (Rechnungsadresse)", + "Ort (Rechnungsadresse)", + "Land (Rechnungsadresse)", + "Versandzusatz (Rechnungsadresse)", + "Adresszusatz (Rechnungsadresse)", + "Abw. Zustellbezeichnung 1 (Rechnungsadresse)", + "Abw. Zustellbezeichnung 2 (Rechnungsadresse)", + "Adresse Gültig von (Rechnungsadresse)", + "Adresse Gültig bis (Rechnungsadresse)", + "Bankleitzahl 6", + "Bankbezeichnung 6", + "Bankkonto-Nummer 6", + "Länderkennzeichen 6", + "IBAN 6", + "Leerfeld 7", + "SWIFT-Code 6", + "Abw. Kontoinhaber 6", + "Kennz. Haupt-Bankverb. 6", + "Bankverb 6 gültig von", + "Bankverb 6 gültig bis", + "Bankleitzahl 7", + "Bankbezeichnung 7", + "Bankkonto-Nummer 7", + "Länderkennzeichen 7", + "IBAN 7", + "Leerfeld 8", + "SWIFT-Code 7", + "Abw. Kontoinhaber 7", + "Kennz. Haupt-Bankverb. 7", + "Bankverb 7 gültig von", + "Bankverb 7 gültig bis", + "Bankleitzahl 8", + "Bankbezeichnung 8", + "Bankkonto-Nummer 8", + "Länderkennzeichen 8", + "IBAN 8", + "Leerfeld 9", + "SWIFT-Code 8", + "Abw. Kontoinhaber 8", + "Kennz. Haupt-Bankverb. 8", + "Bankverb 8 gültig von", + "Bankverb 8 gültig bis", + "Bankleitzahl 9", + "Bankbezeichnung 9", + "Bankkonto-Nummer 9", + "Länderkennzeichen 9", + "IBAN 9", + "Leerfeld 10", + "SWIFT-Code 9", + "Abw. Kontoinhaber 9", + "Kennz. Haupt-Bankverb. 9", + "Bankverb 9 gültig von", + "Bankverb 9 gültig bis", + "Bankleitzahl 10", + "Bankbezeichnung 10", + "Bankkonto-Nummer 10", + "Länderkennzeichen 10", + "IBAN 10", + "Leerfeld 11", + "SWIFT-Code 10", + "Abw. Kontoinhaber 10", + "Kennz. Haupt-Bankverb. 10", + "Bankverb 10 gültig von", + "Bankverb 10 gültig bis", + "Nummer Fremdsystem", + "Insolvent", + "SEPA-Mandatsreferenz 1", + "SEPA-Mandatsreferenz 2", + "SEPA-Mandatsreferenz 3", + "SEPA-Mandatsreferenz 4", + "SEPA-Mandatsreferenz 5", + "SEPA-Mandatsreferenz 6", + "SEPA-Mandatsreferenz 7", + "SEPA-Mandatsreferenz 8", + "SEPA-Mandatsreferenz 9", + "SEPA-Mandatsreferenz 10", + "Verknüpftes OPOS-Konto", + "Mahnsperre bis", + "Lastschriftsperre bis", + "Zahlungssperre bis", + "Gebührenberechnung", + "Mahngebühr 1", + "Mahngebühr 2", + "Mahngebühr 3", + "Pauschalberechnung", + "Verzugspauschale 1", + "Verzugspauschale 2", + "Verzugspauschale 3", + "Alternativer Suchname", + "Status", + "Anschrift manuell geändert (Korrespondenzadresse)", + "Anschrift individuell (Korrespondenzadresse)", + "Anschrift manuell geändert (Rechnungsadresse)", + "Anschrift individuell (Rechnungsadresse)", + "Fristberechnung bei Debitor", + "Mahnfrist 1", + "Mahnfrist 2", + "Mahnfrist 3", + "Letzte Frist" +] + +ACCOUNT_NAME_COLUMNS = [ + # Account number + "Konto", + # Account name + "Kontenbeschriftung", + # Language of the account name + # "de-DE" or "en-GB" + "Sprach-ID" +] + +QUERY_REPORT_COLUMNS = [ + { + "label": "Umsatz (ohne Soll/Haben-Kz)", + "fieldname": "Umsatz (ohne Soll/Haben-Kz)", + "fieldtype": "Currency", + }, + { + "label": "Soll/Haben-Kennzeichen", + "fieldname": "Soll/Haben-Kennzeichen", + "fieldtype": "Data", + }, + { + "label": "Kontonummer", + "fieldname": "Kontonummer", + "fieldtype": "Data", + }, + { + "label": "Gegenkonto (ohne BU-Schlüssel)", + "fieldname": "Gegenkonto (ohne BU-Schlüssel)", + "fieldtype": "Data", + }, + { + "label": "Belegdatum", + "fieldname": "Belegdatum", + "fieldtype": "Date", + }, + { + "label": "Buchungstext", + "fieldname": "Buchungstext", + "fieldtype": "Text", + }, + { + "label": "Beleginfo - Art 1", + "fieldname": "Beleginfo - Art 1", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Inhalt 1", + "fieldname": "Beleginfo - Inhalt 1", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Art 2", + "fieldname": "Beleginfo - Art 2", + "fieldtype": "Data", + }, + { + "label": "Beleginfo - Inhalt 2", + "fieldname": "Beleginfo - Inhalt 2", + "fieldtype": "Data", + } +] + +class DataCategory(): + """Field of the CSV Header.""" + + DEBTORS_CREDITORS = "16" + ACCOUNT_NAMES = "20" + TRANSACTIONS = "21" + POSTING_TEXT_CONSTANTS = "67" + +class FormatName(): + """Field of the CSV Header, corresponds to DataCategory.""" + + DEBTORS_CREDITORS = "Debitoren/Kreditoren" + ACCOUNT_NAMES = "Kontenbeschriftungen" + TRANSACTIONS = "Buchungsstapel" + POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten" + +class Transactions(): + DATA_CATEGORY = DataCategory.TRANSACTIONS + FORMAT_NAME = FormatName.TRANSACTIONS + COLUMNS = TRANSACTION_COLUMNS + +class DebtorsCreditors(): + DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS + FORMAT_NAME = FormatName.DEBTORS_CREDITORS + COLUMNS = DEBTOR_CREDITOR_COLUMNS + +class AccountNames(): + DATA_CATEGORY = DataCategory.ACCOUNT_NAMES + FORMAT_NAME = FormatName.ACCOUNT_NAMES + COLUMNS = ACCOUNT_NAME_COLUMNS diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py new file mode 100644 index 00000000000..3cc65fe9d3a --- /dev/null +++ b/erpnext/regional/report/datev/test_datev.py @@ -0,0 +1,244 @@ +# coding=utf-8 +from __future__ import unicode_literals + +import os +import json +import zipfile +from six import BytesIO +from unittest import TestCase + +import frappe +from frappe.utils import getdate, today, now_datetime, cstr +from frappe.test_runner import make_test_objects +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts + +from erpnext.regional.report.datev.datev import validate +from erpnext.regional.report.datev.datev import get_transactions +from erpnext.regional.report.datev.datev import get_customers +from erpnext.regional.report.datev.datev import get_suppliers +from erpnext.regional.report.datev.datev import get_account_names +from erpnext.regional.report.datev.datev import get_datev_csv +from erpnext.regional.report.datev.datev import get_header +from erpnext.regional.report.datev.datev import download_datev_csv + +from erpnext.regional.report.datev.datev_constants import DataCategory +from erpnext.regional.report.datev.datev_constants import Transactions +from erpnext.regional.report.datev.datev_constants import DebtorsCreditors +from erpnext.regional.report.datev.datev_constants import AccountNames +from erpnext.regional.report.datev.datev_constants import QUERY_REPORT_COLUMNS + +def make_company(company_name, abbr): + if not frappe.db.exists("Company", company_name): + company = frappe.get_doc({ + "doctype": "Company", + "company_name": company_name, + "abbr": abbr, + "default_currency": "EUR", + "country": "Germany", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "SKR04 mit Kontonummern" + }) + company.insert() + else: + company = frappe.get_doc("Company", company_name) + + # indempotent + company.create_default_warehouses() + + if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}): + company.create_default_cost_center() + + company.save() + return company + +def setup_fiscal_year(): + fiscal_year = None + year = cstr(now_datetime().year) + if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"): + try: + fiscal_year = frappe.get_doc({ + "doctype": "Fiscal Year", + "year": year, + "year_start_date": "{0}-01-01".format(year), + "year_end_date": "{0}-12-31".format(year) + }) + fiscal_year.insert() + except frappe.NameError: + pass + + if fiscal_year: + fiscal_year.set_as_default() + +def make_customer_with_account(customer_name, company): + acc_name = frappe.db.get_value("Account", { + "account_name": customer_name, + "company": company.name + }, "name") + + if not acc_name: + acc = frappe.get_doc({ + "doctype": "Account", + "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG", + "account_name": customer_name, + "company": company.name, + "account_type": "Receivable", + "account_number": "10001" + }) + acc.insert() + acc_name = acc.name + + if not frappe.db.exists("Customer", customer_name): + customer = frappe.get_doc({ + "doctype": "Customer", + "customer_name": customer_name, + "customer_type": "Company", + "accounts": [{ + "company": company.name, + "account": acc_name + }] + }) + customer.insert() + else: + customer = frappe.get_doc("Customer", customer_name) + + return customer + +def make_item(item_code, company): + warehouse_name = frappe.db.get_value("Warehouse", { + "warehouse_name": "Stores", + "company": company.name + }, "name") + + if not frappe.db.exists("Item", item_code): + item = frappe.get_doc({ + "doctype": "Item", + "item_code": item_code, + "item_name": item_code, + "description": item_code, + "item_group": "All Item Groups", + "is_stock_item": 0, + "is_purchase_item": 0, + "is_customer_provided_item": 0, + "item_defaults": [{ + "default_warehouse": warehouse_name, + "company": company.name + }] + }) + item.insert() + else: + item = frappe.get_doc("Item", item_code) + return item + +def make_datev_settings(company): + if not frappe.db.exists("DATEV Settings", company.name): + frappe.get_doc({ + "doctype": "DATEV Settings", + "client": company.name, + "client_number": "12345", + "consultant_number": "67890" + }).insert() + + +class TestDatev(TestCase): + def setUp(self): + self.company = make_company("_Test GmbH", "_TG") + self.customer = make_customer_with_account("_Test Kunde GmbH", self.company) + self.filters = { + "company": self.company.name, + "from_date": today(), + "to_date": today() + } + + make_datev_settings(self.company) + item = make_item("_Test Item", self.company) + setup_fiscal_year() + + warehouse = frappe.db.get_value("Item Default", { + "parent": item.name, + "company": self.company.name + }, "default_warehouse") + + income_account = frappe.db.get_value("Account", { + "account_number": "4200", + "company": self.company.name + }, "name") + + tax_account = frappe.db.get_value("Account", { + "account_number": "3806", + "company": self.company.name + }, "name") + + si = create_sales_invoice( + company=self.company.name, + customer=self.customer.name, + currency=self.company.default_currency, + debit_to=self.customer.accounts[0].account, + income_account="4200 - Erlöse - _TG", + expense_account="6990 - Herstellungskosten - _TG", + cost_center=self.company.cost_center, + warehouse=warehouse, + item=item.name, + do_not_save=1 + ) + + si.append("taxes", { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": "Umsatzsteuer 19 %", + "rate": 19 + }) + + si.save() + si.submit() + + def test_columns(self): + def is_subset(get_data, allowed_keys): + """ + Validate that the dict contains only allowed keys. + + Params: + get_data -- Function that returns a list of dicts. + allowed_keys -- List of allowed keys + """ + data = get_data(self.filters) + if data == []: + # No data and, therefore, no columns is okay + return True + actual_set = set(data[0].keys()) + # allowed set must be interpreted as unicode to match the actual set + allowed_set = set({frappe.as_unicode(key) for key in allowed_keys}) + return actual_set.issubset(allowed_set) + + self.assertTrue(is_subset(get_transactions, Transactions.COLUMNS)) + self.assertTrue(is_subset(get_customers, DebtorsCreditors.COLUMNS)) + self.assertTrue(is_subset(get_suppliers, DebtorsCreditors.COLUMNS)) + self.assertTrue(is_subset(get_account_names, AccountNames.COLUMNS)) + + def test_header(self): + self.assertTrue(Transactions.DATA_CATEGORY in get_header(self.filters, Transactions)) + self.assertTrue(AccountNames.DATA_CATEGORY in get_header(self.filters, AccountNames)) + self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors)) + + def test_csv(self): + test_data = [{ + "Umsatz (ohne Soll/Haben-Kz)": 100, + "Soll/Haben-Kennzeichen": "H", + "Kontonummer": "4200", + "Gegenkonto (ohne BU-Schlüssel)": "10000", + "Belegdatum": today(), + "Buchungstext": "No remark", + "Beleginfo - Art 1": "Sales Invoice", + "Beleginfo - Inhalt 1": "SINV-0001" + }] + get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions) + + def test_download(self): + """Assert that the returned file is a ZIP file.""" + download_datev_csv(self.filters) + + # zipfile.is_zipfile() expects a file-like object + zip_buffer = BytesIO() + zip_buffer.write(frappe.response['filecontent']) + + self.assertTrue(zipfile.is_zipfile(zip_buffer)) From 10017c14f3c976fe1ef579c76289f1ee1cf4be21 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 29 Nov 2019 17:33:10 +0530 Subject: [PATCH 317/679] feat: HSN Code wise Item Tax (#19478) * feat: HSN Code wise Item Tax * feat: enqueued task of updating all items --- .../doctype/gst_hsn_code/gst_hsn_code.js | 24 ++- .../doctype/gst_hsn_code/gst_hsn_code.json | 138 +++++------------- .../doctype/gst_hsn_code/gst_hsn_code.py | 19 +++ erpnext/stock/doctype/item/item.js | 14 ++ 4 files changed, 95 insertions(+), 100 deletions(-) diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js index e7cc91952d8..7ff4de48639 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.js @@ -3,6 +3,26 @@ frappe.ui.form.on('GST HSN Code', { refresh: function(frm) { - + if(! frm.doc.__islocal && frm.doc.taxes.length){ + frm.add_custom_button(__('Update Taxes for Items'), function(){ + frappe.confirm( + 'Are you sure? It will overwrite taxes for all items with HSN Code '+frm.doc.name+'.', + function(){ + frappe.call({ + args:{ + taxes: frm.doc.taxes, + hsn_code: frm.doc.name + }, + method: 'erpnext.regional.doctype.gst_hsn_code.gst_hsn_code.update_taxes_in_item_master', + callback: function(r) { + if(r.message){ + frappe.show_alert(__('Item taxes updated')); + } + } + }); + } + ); + }); + } } -}); +}); \ No newline at end of file diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json index 2a2145c7f71..06dab3726d7 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.json @@ -1,104 +1,46 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "field:hsn_code", - "beta": 0, - "creation": "2017-06-21 10:48:56.422086", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "autoname": "field:hsn_code", + "creation": "2017-06-21 10:48:56.422086", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "hsn_code", + "description", + "taxes" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "hsn_code", - "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": "HSN 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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "hsn_code", + "fieldtype": "Data", + "in_list_view": 1, + "label": "HSN Code", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Small Text", - "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": "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, - "unique": 0 + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + }, + { + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Taxes", + "options": "Item Tax" } - ], - "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": "2017-09-29 14:38:52.220743", - "modified_by": "Administrator", - "module": "Regional", - "name": "GST HSN Code", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "hsn_code, description", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "hsn_code", - "track_changes": 1, - "track_seen": 0 + ], + "modified": "2019-11-01 11:18:59.556931", + "modified_by": "Administrator", + "module": "Regional", + "name": "GST HSN Code", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "search_fields": "hsn_code, description", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "hsn_code", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py index 9637c2e502a..fa2cb1299a5 100644 --- a/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py +++ b/erpnext/regional/doctype/gst_hsn_code/gst_hsn_code.py @@ -8,3 +8,22 @@ from frappe.model.document import Document class GSTHSNCode(Document): pass + +@frappe.whitelist() +def update_taxes_in_item_master(taxes, hsn_code): + items = frappe.get_list("Item", filters={ + 'gst_hsn_code': hsn_code + }) + + taxes = frappe.parse_json(taxes) + frappe.enqueue(update_item_document, items=items, taxes=taxes) + return 1 + +def update_item_document(items, taxes): + for item in items: + item_to_be_updated=frappe.get_doc("Item", item.name) + item_to_be_updated.taxes = [] + for tax in taxes: + tax = frappe._dict(tax) + item_to_be_updated.append("taxes", {'item_tax_template': tax.item_tax_template, 'tax_category': tax.tax_category}) + item_to_be_updated.save() \ No newline at end of file diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 410d9f1b45b..e3d356f93b3 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -136,6 +136,20 @@ frappe.ui.form.on("Item", { frm.toggle_reqd('customer', frm.doc.is_customer_provided_item ? 1:0); }, + gst_hsn_code: function(frm){ + if(!frm.doc.taxes){ + frappe.db.get_doc("GST HSN Code", frm.doc.gst_hsn_code).then(hsn_doc=>{ + frm.doc.taxes = []; + $.each(hsn_doc.taxes || [], function(i, tax) { + let a = frappe.model.add_child(cur_frm.doc, 'Item Tax', 'taxes'); + a.item_tax_template = tax.item_tax_template; + a.tax_category = tax.tax_category; + frm.refresh_field('taxes'); + }); + }); + } + }, + is_fixed_asset: function(frm) { // set serial no to false & toggles its visibility frm.set_value('has_serial_no', 0); From 611d2ccd04edf186718704b60f6f0f125c3f9e32 Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Thu, 28 Nov 2019 10:11:03 +0530 Subject: [PATCH 318/679] chore: updated path for set_request in v13-develop --- erpnext/portal/doctype/homepage/test_homepage.py | 2 +- .../portal/doctype/homepage_section/test_homepage_section.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/portal/doctype/homepage/test_homepage.py b/erpnext/portal/doctype/homepage/test_homepage.py index b262c4640cc..bf5c4025a0b 100644 --- a/erpnext/portal/doctype/homepage/test_homepage.py +++ b/erpnext/portal/doctype/homepage/test_homepage.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe import unittest -from frappe.tests.test_website import set_request +from frappe.utils import set_request from frappe.website.render import render class TestHomepage(unittest.TestCase): diff --git a/erpnext/portal/doctype/homepage_section/test_homepage_section.py b/erpnext/portal/doctype/homepage_section/test_homepage_section.py index c52b7a96c65..5b3196def25 100644 --- a/erpnext/portal/doctype/homepage_section/test_homepage_section.py +++ b/erpnext/portal/doctype/homepage_section/test_homepage_section.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe import unittest from bs4 import BeautifulSoup -from frappe.tests.test_website import set_request +from frappe.utils import set_request from frappe.website.render import render class TestHomepageSection(unittest.TestCase): From 54694dd8161b959cf558c10180b3564ac043477c Mon Sep 17 00:00:00 2001 From: Sammish Thundiyil Date: Fri, 29 Nov 2019 15:30:30 +0300 Subject: [PATCH 319/679] modified: erpnext/hr/doctype/repayment_schedule/repayment_schedule.json (#19470) --- .../repayment_schedule.json | 293 +++++------------- 1 file changed, 72 insertions(+), 221 deletions(-) diff --git a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json b/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json index a1161851d04..5bb2d370fa8 100644 --- a/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json +++ b/erpnext/hr/doctype/repayment_schedule/repayment_schedule.json @@ -1,231 +1,82 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2016-12-20 15:32:25.078334", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2016-12-20 15:32:25.078334", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_date", + "principal_amount", + "interest_amount", + "total_payment", + "balance_loan_amount", + "paid" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "payment_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": 0, - "label": "Payment Date", - "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_on_submit": 1, + "columns": 2, + "fieldname": "payment_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Payment Date" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "principal_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": "Principal Amount", - "length": 0, - "no_copy": 1, - "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 - }, + "columns": 2, + "fieldname": "principal_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Principal Amount", + "no_copy": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "interest_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": "Interest Amount", - "length": 0, - "no_copy": 1, - "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 - }, + "columns": 2, + "fieldname": "interest_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Interest Amount", + "no_copy": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "total_payment", - "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": "Total Payment", - "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 - }, + "columns": 2, + "fieldname": "total_payment", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total Payment", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "balance_loan_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": "Balance Loan Amount", - "length": 0, - "no_copy": 1, - "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 - }, + "columns": 2, + "fieldname": "balance_loan_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Balance Loan Amount", + "no_copy": 1, + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "paid", - "fieldtype": "Check", - "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": "Paid", - "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": "paid", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Paid", + "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": 1, - "max_attachments": 0, - "modified": "2018-03-30 17:37:31.834792", - "modified_by": "Administrator", - "module": "HR", - "name": "Repayment Schedule", - "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-10-29 11:45:10.694557", + "modified_by": "Administrator", + "module": "HR", + "name": "Repayment Schedule", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From 935517d914f5fe0fc594c838901975d804e62c36 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 29 Nov 2019 18:08:42 +0530 Subject: [PATCH 320/679] fix: added bank reconciliation page in the accounting module (#19719) * fix: added bank reconcilation page inaccounting module * Update accounts.py --- erpnext/config/accounts.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/config/accounts.py b/erpnext/config/accounts.py index ab75f211c04..08711fc09ea 100644 --- a/erpnext/config/accounts.py +++ b/erpnext/config/accounts.py @@ -197,6 +197,11 @@ def get_data(): "name": "Bank Reconciliation Statement", "is_query_report": True, "doctype": "Journal Entry" + },{ + "type": "page", + "name": "bank-reconciliation", + "label": _("Bank Reconciliation"), + "icon": "fa fa-bar-chart" }, { "type": "report", From b3af2adc4a145b5d540fbaef030c9a4a1a6ced47 Mon Sep 17 00:00:00 2001 From: Victor Munene Date: Fri, 29 Nov 2019 15:42:13 +0300 Subject: [PATCH 321/679] use program.courses instead of get_all_children to get course list (#19695) --- .../education/doctype/program_enrollment/program_enrollment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py index d5348ffd067..75361728917 100644 --- a/erpnext/education/doctype/program_enrollment/program_enrollment.py +++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py @@ -71,7 +71,7 @@ class ProgramEnrollment(Document): def create_course_enrollments(self): student = frappe.get_doc("Student", self.student) program = frappe.get_doc("Program", self.program) - course_list = [course.course for course in program.get_all_children()] + course_list = [course.course for course in program.courses] for course_name in course_list: student.enroll_in_course(course_name=course_name, program_enrollment=self.name) From 8c6bc34bc4204e19f9712c6df25e71acae0b6ffb Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 29 Nov 2019 18:42:20 +0530 Subject: [PATCH 322/679] added test cases --- .../doctype/work_order/test_work_order.py | 46 ++++++++++++++++--- .../doctype/work_order/work_order.py | 3 +- 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index ea2e7a96e1d..0a8f41fc49f 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -5,10 +5,10 @@ from __future__ import unicode_literals import unittest import frappe -from frappe.utils import flt, time_diff_in_hours, now, add_days, cint +from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory -from erpnext.manufacturing.doctype.work_order.work_order \ - import make_stock_entry, ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError +from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry, + ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError) from erpnext.stock.doctype.stock_entry import test_stock_entry from erpnext.stock.utils import get_bin from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order @@ -307,14 +307,50 @@ class TestWorkOrder(unittest.TestCase): {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) if data: + frappe.db.set_value("Manufacturing Settings", + None, "disable_capacity_planning", 0) + bom, bom_item = data bom_doc = frappe.get_doc('BOM', bom) work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom) + self.assertTrue(work_order.planned_end_date) job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name}) self.assertEqual(len(job_cards), len(bom_doc.operations)) + def test_capcity_planning(self): + frappe.db.set_value("Manufacturing Settings", None, { + "disable_capacity_planning": 0, + "capacity_planning_for_days": 1 + }) + + data = frappe.get_cached_value('BOM', {'docstatus': 1, 'item': '_Test FG Item 2', + 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item']) + + if data: + bom, bom_item = data + + planned_start_date = add_months(today(), months=-1) + work_order = make_wo_order_test_record(item=bom_item, + qty=10, bom_no=bom, planned_start_date=planned_start_date) + + work_order1 = make_wo_order_test_record(item=bom_item, + qty=30, bom_no=bom, planned_start_date=planned_start_date, do_not_submit=1) + + self.assertRaises(CapacityError, work_order1.submit) + + frappe.db.set_value("Manufacturing Settings", None, { + "capacity_planning_for_days": 30 + }) + + work_order1.reload() + work_order1.submit() + self.assertTrue(work_order1.docstatus, 1) + + work_order1.cancel() + work_order.cancel() + def test_work_order_with_non_transfer_item(self): items = {'Finished Good Transfer Item': 1, '_Test FG Item': 1, '_Test FG Item 1': 0} for item, allow_transfer in items.items(): @@ -371,14 +407,12 @@ def make_wo_order_test_record(**args): wo_order.skip_transfer=1 wo_order.get_items_and_operations_from_bom() wo_order.sales_order = args.sales_order or None + wo_order.planned_start_date = args.planned_start_date or now() if args.source_warehouse: for item in wo_order.get("required_items"): item.source_warehouse = args.source_warehouse - if args.planned_start_date: - wo_order.planned_start_date = args.planned_start_date - if not args.do_not_save: wo_order.insert() diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 84f570b07af..ff489542f66 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -22,6 +22,7 @@ from erpnext.utilities.transaction_base import validate_uom_is_integer from frappe.model.mapper import get_mapped_doc class OverProductionError(frappe.ValidationError): pass +class CapacityError(frappe.ValidationError): pass class StockOverProductionError(frappe.ValidationError): pass class OperationTooLongError(frappe.ValidationError): pass class ItemHasVariantError(frappe.ValidationError): pass @@ -282,7 +283,7 @@ class WorkOrder(Document): if date_diff(row.planned_start_time, original_start_time) > plan_days: frappe.throw(_("Unable to find the time slot in the next {0} days for the operation {1}.") - .format(plan_days, row.operation)) + .format(plan_days, row.operation), CapacityError) row.db_update() From 23b1dae6a7218df6a4220e905dbebcf00488aecb Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Fri, 29 Nov 2019 20:22:06 +0530 Subject: [PATCH 323/679] chore: dropped dead setup_wizard test fix: fixed imports for teste utils --- .../test_product_configurator.py | 2 +- .../setup/setup_wizard/test_setup_wizard.py | 71 ------------------- 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 erpnext/setup/setup_wizard/test_setup_wizard.py diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py index a534e5f8382..97042dba92c 100644 --- a/erpnext/portal/product_configurator/test_product_configurator.py +++ b/erpnext/portal/product_configurator/test_product_configurator.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from bs4 import BeautifulSoup import frappe, unittest -from frappe.tests.test_website import set_request, get_html_for_route +from frappe.utils import set_request, get_html_for_route from frappe.website.render import render from erpnext.portal.product_configurator.utils import get_products_for_website from erpnext.stock.doctype.item.test_item import make_item_variant diff --git a/erpnext/setup/setup_wizard/test_setup_wizard.py b/erpnext/setup/setup_wizard/test_setup_wizard.py deleted file mode 100644 index a489133abae..00000000000 --- a/erpnext/setup/setup_wizard/test_setup_wizard.py +++ /dev/null @@ -1,71 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - -from __future__ import unicode_literals - -import frappe, time -from frappe.utils.selenium_testdriver import TestDriver - -def run_setup_wizard_test(): - driver = TestDriver() - frappe.db.set_default('in_selenium', '1') - frappe.db.commit() - - driver.login('#page-setup-wizard') - print('Running Setup Wizard Test...') - - # Language slide - driver.wait_for_ajax(True) - time.sleep(1) - - driver.set_select("language", "English (United States)") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - - # Region slide - driver.wait_for_ajax(True) - driver.set_select("country", "India") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - - # Profile slide - driver.set_field("full_name", "Great Tester") - driver.set_field("email", "great@example.com") - driver.set_field("password", "test") - driver.wait_for_ajax(True) - time.sleep(1) - driver.click(".next-btn") - time.sleep(1) - - # domain slide - driver.set_multicheck("domains", ["Manufacturing"]) - time.sleep(1) - driver.click(".next-btn") - - # Org slide - driver.set_field("company_name", "For Testing") - time.sleep(1) - driver.print_console() - driver.click(".next-btn") - - driver.set_field("company_tagline", "Just for GST") - driver.set_field("bank_account", "HDFC") - time.sleep(3) - driver.click(".complete-btn") - - # Wait for desktop - driver.wait_for('#page-desktop', timeout=600) - - driver.print_console() - time.sleep(3) - - frappe.db.set_default('in_selenium', None) - frappe.db.set_value("Company", "For Testing", "write_off_account", "Write Off - FT") - frappe.db.set_value("Company", "For Testing", "exchange_gain_loss_account", "Exchange Gain/Loss - FT") - frappe.db.commit() - - driver.close() - - return True From ad66677029bccd534639ef8158dcf7f1af03e2d7 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 29 Nov 2019 23:59:02 +0530 Subject: [PATCH 324/679] fix: add conditional message in template string --- erpnext/public/js/hub/pages/Search.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/hub/pages/Search.vue b/erpnext/public/js/hub/pages/Search.vue index 2a6088925fd..8ab48d0e8d7 100644 --- a/erpnext/public/js/hub/pages/Search.vue +++ b/erpnext/public/js/hub/pages/Search.vue @@ -42,7 +42,7 @@ export default { computed: { page_title() { return this.items.length - ? __(`Results for ${this.search_value} in category ${this.category}`) + ? __(`Results for "${this.search_value}" ${this.category!=='All'? `in category ${this.category}`: ''}`) : __('No Items found.'); } }, From 4405a2a5f86ff66099a676ec050780b9b8aaf613 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Sat, 30 Nov 2019 16:58:31 +0530 Subject: [PATCH 325/679] fix: add project in child for items --- erpnext/projects/doctype/project/project.js | 50 ++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 25c97d1fb84..069e3dc6fee 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -16,6 +16,54 @@ frappe.ui.form.on("Project", { time_log.parenttype = 'Timesheet'; new_doc.time_logs = [time_log]; + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + }, + 'Purchase Order': () => { + let doctype = 'Purchase Order'; + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let purchase_order_item = frappe.model.get_new_doc('Purchase Order Item'); + purchase_order_item.project = frm.doc.name; + purchase_order_item.parent = new_doc.name; + purchase_order_item.parentfield = 'items'; + purchase_order_item.parenttype = 'Purchase Order'; + new_doc.items = [purchase_order_item]; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + }, + 'Purchase Receipt': () => { + let doctype = 'Purchase Receipt'; + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let purchase_receipt_item = frappe.model.get_new_doc('Purchase Receipt Item'); + purchase_receipt_item.project = frm.doc.name; + purchase_receipt_item.parent = new_doc.name; + purchase_receipt_item.parentfield = 'items'; + purchase_receipt_item.parenttype = 'Purchase Receipt'; + new_doc.items = [purchase_receipt_item]; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + }, + 'Purchase Invoice': () => { + let doctype = 'Purchase Invoice'; + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let purchase_invoice_item = frappe.model.get_new_doc('Purchase Invoice Item'); + purchase_invoice_item.project = frm.doc.name; + purchase_invoice_item.parent = new_doc.name; + purchase_invoice_item.parentfield = 'items'; + purchase_invoice_item.parenttype = 'Purchase Invoice'; + new_doc.items = [purchase_invoice_item]; + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); }, @@ -80,7 +128,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 = { From 98a54355ae5c88fdfcec488a5c12eb58f52b721a Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 1 Dec 2019 10:06:16 +0530 Subject: [PATCH 326/679] fix: Post GL entry fix for asset (#19751) --- 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 5249a6348fbbc861652b9a70c23dedd8502f1c00 Mon Sep 17 00:00:00 2001 From: Wolfram Schmidt Date: Sun, 1 Dec 2019 06:17:21 +0100 Subject: [PATCH 327/679] Update de.csv (#19755) Changed all translations from "Aufgabe" to "Vorgang" in singular and plural where the word "Task" is project related. Left all agricultural and others how they are (as Aufgabe). --- erpnext/translations/de.csv | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/translations/de.csv b/erpnext/translations/de.csv index 2e25a127d8e..cdff3ff422b 100644 --- a/erpnext/translations/de.csv +++ b/erpnext/translations/de.csv @@ -1329,7 +1329,7 @@ apps/erpnext/erpnext/hr/doctype/job_offer/job_offer.js,Create Employee,Mitarbeit apps/erpnext/erpnext/utilities/transaction_base.py,Invalid Posting Time,Ungültige Buchungszeit DocType: Salary Component,Condition and Formula,Zustand und Formel DocType: Lead,Campaign Name,Kampagnenname -apps/erpnext/erpnext/setup/default_energy_point_rules.py,On Task Completion,Bei Abschluss der Aufgabe +apps/erpnext/erpnext/setup/default_energy_point_rules.py,On Task Completion,Bei Abschluss des Vorgangs apps/erpnext/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py,There is no leave period in between {0} and {1},Es gibt keinen Urlaub zwischen {0} und {1} DocType: Fee Validity,Healthcare Practitioner,praktischer Arzt DocType: Hotel Room,Capacity,Kapazität @@ -1353,7 +1353,7 @@ DocType: Payment Entry,Received Amount (Company Currency),Erhaltene Menge (Gesel apps/erpnext/erpnext/erpnext_integrations/doctype/gocardless_settings/gocardless_settings.py,Payment Cancelled. Please check your GoCardless Account for more details,Zahlung abgebrochen. Bitte überprüfen Sie Ihr GoCardless Konto für weitere Details DocType: Work Order,Skip Material Transfer to WIP Warehouse,Überspringen Sie die Materialübertragung in das WIP-Lager DocType: Contract,N/A,nicht verfügbar -DocType: Task Type,Task Type,Aufgabentyp +DocType: Task Type,Task Type,Vorgangstyp DocType: Topic,Topic Content,Themeninhalt DocType: Delivery Settings,Send with Attachment,Senden mit Anhang DocType: Service Level,Priorities,Prioritäten @@ -2449,7 +2449,7 @@ apps/erpnext/erpnext/public/js/utils/serial_no_batch_selector.js,Please select b DocType: Asset,Depreciation Schedules,Abschreibungen Termine apps/erpnext/erpnext/projects/doctype/timesheet/timesheet.js,Create Sales Invoice,Verkaufsrechnung erstellen apps/erpnext/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html,Ineligible ITC,Nicht förderfähiges ITC -DocType: Task,Dependent Tasks,Abhängige Aufgaben +DocType: Task,Dependent Tasks,Abhängige Vorgänge apps/erpnext/erpnext/regional/report/gstr_1/gstr_1.py,Following accounts might be selected in GST Settings:,In den GST-Einstellungen können folgende Konten ausgewählt werden: apps/erpnext/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js,Quantity to Produce,Menge zu produzieren apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py,Application period cannot be outside leave allocation period,Beantragter Zeitraum kann nicht außerhalb der beantragten Urlaubszeit liegen @@ -2846,7 +2846,7 @@ DocType: Loan,Applicant Type,Bewerbertyp DocType: Purchase Invoice,03-Deficiency in services,03-Mangel an Dienstleistungen DocType: Healthcare Settings,Default Medical Code Standard,Default Medical Code Standard DocType: Purchase Invoice Item,HSN/SAC,HSN / SAC -DocType: Project Template Task,Project Template Task,Projektvorlagenaufgabe +DocType: Project Template Task,Project Template Task,Projektvorgangsvorlage DocType: Accounts Settings,Over Billing Allowance (%),Mehr als Abrechnungsbetrag (%) apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py,Purchase Receipt {0} is not submitted,Kaufbeleg {0} wurde nicht übertragen DocType: Company,Default Payable Account,Standard-Verbindlichkeitenkonto @@ -3323,7 +3323,7 @@ DocType: Soil Texture,Silt,Schlick ,Qty to Order,Zu bestellende Menge DocType: Period Closing Voucher,"The account head under Liability or Equity, in which Profit/Loss will be booked","Der Kontenkopf unter Eigen- oder Fremdkapital, in dem Gewinn / Verlust verbucht wird" apps/erpnext/erpnext/accounts/doctype/budget/budget.py,Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4},Ein weiterer Budgeteintrag '{0}' existiert bereits für {1} '{2}' und für '{3}' für das Geschäftsjahr {4} -apps/erpnext/erpnext/config/projects.py,Gantt chart of all tasks.,Gantt-Diagramm aller Aufgaben +apps/erpnext/erpnext/config/projects.py,Gantt chart of all tasks.,Gantt-Diagramm aller Vorgänge DocType: Opportunity,Mins to First Response,Minuten zum First Response DocType: Pricing Rule,Margin Type,Margenart apps/erpnext/erpnext/projects/doctype/project/project_dashboard.html,{0} hours,{0} Stunden @@ -3961,7 +3961,7 @@ apps/erpnext/erpnext/selling/doctype/sales_order/sales_order.py,Maintenance Sche apps/erpnext/erpnext/education/doctype/student/student_dashboard.py,Student LMS Activity,Student LMS Aktivität DocType: POS Profile,Applicable for Users,Anwendbar für Benutzer DocType: Supplier Quotation,PUR-SQTN-.YYYY.-,PUR-SQTN-.JJJJ.- -apps/erpnext/erpnext/projects/doctype/project/project.js,Set Project and all Tasks to status {0}?,Projekt und alle Aufgaben auf Status {0} setzen? +apps/erpnext/erpnext/projects/doctype/project/project.js,Set Project and all Tasks to status {0}?,Projekt und alle Vorgänge auf Status {0} setzen? DocType: Purchase Invoice,Set Advances and Allocate (FIFO),Vorschüsse setzen und zuordnen (FIFO) apps/erpnext/erpnext/manufacturing/doctype/production_plan/production_plan.py,No Work Orders created,Keine Arbeitsaufträge erstellt apps/erpnext/erpnext/hr/doctype/salary_slip/salary_slip.py,Salary Slip of employee {0} already created for this period,Gehaltsabrechnung der Mitarbeiter {0} für diesen Zeitraum bereits erstellt @@ -4418,7 +4418,7 @@ DocType: Normal Test Items,Result Value,Ergebnis Wert DocType: Hotel Room,Hotels,Hotels apps/erpnext/erpnext/accounts/doctype/cost_center/cost_center_tree.js,New Cost Center Name,Neuer Kostenstellenname DocType: Leave Control Panel,Leave Control Panel,Urlaubsverwaltung -DocType: Project,Task Completion,Aufgabenerledigung +DocType: Project,Task Completion,Vorgangserfüllung apps/erpnext/erpnext/templates/generators/item/item_add_to_cart.html,Not in Stock,Nicht lagernd DocType: Volunteer,Volunteer Skills,Freiwillige Fähigkeiten DocType: Additional Salary,HR User,Nutzer Personalabteilung @@ -5197,7 +5197,7 @@ DocType: Work Order,Material Transferred for Manufacturing,Material zur Herstell apps/erpnext/erpnext/accounts/report/general_ledger/general_ledger.py,Account {0} does not exists,Konto {0} existiert nicht apps/erpnext/erpnext/accounts/doctype/sales_invoice/sales_invoice.js,Select Loyalty Program,Wählen Sie Treueprogramm DocType: Project,Project Type,Projekttyp -apps/erpnext/erpnext/projects/doctype/task/task.py,Child Task exists for this Task. You can not delete this Task.,Für diese Aufgabe existiert eine untergeordnete Aufgabe. Sie können diese Aufgabe daher nicht löschen. +apps/erpnext/erpnext/projects/doctype/task/task.py,Child Task exists for this Task. You can not delete this Task.,Für diesen Vorgang existiert ein untergeordneter Vorgang. Sie können diese Aufgabe daher nicht löschen. apps/erpnext/erpnext/setup/doctype/sales_person/sales_person.py,Either target qty or target amount is mandatory.,Entweder Zielstückzahl oder Zielmenge ist zwingend erforderlich. apps/erpnext/erpnext/config/projects.py,Cost of various activities,Aufwendungen für verschiedene Tätigkeiten apps/erpnext/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py,"Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}","Einstellen Events auf {0}, da die Mitarbeiter auf die beigefügten unter Verkaufs Personen keine Benutzer-ID {1}" @@ -5597,7 +5597,7 @@ apps/erpnext/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py,Paid apps/erpnext/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py,{0} is not a valid Batch Number for Item {1},{0} ist keine gültige Chargennummer für Artikel {1} apps/erpnext/erpnext/shopping_cart/cart.py,Please enter valid coupon code !!,Bitte geben Sie einen gültigen Gutscheincode ein !! apps/erpnext/erpnext/hr/doctype/leave_application/leave_application.py,Note: There is not enough leave balance for Leave Type {0},Hinweis: Es gibt nicht genügend Urlaubsguthaben für Abwesenheitstyp {0} -DocType: Task,Task Description,Aufgabenbeschreibung +DocType: Task,Task Description,Vorgangsbeschreibung DocType: Training Event,Seminar,Seminar DocType: Program Enrollment Fee,Program Enrollment Fee,Programm Einschreibegebühr DocType: Item,Supplier Items,Lieferantenartikel @@ -5754,7 +5754,7 @@ apps/erpnext/erpnext/accounts/doctype/sales_invoice/pos.py,All Territories,Alle DocType: Lost Reason Detail,Lost Reason Detail,Verlorene Begründung Detail apps/erpnext/erpnext/hr/utils.py,Please set leave policy for employee {0} in Employee / Grade record,Legen Sie die Abwesenheitsrichtlinie für den Mitarbeiter {0} im Mitarbeiter- / Notensatz fest apps/erpnext/erpnext/public/js/controllers/transaction.js,Invalid Blanket Order for the selected Customer and Item,Ungültiger Blankoauftrag für den ausgewählten Kunden und Artikel -apps/erpnext/erpnext/projects/doctype/task/task_tree.js,Add Multiple Tasks,Mehrere Aufgaben hinzufügen +apps/erpnext/erpnext/projects/doctype/task/task_tree.js,Add Multiple Tasks,Mehrere Vorgänge hinzufügen DocType: Purchase Invoice,Items,Artikel apps/erpnext/erpnext/crm/doctype/contract/contract.py,End Date cannot be before Start Date.,Das Enddatum darf nicht vor dem Startdatum liegen. apps/erpnext/erpnext/education/doctype/course_enrollment/course_enrollment.py,Student is already enrolled.,Student ist bereits eingetragen sind. From f89b1722028684f6d1e19bc367de1f547aeea8c3 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sun, 1 Dec 2019 21:53:32 +0530 Subject: [PATCH 328/679] 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 c131bd1b257ddd13fef98ef0527da63ae86e1255 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 2 Dec 2019 01:09:59 +0530 Subject: [PATCH 329/679] feat(marketplace): edit item dialog --- .../js/hub/components/edit_details_dialog.js | 45 +++++++++++++++++++ erpnext/public/js/hub/pages/Item.vue | 37 ++++++++++++++- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 erpnext/public/js/hub/components/edit_details_dialog.js diff --git a/erpnext/public/js/hub/components/edit_details_dialog.js b/erpnext/public/js/hub/components/edit_details_dialog.js new file mode 100644 index 00000000000..b341901fece --- /dev/null +++ b/erpnext/public/js/hub/components/edit_details_dialog.js @@ -0,0 +1,45 @@ +function EditDetailsDialog(primary_action, defaults) { + let dialog = new frappe.ui.Dialog({ + title: __('Update Details'), + fields: [ + { + "label": "Item Name", + "fieldname": "item_name", + "fieldtype": "Data", + "default": defaults.item_name, + "reqd": 1 + }, + { + "label": "Hub Category", + "fieldname": "hub_category", + "fieldtype": "Autocomplete", + "default": defaults.hub_category, + "options": [], + "reqd": 1 + }, + { + "label": "Description", + "fieldname": "description", + "fieldtype": "Text", + "default": defaults.description, + "options": [], + "reqd": 1 + } + ], + primary_action_label: primary_action.label || __('Update Details'), + primary_action: primary_action.fn, + }); + + hub.call('get_categories') + .then(categories => { + categories = categories.map(d => d.name); + dialog.fields_dict.hub_category.df.options = categories; + dialog.fields_dict.hub_category.set_options(); + }); + + return dialog; +} + +export { + EditDetailsDialog +}; \ No newline at end of file diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue index 841d0046db8..b8399e30d6d 100644 --- a/erpnext/public/js/hub/pages/Item.vue +++ b/erpnext/public/js/hub/pages/Item.vue @@ -35,6 +35,7 @@ From ba48546678245b28d48880add9062494c8c3b8ed Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 9 Dec 2019 16:31:14 +0530 Subject: [PATCH 369/679] fix: display new item value --- .../js/hub/components/edit_details_dialog.js | 50 +++++++++---------- erpnext/public/js/hub/pages/Item.vue | 4 +- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/erpnext/public/js/hub/components/edit_details_dialog.js b/erpnext/public/js/hub/components/edit_details_dialog.js index b341901fece..e249ca5d7a4 100644 --- a/erpnext/public/js/hub/components/edit_details_dialog.js +++ b/erpnext/public/js/hub/components/edit_details_dialog.js @@ -3,43 +3,39 @@ function EditDetailsDialog(primary_action, defaults) { title: __('Update Details'), fields: [ { - "label": "Item Name", - "fieldname": "item_name", - "fieldtype": "Data", - "default": defaults.item_name, - "reqd": 1 + label: 'Item Name', + fieldname: 'item_name', + fieldtype: 'Data', + default: defaults.item_name, + reqd: 1 }, { - "label": "Hub Category", - "fieldname": "hub_category", - "fieldtype": "Autocomplete", - "default": defaults.hub_category, - "options": [], - "reqd": 1 + label: 'Hub Category', + fieldname: 'hub_category', + fieldtype: 'Autocomplete', + default: defaults.hub_category, + options: [], + reqd: 1 }, { - "label": "Description", - "fieldname": "description", - "fieldtype": "Text", - "default": defaults.description, - "options": [], - "reqd": 1 + label: 'Description', + fieldname: 'description', + fieldtype: 'Text', + default: defaults.description, + options: [], + reqd: 1 } ], primary_action_label: primary_action.label || __('Update Details'), - primary_action: primary_action.fn, + primary_action: primary_action.fn }); - hub.call('get_categories') - .then(categories => { - categories = categories.map(d => d.name); - dialog.fields_dict.hub_category.df.options = categories; - dialog.fields_dict.hub_category.set_options(); - }); + hub.call('get_categories').then(categories => { + categories = categories.map(d => d.name); + dialog.fields_dict.hub_category.set_data(categories); + }); return dialog; } -export { - EditDetailsDialog -}; \ No newline at end of file +export { EditDetailsDialog }; diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue index b8399e30d6d..31cc8d5c8cf 100644 --- a/erpnext/public/js/hub/pages/Item.vue +++ b/erpnext/public/js/hub/pages/Item.vue @@ -311,7 +311,9 @@ export default { } ) .then((r) => { - this.get_item_details(); + return this.get_item_details(); + }) + .then(() => { frappe.show_alert(__(`${this.item.item_name} Updated`)); }) }, From 2944301c87a8726d039a5f9fa38a1e9139d2a369 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 9 Dec 2019 16:35:54 +0530 Subject: [PATCH 370/679] fix: website showing disabled items in list of products --- erpnext/portal/product_configurator/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 3a373a4ab19..9c0120d4168 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -52,7 +52,6 @@ def get_attribute_filter_data(): def get_products_for_website(field_filters=None, attribute_filters=None, search=None): - if attribute_filters: item_codes = get_item_codes_by_attributes(attribute_filters) items_by_attributes = get_items([['name', 'in', item_codes]]) @@ -336,7 +335,9 @@ def get_items(filters=None, search=None): filter_condition = get_conditions(filters, 'and') - where_conditions = ' and '.join( + where_conditions = 'disabled = 0 and ' + + where_conditions += ' and '.join( [condition for condition in [show_in_website_condition, search_condition, filter_condition] if condition] ) From 7e958da6d6062a6d1a363bde8ec42ae4e0d3ec0a Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 9 Dec 2019 17:23:19 +0530 Subject: [PATCH 371/679] fix: sales order reqd only checks for stock items (#19865) --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 6 ++---- .../selling/doctype/selling_settings/selling_settings.json | 3 +-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index c4d64a72fd1..0f4d4451be9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -535,10 +535,8 @@ class SalesInvoice(SellingController): for i in dic: if frappe.db.get_single_value('Selling Settings', dic[i][0]) == 'Yes': for d in self.get('items'): - is_stock_item = frappe.get_cached_value('Item', d.item_code, 'is_stock_item') - if (d.item_code and is_stock_item == 1\ - and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): - msgprint(_("{0} is mandatory for Stock Item {1}").format(i,d.item_code), raise_exception=1) + if (d.item_code and not d.get(i.lower().replace(' ','_')) and not self.get(dic[i][1])): + msgprint(_("{0} is mandatory for Item {1}").format(i,d.item_code), raise_exception=1) def validate_proj_cust(self): diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json index 5033d7aa7f9..c04bfd281e2 100644 --- a/erpnext/selling/doctype/selling_settings/selling_settings.json +++ b/erpnext/selling/doctype/selling_settings/selling_settings.json @@ -77,7 +77,6 @@ "fieldtype": "Column Break" }, { - "description": "Only for Stock Items", "fieldname": "so_required", "fieldtype": "Select", "label": "Sales Order Required", @@ -138,7 +137,7 @@ "icon": "fa fa-cog", "idx": 1, "issingle": 1, - "modified": "2019-11-25 18:35:51.472653", + "modified": "2019-12-09 13:38:36.486298", "modified_by": "Administrator", "module": "Selling", "name": "Selling Settings", From 9e0b3b29fa71f940eec73ffd514c9d459c3cbdd3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 9 Dec 2019 18:21:23 +0530 Subject: [PATCH 372/679] feat: better message for serial number creation (#19863) --- erpnext/stock/doctype/serial_no/serial_no.py | 30 ++++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 19eb398130c..23d00da7c1f 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -353,17 +353,19 @@ def get_auto_serial_nos(serial_no_series, qty): def auto_make_serial_nos(args): serial_nos = get_serial_nos(args.get('serial_no')) created_numbers = [] + voucher_type = args.get('voucher_type') + item_code = args.get('item_code') for serial_no in serial_nos: if frappe.db.exists("Serial No", serial_no): sr = frappe.get_doc("Serial No", serial_no) sr.via_stock_ledger = True - sr.item_code = args.get('item_code') + sr.item_code = item_code sr.warehouse = args.get('warehouse') if args.get('actual_qty', 0) > 0 else None sr.batch_no = args.get('batch_no') sr.location = args.get('location') sr.company = args.get('company') sr.supplier = args.get('supplier') - if sr.sales_order and args.get('voucher_type') == "Stock Entry" \ + if sr.sales_order and voucher_type == "Stock Entry" \ and not args.get('actual_qty', 0) > 0: sr.sales_order = None sr.save(ignore_permissions=True) @@ -371,10 +373,28 @@ def auto_make_serial_nos(args): created_numbers.append(make_serial_no(serial_no, args)) form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers)) + + # Setting up tranlated title field for all cases + singular_title = _("Serial Number Created") + multiple_title = _("Serial Numbers Created") + + if voucher_type: + multiple_title = singular_title = _("{0} Created").format(voucher_type) + if len(form_links) == 1: - frappe.msgprint(_("Serial No {0} created").format(form_links[0])) + frappe.msgprint(_("Serial No {0} Created").format(form_links[0]), singular_title) elif len(form_links) > 0: - frappe.msgprint(_("The following serial numbers were created:
{0}").format(', '.join(form_links))) + message = _("The following serial numbers were created:

{0}").format(get_items_html(form_links, item_code)) + frappe.msgprint(message, multiple_title) + +def get_items_html(serial_nos, item_code): + body = ', '.join(serial_nos) + return '''
+ {0}: {1} Serial Numbers + +
{2}
+ '''.format(item_code, len(serial_nos), body) + def get_item_details(item_code): return frappe.db.sql("""select name, has_batch_no, docstatus, @@ -397,7 +417,7 @@ def make_serial_no(serial_no, args): sr.via_stock_ledger = args.get('via_stock_ledger') or True sr.asset = args.get('asset') sr.location = args.get('location') - + if args.get('purchase_document_type'): sr.purchase_document_type = args.get('purchase_document_type') From da4847e46d55dbc2647c91ca15ee8648726599f9 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 9 Dec 2019 18:27:50 +0530 Subject: [PATCH 373/679] patch: Set against_blanket_order value in existing SO/PO (#19810) * patch: Set against_blanket_order value in existing SO/PO * Update set_against_blanket_order_in_sales_and_purchase_order.py --- erpnext/patches.txt | 3 ++- ..._against_blanket_order_in_sales_and_purchase_order.py | 9 +++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index daedca7a28c..68b6cc0f370 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -647,4 +647,5 @@ 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 -erpnext.patches.v12_0.set_production_capacity_in_workstation \ No newline at end of file +erpnext.patches.v12_0.set_production_capacity_in_workstation +erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order \ No newline at end of file diff --git a/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py new file mode 100644 index 00000000000..555d8ae7976 --- /dev/null +++ b/erpnext/patches/v12_0/set_against_blanket_order_in_sales_and_purchase_order.py @@ -0,0 +1,9 @@ +import frappe +def execute(): + for doctype in ['Sales Order Item', 'Purchase Order Item']: + frappe.reload_doctype(doctype) + frappe.db.sql(""" + UPDATE `tab{0}` + SET against_blanket_order = 1 + WHERE ifnull(blanket_order, '') != '' + """.format(doctype)) From ae02b4119dae0482334a512b92301f61f2d85201 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Mon, 9 Dec 2019 13:33:35 +0000 Subject: [PATCH 374/679] add init.py for appointment modules (#19823) --- erpnext/www/book-appointment/__init__.py | 0 erpnext/www/book-appointment/verify/__init__.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) 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/verify/__init__.py b/erpnext/www/book-appointment/verify/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 5998034b02c0bc474769f2ca719e6645beb34ecb Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 9 Dec 2019 19:07:05 +0530 Subject: [PATCH 375/679] fix: error message displays asset category as None (#19873) * 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 0cb2783f8039592b2c334da2ba708a63e61ee1e0 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 9 Dec 2019 19:24:29 +0530 Subject: [PATCH 376/679] fix: create unpublish log on the hub --- erpnext/hub_node/api.py | 20 ++++++++------------ erpnext/public/js/hub/pages/Home.vue | 8 ++++---- erpnext/public/js/hub/pages/Item.vue | 6 +++--- 3 files changed, 15 insertions(+), 19 deletions(-) diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py index b0b00b2b469..3a5b06fdc4b 100644 --- a/erpnext/hub_node/api.py +++ b/erpnext/hub_node/api.py @@ -148,21 +148,17 @@ def publish_selected_items(items_to_publish): frappe.log_error(message=e, title='Hub Sync Error') @frappe.whitelist() -def unpublish_item(item): +def unpublish_item(item_code, hub_item_name): ''' Remove item listing from the marketplace ''' - item = json.loads(item) - item_code = item.get('item_code') - frappe.db.set_value('Item', item_code, 'publish_in_hub', 0) + response = call_hub_method('unpublish_item', { + 'hub_item_name': hub_item_name + }) - item = map_fields([item])[0] - - try: - connection = get_hub_connection() - connection.set_value('Hub Item', item.get('name'), 'published', 0) - - except Exception as e: - frappe.log_error(message=e, title='Hub Sync Error') + if response: + frappe.db.set_value('Item', item_code, 'publish_in_hub', 0) + else: + frappe.throw(_('Unable to update remote activity')) @frappe.whitelist() def get_unregistered_users(): diff --git a/erpnext/public/js/hub/pages/Home.vue b/erpnext/public/js/hub/pages/Home.vue index 84a42367b22..79cfe46e158 100644 --- a/erpnext/public/js/hub/pages/Home.vue +++ b/erpnext/public/js/hub/pages/Home.vue @@ -58,10 +58,10 @@ export default { this.search_value = ''; this.get_items(); }, - watch:{ - $route (to, from){ - this.get_items() - } + mounted() { + frappe.route.on('change', () => { + this.get_items(); + }) }, methods: { get_items() { diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue index e4b0eeaecb5..d0c0ebe603e 100644 --- a/erpnext/public/js/hub/pages/Item.vue +++ b/erpnext/public/js/hub/pages/Item.vue @@ -309,11 +309,11 @@ export default { }, unpublish_item() { - let me = this; - frappe.confirm(__(`Unpublish ${this.item.item_name}?`), function() { + frappe.confirm(__(`Unpublish ${this.item.item_name}?`), () => { frappe .call('erpnext.hub_node.api.unpublish_item', { - item: me.item + item_code: this.item.item_code, + hub_item_name: this.hub_item_name }) .then(r => { frappe.set_route(`marketplace/home`); From 214acc9a42a202f652d33f4b1c9197ac48eb681d Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 9 Dec 2019 20:26:50 +0530 Subject: [PATCH 377/679] 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 6f68f2d02048fc1dca795dd7e610e7478b52f805 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 10 Dec 2019 08:40:10 +0530 Subject: [PATCH 378/679] feat: Cost center for each expenses in expense claim and allowed deferred expense (#19807) * refactor: Expense Claim Cost Center * refactor: Expense Claim Cost Center * refactor: Expense Claim Cost Center * fix: copy cost center from other row * patch: set cost center in children based on parent * fix: test cases for expense claim --- .../hr/doctype/expense_claim/expense_claim.js | 96 +++--- .../doctype/expense_claim/expense_claim.json | 3 +- .../hr/doctype/expense_claim/expense_claim.py | 7 +- .../expense_claim/test_expense_claim.py | 13 +- .../expense_claim_detail.json | 316 ++---------------- .../expense_claim_type/expense_claim_type.js | 2 +- .../expense_claim_type.json | 225 ++++--------- erpnext/patches.txt | 3 +- ..._center_in_child_table_of_expense_claim.py | 8 + 9 files changed, 160 insertions(+), 513 deletions(-) create mode 100644 erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 0d37c10e9cc..570f2ef4c77 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -42,12 +42,6 @@ cur_frm.cscript.onload = function(doc) { cur_frm.set_value("posting_date", frappe.datetime.get_today()); cur_frm.cscript.clear_sanctioned(doc); } - - cur_frm.fields_dict.employee.get_query = function() { - return { - query: "erpnext.controllers.queries.employee_query" - }; - }; }; cur_frm.cscript.clear_sanctioned = function(doc) { @@ -119,7 +113,7 @@ cur_frm.cscript.calculate_total_amount = function(doc,cdt,cdn){ }; erpnext.expense_claim = { - set_title :function(frm) { + set_title: function(frm) { if (!frm.doc.task) { frm.set_value("title", frm.doc.employee_name); } @@ -131,20 +125,20 @@ erpnext.expense_claim = { frappe.ui.form.on("Expense Claim", { setup: function(frm) { - frm.trigger("set_query_for_cost_center"); - frm.trigger("set_query_for_payable_account"); frm.add_fetch("company", "cost_center", "cost_center"); frm.add_fetch("company", "default_expense_claim_payable_account", "payable_account"); - frm.set_query("employee_advance", "advances", function(doc) { + + frm.set_query("employee_advance", "advances", function() { return { filters: [ ['docstatus', '=', 1], - ['employee', '=', doc.employee], + ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], ['paid_amount', '>', 'claimed_amount'] ] }; }); + frm.set_query("expense_approver", function() { return { query: "erpnext.hr.doctype.department_approver.department_approver.get_approvers", @@ -154,14 +148,49 @@ frappe.ui.form.on("Expense Claim", { } }; }); - frm.set_query("account_head", "taxes", function(doc) { + + frm.set_query("account_head", "taxes", function() { return { filters: [ - ['company', '=', doc.company], + ['company', '=', frm.doc.company], ['account_type', 'in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation"]] ] }; }); + + frm.set_query("cost_center", "expenses", function() { + return { + filters: { + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("payable_account", function() { + return { + filters: { + "report_type": "Balance Sheet", + "account_type": "Payable", + "company": frm.doc.company, + "is_group": 0 + } + }; + }); + + frm.set_query("task", function() { + return { + filters: { + 'project': frm.doc.project + } + }; + }); + + frm.set_query("employee", function() { + return { + query: "erpnext.controllers.queries.employee_query" + }; + }); }, onload: function(frm) { @@ -244,30 +273,6 @@ frappe.ui.form.on("Expense Claim", { }); }, - set_query_for_cost_center: function(frm) { - frm.fields_dict["cost_center"].get_query = function() { - return { - filters: { - "company": frm.doc.company, - "is_group": 0 - } - }; - }; - }, - - set_query_for_payable_account: function(frm) { - frm.fields_dict["payable_account"].get_query = function() { - return { - filters: { - "report_type": "Balance Sheet", - "account_type": "Payable", - "company": frm.doc.company, - "is_group": 0 - } - }; - }; - }, - is_paid: function(frm) { frm.trigger("toggle_fields"); }, @@ -329,6 +334,10 @@ frappe.ui.form.on("Expense Claim", { }); frappe.ui.form.on("Expense Claim Detail", { + expenses_add: function(frm, cdt, cdn) { + var row = frappe.get_doc(cdt, cdn); + frm.script_manager.copy_from_first_row("expenses", row, ["cost_center"]); + }, amount: function(frm, cdt, cdn) { var child = locals[cdt][cdn]; var doc = frm.doc; @@ -341,6 +350,9 @@ frappe.ui.form.on("Expense Claim Detail", { cur_frm.cscript.calculate_total(doc,cdt,cdn); frm.trigger("get_taxes"); frm.trigger("calculate_grand_total"); + }, + cost_center: function(frm, cdt, cdn) { + erpnext.utils.copy_value_in_all_rows(frm.doc, cdt, cdn, "expenses", "cost_center"); } }); @@ -411,12 +423,4 @@ frappe.ui.form.on("Expense Taxes and Charges", { tax_amount: function(frm, cdt, cdn) { frm.trigger("calculate_total_tax", cdt, cdn); } -}); - -cur_frm.fields_dict['task'].get_query = function(doc) { - return { - filters:{ - 'project': doc.project - } - }; -}; +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index 5c2f4901713..b5b6823e1ce 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -43,7 +43,6 @@ "accounting_dimensions_section", "project", "dimension_col_break", - "cost_center", "more_details", "status", "amended_from", @@ -366,7 +365,7 @@ "icon": "fa fa-money", "idx": 1, "is_submittable": 1, - "modified": "2019-11-08 14:13:08.964547", + "modified": "2019-11-09 14:13:08.964547", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 59391505fa0..dfb0bb96d82 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -127,7 +127,7 @@ class ExpenseClaim(AccountsController): "debit": data.sanctioned_amount, "debit_in_account_currency": data.sanctioned_amount, "against": self.employee, - "cost_center": self.cost_center + "cost_center": data.cost_center }) ) @@ -190,8 +190,9 @@ class ExpenseClaim(AccountsController): ) def validate_account_details(self): - if not self.cost_center: - frappe.throw(_("Cost center is required to book an expense claim")) + for data in self.expenses: + if not data.cost_center: + frappe.throw(_("Cost center is required to book an expense claim")) if self.is_paid: if not self.mode_of_payment: diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py index b559dfd81d1..6e97f0513d6 100644 --- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py @@ -126,7 +126,7 @@ def generate_taxes(): def make_expense_claim(payable_account, amount, sanctioned_amount, company, account, project=None, task_name=None, do_not_submit=False, taxes=None): employee = frappe.db.get_value("Employee", {"status": "Active"}) - currency = frappe.db.get_value('Company', company, 'default_currency') + currency, cost_center = frappe.db.get_value('Company', company, ['default_currency', 'cost_center']) expense_claim = { "doctype": "Expense Claim", "employee": employee, @@ -134,12 +134,15 @@ def make_expense_claim(payable_account, amount, sanctioned_amount, company, acco "approval_status": "Approved", "company": company, 'currency': currency, - "expenses": - [{"expense_type": "Travel", + "expenses": [{ + "expense_type": "Travel", "default_account": account, - 'currency': currency, + "currency": currency, "amount": amount, - "sanctioned_amount": sanctioned_amount}]} + "sanctioned_amount": sanctioned_amount, + "cost_center": cost_center + }] + } if taxes: expense_claim.update(taxes) diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index b23fb6af0fe..b60db2c3af0 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -1,378 +1,118 @@ { - "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:46", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "expense_date", + "column_break_2", + "expense_type", + "default_account", + "section_break_4", + "description", + "section_break_6", + "amount", + "column_break_8", + "sanctioned_amount", + "cost_center" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expense_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": 0, "label": "Expense Date", - "length": 0, - "no_copy": 0, "oldfieldname": "expense_date", "oldfieldtype": "Date", - "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": 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": "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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "expense_type", "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": "Expense Claim Type", - "length": 0, - "no_copy": 0, "oldfieldname": "expense_type", "oldfieldtype": "Link", "options": "Expense Claim Type", - "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": 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, "depends_on": "expense_type", "fieldname": "default_account", "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": "Default Account", - "length": 0, - "no_copy": 0, "options": "Account", - "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": "section_break_4", - "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, - "fetch_from": "", "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": 1, - "in_standard_filter": 0, "label": "Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Small Text", - "options": "", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "300px", - "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": "300px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "section_break_6", - "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, "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": "claim_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "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": 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, "fieldname": "column_break_8", - "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": "sanctioned_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": "Sanctioned Amount", - "length": 0, "no_copy": 1, "oldfieldname": "sanctioned_amount", "oldfieldtype": "Currency", "options": "Company:company:default_currency", - "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": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" + }, + { + "fieldname": "cost_center", + "fieldtype": "Link", + "label": "Cost Center", + "options": "Cost Center" } ], - "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-06-10 08:41:36.122565", - "modified_by": "Administrator", + "modified": "2019-11-22 11:57:25.110942", + "modified_by": "jangeles@bai.ph", "module": "HR", "name": "Expense Claim Detail", "owner": "harshada@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": 0, - "track_seen": 0, - "track_views": 0 + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js index fda6809e6b2..c4877976775 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js @@ -8,7 +8,7 @@ frappe.ui.form.on("Expense Claim Type", { return{ filters: { "is_group": 0, - "root_type": "Expense", + "root_type": frm.doc.deferred_expense_account ? "Asset" : "Expense", 'company': d.company } } diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json index d0c41221cc8..e45f640c078 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json @@ -1,181 +1,72 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:expense_type", - "beta": 0, - "creation": "2012-03-27 14:35:55", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, - "engine": "InnoDB", + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:expense_type", + "creation": "2012-03-27 14:35:55", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "deferred_expense_account", + "expense_type", + "description", + "accounts" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "expense_type", - "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": "Expense Claim Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "expense_type", - "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, - "translatable": 0, + "fieldname": "expense_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Expense Claim Type", + "oldfieldname": "expense_type", + "oldfieldtype": "Data", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "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": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Small 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, + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Small Text", "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "accounts", - "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": "Accounts", - "length": 0, - "no_copy": 0, - "options": "Expense Claim Account", - "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": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Expense Claim Account" + }, + { + "default": "0", + "fieldname": "deferred_expense_account", + "fieldtype": "Check", + "label": "Deferred Expense Account" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-flag", - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-18 14:13:43.770829", - "modified_by": "Administrator", - "module": "HR", - "name": "Expense Claim Type", - "owner": "harshada@webnotestech.com", + ], + "icon": "fa fa-flag", + "idx": 1, + "modified": "2019-11-22 12:00:18.710408", + "modified_by": "jangeles@bai.ph", + "module": "HR", + "name": "Expense Claim Type", + "owner": "harshada@webnotestech.com", "permissions": [ { - "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": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Employee", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Employee" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 68b6cc0f370..053cae05672 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -648,4 +648,5 @@ 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 erpnext.patches.v12_0.set_production_capacity_in_workstation -erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order \ No newline at end of file +erpnext.patches.v12_0.set_against_blanket_order_in_sales_and_purchase_order +erpnext.patches.v12_0.set_cost_center_in_child_table_of_expense_claim diff --git a/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py new file mode 100644 index 00000000000..8ba0d79a831 --- /dev/null +++ b/erpnext/patches/v12_0/set_cost_center_in_child_table_of_expense_claim.py @@ -0,0 +1,8 @@ +import frappe +def execute(): + frappe.reload_doc('hr', 'doctype', 'expense_claim_detail') + frappe.db.sql(""" + UPDATE `tabExpense Claim Detail` child, `tabExpense Claim` par + SET child.cost_center = par.cost_center + WHERE child.parent = par.name + """) \ No newline at end of file From ce42f48bfbe8ec13dc5d0da44b718eb1355091b0 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 10 Dec 2019 12:15:34 +0530 Subject: [PATCH 379/679] fix: Append expense account only if expense account exists (#19880) --- 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 9cc5c18f36ec8736a688315adb520d995ed0d753 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 10 Dec 2019 15:20:47 +0530 Subject: [PATCH 380/679] fix: removed only custom dashboard --- erpnext/hr/doctype/leave_application/leave_application.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js index e32d57011d8..14ffa0ed1f0 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.js +++ b/erpnext/hr/doctype/leave_application/leave_application.js @@ -60,6 +60,7 @@ frappe.ui.form.on("Leave Application", { } } }); + $("div").remove(".form-dashboard-section.custom"); frm.dashboard.add_section( frappe.render_template('leave_application_dashboard', { data: leave_details From 6e2c13f4c86c73c09f13c9167717f602b701de71 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 10 Dec 2019 15:55:05 +0530 Subject: [PATCH 381/679] feat(regional): Auto state wise taxation for GST India (#19469) * 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: Iteritems compatibility for python 3 --- .../purchase_invoice/regional/india.js | 3 + .../purchase_taxes_and_charges_template.json | 374 +++++------------ .../doctype/sales_invoice/regional/india.js | 5 + .../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 | 1 + .../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, 598 insertions(+), 669 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..48fa364faf0 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() { @@ -35,4 +39,5 @@ 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 2ea74f6d854..7f4ae3c1fc4 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 ed5bf9b5d82..2a5e6d8f49a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -246,10 +246,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 053cae05672..e26b1c88a87 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -645,6 +645,7 @@ 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.add_export_type_field_in_party_master erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger erpnext.patches.v12_0.update_price_or_product_discount erpnext.patches.v12_0.set_production_capacity_in_workstation 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 664d0d89b53f4a39df3566a4c87694e432c234c3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 10 Dec 2019 21:32:17 +0530 Subject: [PATCH 382/679] fix: incorrect account mapping for child companies (#19887) * 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 c58495dceedad0031fa946abc862c0de37956c26 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 10 Dec 2019 21:34:03 +0530 Subject: [PATCH 383/679] fix: Item-wise Sales History report not working (#19889) --- .../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 6aa763221b658453d59b01706ce788ea0914d7a3 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 10 Dec 2019 16:05:28 +0000 Subject: [PATCH 384/679] fix: rename appointment booking route (#19886) * rename appoinment booking route * fix: replace all references to book-appointment route --- erpnext/crm/doctype/appointment/appointment.py | 2 +- erpnext/support/web_form/issues/issues.json | 2 +- .../{book-appointment => book_appointment}/__init__.py | 0 .../www/{book-appointment => book_appointment}/index.css | 0 .../www/{book-appointment => book_appointment}/index.html | 2 +- .../www/{book-appointment => book_appointment}/index.js | 8 ++++---- .../www/{book-appointment => book_appointment}/index.py | 0 .../verify/__init__.py | 0 .../verify/index.html | 0 .../verify/index.py | 0 10 files changed, 7 insertions(+), 7 deletions(-) rename erpnext/www/{book-appointment => book_appointment}/__init__.py (100%) rename erpnext/www/{book-appointment => book_appointment}/index.css (100%) rename erpnext/www/{book-appointment => book_appointment}/index.html (97%) 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/__init__.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/support/web_form/issues/issues.json b/erpnext/support/web_form/issues/issues.json index 652114f7382..9b904ad7962 100644 --- a/erpnext/support/web_form/issues/issues.json +++ b/erpnext/support/web_form/issues/issues.json @@ -18,7 +18,7 @@ "is_standard": 1, "login_required": 1, "max_attachment_size": 0, - "modified": "2019-06-27 22:58:49.609672", + "modified": "2019-12-10 13:48:19.894186", "modified_by": "Administrator", "module": "Support", "name": "issues", diff --git a/erpnext/www/book-appointment/__init__.py b/erpnext/www/book_appointment/__init__.py similarity index 100% rename from erpnext/www/book-appointment/__init__.py rename to erpnext/www/book_appointment/__init__.py 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 97% rename from erpnext/www/book-appointment/index.html rename to 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/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/__init__.py b/erpnext/www/book_appointment/verify/__init__.py similarity index 100% rename from erpnext/www/book-appointment/verify/__init__.py rename to erpnext/www/book_appointment/verify/__init__.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 9046c13858be493e53be47312fbb3d5c1670cb2d Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 10 Dec 2019 21:37:27 +0530 Subject: [PATCH 385/679] fix: added extra condition (#19884) --- erpnext/hr/doctype/leave_application/leave_application.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 65fcbf7a999..915cea149d8 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -351,7 +351,7 @@ class LeaveApplication(Document): pass def create_leave_ledger_entry(self, submit=True): - if self.status != 'Approved': + if self.status != 'Approved' and submit: return expiry_date = get_allocation_expiry(self.employee, self.leave_type, From 5380a4c3db67cf8e69fce0c7c06661c893a4fee4 Mon Sep 17 00:00:00 2001 From: Khushal Trivedi Date: Tue, 10 Dec 2019 21:38:42 +0530 Subject: [PATCH 386/679] fix-education: date of birth validation on student form (#19875) * fix: date validation on inpatient record, else condition removing on clinical prcd templ which is not req * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix:Pricing Rule error AttributeError: 'str' object has no attribute 'get' #19770 * fix: joining and relieving Date can be on same date as valid use case * fix-education: date of birth validation --- erpnext/education/doctype/student/student.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 76825cec1b2..8e4b4e16f9a 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -5,12 +5,14 @@ from __future__ import unicode_literals import frappe from frappe.model.document import Document +from frappe.utils import getdate,today from frappe import _ from frappe.desk.form.linked_with import get_linked_doctypes from erpnext.education.utils import check_content_completion, check_quiz_completion class Student(Document): def validate(self): self.title = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name])) + self.validate_dates() if self.student_applicant: self.check_unique() @@ -19,6 +21,10 @@ class Student(Document): if frappe.get_value("Student", self.name, "title") != self.title: self.update_student_name_in_linked_doctype() + def validate_dates(self): + if self.date_of_birth and getdate(self.date_of_birth) >= getdate(today()): + frappe.throw(_("Date of Birth cannot be greater than today.")) + def update_student_name_in_linked_doctype(self): linked_doctypes = get_linked_doctypes("Student") for d in linked_doctypes: From 9e32f587f5ab35142c4945ace04ba48718a32335 Mon Sep 17 00:00:00 2001 From: Raffael Meyer Date: Tue, 10 Dec 2019 17:11:05 +0100 Subject: [PATCH 387/679] fix: remove contact info, use international format (#19828) - for international letters, city and country should be upper case: https://www.deutschepost.de/content/dam/dpag/images/B_b/Briefe_ins_Ausland/downloads/dp-brief-international-handlingbroschuere-072019.pdf#page=15 - it is not customary, to use contact information such as phone number in the address --- erpnext/regional/germany/address_template.html | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/germany/address_template.html b/erpnext/regional/germany/address_template.html index 0df786713c9..7fa4c32612a 100644 --- a/erpnext/regional/germany/address_template.html +++ b/erpnext/regional/germany/address_template.html @@ -1,8 +1,8 @@ {{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%} -{{ pincode }} {{ city }}
-{% if country %}{{ country }}
{% endif -%} -
-{% if phone %}Tel: {{ phone }}
{% endif -%} -{% if fax %}Fax: {{ fax }}
{% endif -%} -{% if email_id %}E-Mail: {{ email_id }}
{% endif -%} +{% if country in ["Germany", "Deutschland"] %} + {{ pincode }} {{ city }} +{% else %} + {{ pincode }} {{ city | upper }}
+ {{ country | upper }} +{% endif %} From ca6dbad7cb8c4566cd811dd21162035b2279795f Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 11 Dec 2019 12:10:05 +0530 Subject: [PATCH 388/679] 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 45e4b73e08e231862b264687d10849b3b9e4864e Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 11 Dec 2019 13:07:41 +0530 Subject: [PATCH 389/679] fix: rename fields --- .../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 37cf096102abf4b9520a7a78b4fa2867700d1488 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 11 Dec 2019 13:33:23 +0530 Subject: [PATCH 390/679] 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 d0fae6a2272..ad2f07d0846 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -609,7 +609,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 2ef26fbb0704b6ddd560708584625acca2e437ac Mon Sep 17 00:00:00 2001 From: prssanna Date: Wed, 11 Dec 2019 14:21:04 +0530 Subject: [PATCH 391/679] 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 d3605d235400813ae62ef2b346f8e83bcbf7445f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 11 Dec 2019 15:02:23 +0530 Subject: [PATCH 392/679] fix: issue in javascript timezones --- 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 c8dd5013d5c..5a814c6381e 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -114,7 +114,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 5b60dd5e7b7..e4af7e8e436 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -90,7 +90,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 5b622eace1d0c3602ecb820cccf6d0c769560e8f Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Wed, 11 Dec 2019 16:08:31 +0530 Subject: [PATCH 393/679] 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 9c0120d4168..22208921187 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -335,11 +335,13 @@ def get_items(filters=None, search=None): 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 c6599fd1e21ec04dc11a80a8bd1d7b97bd3a6a33 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 11 Dec 2019 17:29:04 +0530 Subject: [PATCH 394/679] fix: add publihed in hub track item --- .../hub_tracked_item/hub_tracked_item.json | 34 ++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/erpnext/hub_node/doctype/hub_tracked_item/hub_tracked_item.json b/erpnext/hub_node/doctype/hub_tracked_item/hub_tracked_item.json index 2e89887dacf..7d07ba40938 100644 --- a/erpnext/hub_node/doctype/hub_tracked_item/hub_tracked_item.json +++ b/erpnext/hub_node/doctype/hub_tracked_item/hub_tracked_item.json @@ -77,6 +77,38 @@ "translatable": 0, "unique": 0 }, + { + "allow_bulk_edit": 0, + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "published", + "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": "Published", + "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, @@ -120,7 +152,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-09-10 11:37:35.951019", + "modified": "2019-12-10 11:37:35.951019", "modified_by": "Administrator", "module": "Hub Node", "name": "Hub Tracked Item", From 894c2100272d7d7c9c3b95cdd17978e8f77afd93 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Wed, 11 Dec 2019 17:43:04 +0530 Subject: [PATCH 395/679] 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 107989316c7301c9475329f9a9eac81712743f2f Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Wed, 11 Dec 2019 18:15:54 +0530 Subject: [PATCH 396/679] fix: additional notes from Quotations not saved in SO --- .../sales_order_item/sales_order_item.json | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 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 86b09c28148..aad37d321e9 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -77,10 +77,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", @@ -746,15 +748,20 @@ "label": "Image" }, { - "default": "0", - "fieldname": "against_blanket_order", - "fieldtype": "Check", - "label": "Against Blanket Order" + "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-11-19 14:19:29.491945", + "modified": "2019-12-11 18:06:26.238169", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 9b0d7b93dd8c3e8a4d33a986fee256a8ee449699 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 12 Dec 2019 12:10:25 +0530 Subject: [PATCH 397/679] 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 f95267041da8b4c6ebda77ab9ffaa1d70e8dba23 Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 12 Dec 2019 13:06:17 +0530 Subject: [PATCH 398/679] 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 c58dc873d501b38edbc41c6100e60b277fe04d84 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 12 Dec 2019 14:55:57 +0530 Subject: [PATCH 399/679] 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 0e33f792d3aae99c781e804b5fc27ceb497ad5eb Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 12 Dec 2019 16:34:41 +0530 Subject: [PATCH 400/679] 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 81990aee9ff7fc385e3a6eb5a2e124f081d19829 Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 13 Dec 2019 13:12:10 +0530 Subject: [PATCH 401/679] 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 8c9c6ec919570ce73432286b563e2f930b4f3212 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 13 Dec 2019 13:47:15 +0530 Subject: [PATCH 402/679] 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 4bea44b9c93df4aa42d7362bac912b91489a777e Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 13 Dec 2019 15:05:43 +0530 Subject: [PATCH 403/679] fix: update already existing items --- erpnext/hub_node/api.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py index 3a5b06fdc4b..06ad2220345 100644 --- a/erpnext/hub_node/api.py +++ b/erpnext/hub_node/api.py @@ -119,6 +119,7 @@ def get_valid_items(search_value=''): @frappe.whitelist() def publish_selected_items(items_to_publish): items_to_publish = json.loads(items_to_publish) + items_to_update = [] if not len(items_to_publish): frappe.throw(_('No items to publish')) @@ -126,14 +127,24 @@ def publish_selected_items(items_to_publish): item_code = item.get('item_code') frappe.db.set_value('Item', item_code, 'publish_in_hub', 1) - frappe.get_doc({ + hub_dict = { 'doctype': 'Hub Tracked Item', 'item_code': item_code, + 'published': 1, 'hub_category': item.get('hub_category'), 'image_list': item.get('image_list') - }).insert(ignore_if_duplicate=True) + } + if frappe.db.exists('Hub Tracked Item', item_code): + items_to_update.append(item) + hub_tracked_item = frappe.get_doc('Hub Tracked Item', item_code) + hub_tracked_item.update(hub_dict) + hub_tracked_items.save() + else: + frappe.get_doc(hub_dict).insert(ignore_if_duplicate=True) - items = map_fields(items_to_publish) + items_to_publish = list(filter(lambda x: x not in items_to_update, items_to_publish)) + new_items = map_fields(items_to_publish) + existing_items = map_fields(items_to_update) try: item_sync_preprocess(len(items)) @@ -141,7 +152,8 @@ def publish_selected_items(items_to_publish): # TODO: Publish Progress connection = get_hub_connection() - connection.insert_many(items) + connection.insert_many(new_items) + connection.bulk_update(existing_items) item_sync_postprocess() except Exception as e: From bfc43d3b15d3dba788eeef5ad7d98777b0717a0b Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Fri, 13 Dec 2019 15:09:51 +0530 Subject: [PATCH 404/679] 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 5da7e729a59db35c3616a56b7fc833d595cb6169 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Fri, 13 Dec 2019 15:44:39 +0530 Subject: [PATCH 405/679] fix: remove mandatory purchase reference for existing asset --- erpnext/assets/doctype/asset/asset.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index 6b3f2c777cf..f6a7fa20d08 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -144,6 +144,10 @@ frappe.ui.form.on('Asset', { frm.set_df_property('purchase_invoice', 'read_only', 1); frm.set_df_property('purchase_receipt', 'read_only', 1); } + else if (frm.doc.is_existing_asset) { + frm.toggle_reqd('purchase_receipt', 0); + frm.toggle_reqd('purchase_invoice', 0); + } else if (frm.doc.purchase_receipt) { // if purchase receipt link is set then set PI disabled frm.toggle_reqd('purchase_invoice', 0); @@ -256,6 +260,7 @@ frappe.ui.form.on('Asset', { }, is_existing_asset: function(frm) { + frm.trigger("toggle_reference_doc"); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); }, From 94ff8058acf448f4f7f60f8ea1535c1408676c4d Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sat, 14 Dec 2019 21:25:30 +0530 Subject: [PATCH 406/679] 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 40b7ac4987cfd76e8226e924a8bf098592262662 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sun, 15 Dec 2019 21:20:55 +0530 Subject: [PATCH 407/679] fix: Minor filter fix in expense claim type --- erpnext/hr/doctype/expense_claim/expense_claim.js | 6 +++--- erpnext/hr/doctype/expense_claim/expense_claim.json | 5 ++++- erpnext/hr/doctype/expense_claim_type/expense_claim_type.js | 6 +++--- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 570f2ef4c77..e0bfc83a9bc 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -243,11 +243,11 @@ frappe.ui.form.on("Expense Claim", { 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){ + $.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{ + } else { frm.doc.advances[i].allocated_amount = amount_to_be_allocated; amount_to_be_allocated = 0; } diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index b5b6823e1ce..96baaab5950 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-01-10 16:34:14", @@ -43,6 +44,7 @@ "accounting_dimensions_section", "project", "dimension_col_break", + "cost_center", "more_details", "status", "amended_from", @@ -365,7 +367,8 @@ "icon": "fa fa-money", "idx": 1, "is_submittable": 1, - "modified": "2019-11-09 14:13:08.964547", + "links": [], + "modified": "2019-12-14 23:52:05.388458", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js index c4877976775..d007e1a6c23 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.js @@ -2,10 +2,10 @@ // License: GNU General Public License v3. See license.txt frappe.ui.form.on("Expense Claim Type", { - refresh: function(frm){ - frm.fields_dict["accounts"].grid.get_field("default_account").get_query = function(frm, cdt, cdn){ + refresh: function(frm) { + frm.fields_dict["accounts"].grid.get_field("default_account").get_query = function(doc, cdt, cdn) { var d = locals[cdt][cdn]; - return{ + return { filters: { "is_group": 0, "root_type": frm.doc.deferred_expense_account ? "Asset" : "Expense", From b223dd95e2ce03af052dc4b954a2070312de30bf Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sun, 15 Dec 2019 21:21:31 +0530 Subject: [PATCH 408/679] fix: Dashboard for employee advance doctype --- .../employee_advance_dashboard.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py diff --git a/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py new file mode 100644 index 00000000000..c3b4a3a8894 --- /dev/null +++ b/erpnext/hr/doctype/employee_advance/employee_advance_dashboard.py @@ -0,0 +1,19 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'employee_advance', + 'non_standard_fieldnames': { + 'Payment Entry': 'reference_name', + 'Journal Entry': 'reference_name' + }, + 'transactions': [ + { + 'items': ['Expense Claim'] + }, + { + 'items': ['Payment Entry', 'Journal Entry'] + } + ] + } From c05bd1964f066cc965a1d2facb1228bd8f618263 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sun, 15 Dec 2019 21:22:28 +0530 Subject: [PATCH 409/679] feat: Capture return amount against Employee advance via Journal Entry --- .../doctype/journal_entry/journal_entry.js | 1 - .../employee_advance/employee_advance.js | 2 +- .../employee_advance/employee_advance.json | 890 ++++-------------- .../employee_advance/employee_advance.py | 14 + .../hr/doctype/expense_claim/expense_claim.py | 2 +- 5 files changed, 199 insertions(+), 710 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d6236cdb04f..3604b60b751 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -190,7 +190,6 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ if(jvd.reference_type==="Employee Advance") { return { filters: { - 'status': ['=', 'Unpaid'], 'docstatus': 1 } }; diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 77a2bbcbc07..69915fa6e98 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -34,7 +34,7 @@ frappe.ui.form.on('Employee Advance', { } else if ( frm.doc.docstatus === 1 - && flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) + && flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) - flt(frm.doc.return_amount) && frappe.model.can_create("Expense Claim") ) { frm.add_custom_button( diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 3597e76f1e6..a4e46765470 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -1,737 +1,213 @@ { - "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": "2017-10-09 14:26:29.612365", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2017-10-09 14:26:29.612365", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "employee", + "employee_name", + "column_break_4", + "posting_date", + "department", + "section_break_8", + "purpose", + "column_break_11", + "advance_amount", + "paid_amount", + "due_advance_amount", + "claimed_amount", + "return_amount", + "section_break_7", + "status", + "company", + "amended_from", + "column_break_18", + "advance_account", + "mode_of_payment" + ], "fields": [ { - "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": 0, - "options": "HR-EAD-.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", + "label": "Series", + "options": "HR-EAD-.YYYY.-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "employee", - "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": "Employee", - "length": 0, - "no_copy": 0, - "options": "Employee", - "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": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.employee_name", - "fieldname": "employee_name", - "fieldtype": "Read Only", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Employee 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": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Read Only", + "label": "Employee Name" + }, { - "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, - "default": "Today", - "fieldname": "posting_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": 0, - "label": "Posting Date", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "Today", + "fieldname": "posting_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Posting Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fieldname": "department", - "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": "Department", - "length": 0, - "no_copy": 0, - "options": "Department", - "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 - }, + "fetch_from": "employee.department", + "fieldname": "department", + "fieldtype": "Link", + "label": "Department", + "options": "Department", + "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_8", - "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_8", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "purpose", - "fieldtype": "Small Text", - "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": "Purpose", - "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": "purpose", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Purpose", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_11", - "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_11", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "advance_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": "Advance Amount", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "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": "advance_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Advance Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "paid_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": "Paid Amount", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount", + "no_copy": 1, + "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, - "depends_on": "eval:cur_frm.doc.employee", - "fieldname": "due_advance_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": "Due Advance Amount", - "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 - }, + "depends_on": "eval:cur_frm.doc.employee", + "fieldname": "due_advance_amount", + "fieldtype": "Currency", + "label": "Due Advance Amount", + "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": "claimed_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": "Claimed Amount", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "claimed_amount", + "fieldtype": "Currency", + "label": "Claimed Amount", + "no_copy": 1, + "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": "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": "status", - "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": "Status", - "length": 0, - "no_copy": 1, - "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled", - "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": "status", + "fieldtype": "Select", + "label": "Status", + "no_copy": 1, + "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled", + "read_only": 1 + }, { - "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": 0, - "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", + "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": "amended_from", - "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": "Amended From", - "length": 0, - "no_copy": 1, - "options": "Employee Advance", - "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 - }, + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Advance", + "print_hide": 1, + "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_18", - "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_18", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "advance_account", - "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": "Advance Account", - "length": 0, - "no_copy": 0, - "options": "Account", - "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": "advance_account", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Advance Account", + "options": "Account", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "mode_of_payment", - "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": "Mode of Payment", - "length": 0, - "no_copy": 0, - "options": "Mode of Payment", - "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": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, + { + "fieldname": "return_amount", + "fieldtype": "Currency", + "label": "Return Amount", + "options": "Company:company:default_currency", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-30 11:28:15.529649", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Advance", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2019-12-15 19:04:07.044505", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Advance", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 0, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Employee", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "share": 1, "write": 1 - }, + }, { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Expense Approver", - "set_user_permissions": 0, - "share": 1, - "submit": 1, + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Expense Approver", + "share": 1, + "submit": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "employee,employee_name", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "search_fields": "employee,employee_name", + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 7813da78ca4..674e46438bf 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -53,11 +53,25 @@ class EmployeeAdvance(Document): and party = %s """, (self.name, self.employee), as_dict=1)[0].paid_amount + return_amount = frappe.db.sql(""" + select name, ifnull(sum(credit_in_account_currency), 0) as return_amount + from `tabGL Entry` + where against_voucher_type = 'Employee Advance' + and voucher_type != 'Expense Claim' + and against_voucher = %s + and party_type = 'Employee' + and party = %s + """, (self.name, self.employee), as_dict=1)[0].return_amount + if flt(paid_amount) > self.advance_amount: frappe.throw(_("Row {0}# Paid Amount cannot be greater than requested advance amount"), EmployeeAdvanceOverPayment) + if flt(return_amount) > self.paid_amount - self.claimed_amount: + frappe.throw(_("Return amount cannot be greater unclaimed amount")) + self.db_set("paid_amount", paid_amount) + self.db_set("return_amount", return_amount) self.set_status() frappe.db.set_value("Employee Advance", self.name , "status", self.status) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index dfb0bb96d82..9aeb7e8498e 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -322,7 +322,7 @@ def get_expense_claim_account(expense_claim_type, company): @frappe.whitelist() def get_advances(employee, advance_id=None): if not advance_id: - condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount'.format(frappe.db.escape(employee)) + condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee)) else: condition = 'name={0}'.format(frappe.db.escape(advance_id)) From 21fe97e72373cfae21572de39fa37a39732679c5 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 13 Dec 2019 16:18:38 +0530 Subject: [PATCH 410/679] 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 e871d98af6a..af6e4c87afc 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -182,7 +182,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) @@ -241,9 +241,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 @@ -293,7 +295,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 637e503e658..346eba259bd 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 46a58fba7cc..1be4f27289e 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 5ab823beab4d3b7e1f7093ec696984cf98d651ca Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Dec 2019 12:08:41 +0530 Subject: [PATCH 411/679] 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 189261cb2de..151be110fca 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 8bd84b28cae9cd513b3374dadad3c82202c12591 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Dec 2019 12:29:07 +0530 Subject: [PATCH 412/679] 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 af6e4c87afc..b99c07e6367 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -245,7 +245,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 346eba259bd..bd5a14971c1 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 e9c9cbd2fe981bd68d9a9fc36ce458b17e156821 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 16 Dec 2019 14:49:59 +0530 Subject: [PATCH 413/679] 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 f195ccd85ff7ca7b859c6606bb4f5476429fc884 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 16 Dec 2019 15:03:27 +0530 Subject: [PATCH 414/679] 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 22208921187..0993e69e042 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -301,6 +301,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') @@ -336,12 +338,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 197a99ee354a1a67f4cb2fbc1d80ba4f40f61318 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Dec 2019 16:18:33 +0530 Subject: [PATCH 415/679] 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 e3ece569641..f8146bb01e0 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 b5bf22a821fc84923a7bd3fa33496c880716e4c2 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Dec 2019 16:53:00 +0530 Subject: [PATCH 416/679] 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 8ca89171b66..176ca2e4f52 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -605,6 +605,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 227ef787cac..2b936be7985 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -38,7 +38,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 83ecaefd4cd5a8de24ebc1a6dd8fedd82b0a8e58 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 17 Dec 2019 11:03:22 +0530 Subject: [PATCH 417/679] feat: Custom button to create return entry from Employee Advance --- .../employee_advance/employee_advance.js | 27 +++++++++++++++ .../employee_advance/employee_advance.py | 33 +++++++++++++++++-- 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 69915fa6e98..54a75925daa 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -45,6 +45,15 @@ frappe.ui.form.on('Employee Advance', { __('Create') ); } + + if (frm.doc.docstatus === 1 + && (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount)) + && frappe.model.can_create("Journal Entry")) { + + frm.add_custom_button(__("Return"), function() { + frm.trigger('make_return_entry'); + }, __('Create')); + } }, make_payment_entry: function(frm) { @@ -83,6 +92,24 @@ frappe.ui.form.on('Employee Advance', { }); }, + make_return_entry: function(frm) { + frappe.call({ + method: 'erpnext.hr.doctype.employee_advance.employee_advance.make_return_entry', + args: { + 'employee_name': frm.doc.employee, + 'company': frm.doc.company, + 'employee_advance_name': frm.doc.name, + 'return_amount': flt(frm.doc.paid_amount - frm.doc.claimed_amount), + 'mode_of_payment': frm.doc.mode_of_payment, + 'advance_account': frm.doc.advance_account + }, + callback: function(r) { + const doclist = frappe.model.sync(r.message); + frappe.set_route('Form', doclist[0].doctype, doclist[0].name); + } + }) + }, + employee: function (frm) { if (frm.doc.employee) { return frappe.call({ diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 674e46438bf..7fe2ebc79e0 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -7,6 +7,7 @@ import frappe, erpnext from frappe import _ from frappe.model.document import Document from frappe.utils import flt, nowdate +from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account class EmployeeAdvanceOverPayment(frappe.ValidationError): pass @@ -102,8 +103,6 @@ def get_due_advance_amount(employee, posting_date): @frappe.whitelist() def make_bank_entry(dt, dn): - from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account - doc = frappe.get_doc(dt, dn) payment_account = get_default_bank_cash_account(doc.company, account_type="Cash", mode_of_payment=doc.mode_of_payment) @@ -132,3 +131,33 @@ def make_bank_entry(dt, dn): }) return je.as_dict() + +@frappe.whitelist() +def make_return_entry(employee_name, company, employee_advance_name, return_amount, mode_of_payment, advance_account): + return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) + je = frappe.new_doc('Journal Entry') + je.posting_date = nowdate() + je.voucher_type = 'Bank Entry' + je.company = company + je.remark = 'Return against Employee Advance: ' + employee_advance_name + + je.append('accounts', { + 'account': advance_account, + 'credit_in_account_currency': return_amount, + 'reference_type': 'Employee Advance', + 'reference_name': employee_advance_name, + 'party_type': 'Employee', + 'party': employee_name, + 'is_advance': 'Yes' + }) + + je.append("accounts", { + "account": return_account.account, + "debit_in_account_currency": return_amount, + "account_currency": return_account.account_currency, + "account_type": return_account.account_type + }) + + return je.as_dict() + + From 0f583b8c5a3c284d8029bcb7589e041e8e2f49a0 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 17 Dec 2019 12:44:19 +0530 Subject: [PATCH 418/679] fix: incorrect outstanding amount shwoing in the AP/AR report --- .../accounts/report/accounts_receivable/accounts_receivable.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 2c53f6e9971..c70a2cd1a74 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -171,7 +171,7 @@ class ReceivablePayableReport(object): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 0.1/10 ** self.currency_precision: + if abs(row.outstanding) > 1.0/10 ** self.currency_precision: # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: From e65ada28077e980d6a58a168a0bd58f013e10b60 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 17 Dec 2019 17:54:34 +0530 Subject: [PATCH 419/679] feat: Dynamic filters for dimensions in budget variance report --- .../budget_variance_report.js | 21 ++++++--- .../budget_variance_report.py | 43 +++++++++---------- 2 files changed, 36 insertions(+), 28 deletions(-) diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js index 24511871fdb..3ec4d306c35 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.js +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.js @@ -46,13 +46,24 @@ frappe.query_reports["Budget Variance Report"] = { fieldtype: "Select", options: ["Cost Center", "Project"], default: "Cost Center", - reqd: 1 + reqd: 1, + on_change: function() { + frappe.query_report.set_filter_value("budget_against_filter", []); + frappe.query_report.refresh(); + } }, { - fieldname: "cost_center", - label: __("Cost Center"), - fieldtype: "Link", - options: "Cost Center" + fieldname:"budget_against_filter", + label: __('Dimension Filter'), + fieldtype: "MultiSelectList", + get_data: function(txt) { + if (!frappe.query_report.filters) return; + + let budget_against = frappe.query_report.get_filter_value('budget_against'); + if (!budget_against) return; + + return frappe.db.get_link_options(budget_against, txt); + } }, { fieldname:"show_cumulative", diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index 8d65ac87148..39e218bfad2 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -12,22 +12,22 @@ from six import iteritems from pprint import pprint def execute(filters=None): if not filters: filters = {} - validate_filters(filters) + columns = get_columns(filters) - if filters.get("cost_center"): - cost_centers = [filters.get("cost_center")] + if filters.get("budget_against_filter"): + dimensions = filters.get("budget_against_filter") else: - cost_centers = get_cost_centers(filters) + dimensions = get_cost_centers(filters) period_month_ranges = get_period_month_ranges(filters["period"], filters["from_fiscal_year"]) - cam_map = get_cost_center_account_month_map(filters) + cam_map = get_dimension_account_month_map(filters) data = [] - for cost_center in cost_centers: - cost_center_items = cam_map.get(cost_center) - if cost_center_items: - for account, monthwise_data in iteritems(cost_center_items): - row = [cost_center, account] + for dimension in dimensions: + dimension_items = cam_map.get(dimension) + if dimension_items: + for account, monthwise_data in iteritems(dimension_items): + row = [dimension, account] totals = [0, 0, 0] for year in get_fiscal_years(filters): last_total = 0 @@ -55,10 +55,6 @@ def execute(filters=None): return columns, data -def validate_filters(filters): - if filters.get("budget_against") != "Cost Center" and filters.get("cost_center"): - frappe.throw(_("Filter based on Cost Center is only applicable if Budget Against is selected as Cost Center")) - def get_columns(filters): columns = [_(filters.get("budget_against")) + ":Link/%s:150"%(filters.get("budget_against")), _("Account") + ":Link/Account:150"] @@ -98,11 +94,12 @@ def get_cost_centers(filters): else: return frappe.db.sql_list("""select name from `tab{tab}`""".format(tab=filters.get("budget_against"))) #nosec -#Get cost center & target details -def get_cost_center_target_details(filters): +#Get dimension & target details +def get_dimension_target_details(filters): cond = "" - if filters.get("cost_center"): - cond += " and b.cost_center=%s" % frappe.db.escape(filters.get("cost_center")) + if filters.get("budget_against_filter"): + cond += " and b.{budget_against} in (%s)".format(budget_against = \ + frappe.scrub(filters.get('budget_against'))) % ', '.join(['%s']* len(filters.get('budget_against_filter'))) return frappe.db.sql(""" select b.{budget_against} as budget_against, b.monthly_distribution, ba.account, ba.budget_amount,b.fiscal_year @@ -110,8 +107,8 @@ def get_cost_center_target_details(filters): where b.name=ba.parent and b.docstatus = 1 and b.fiscal_year between %s and %s and b.budget_against = %s and b.company=%s {cond} order by b.fiscal_year """.format(budget_against=filters.get("budget_against").replace(" ", "_").lower(), cond=cond), - (filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company), as_dict=True) - + tuple([filters.from_fiscal_year,filters.to_fiscal_year,filters.budget_against, filters.company] + filters.get('budget_against_filter')), + as_dict=True) #Get target distribution details of accounts of cost center @@ -153,14 +150,14 @@ def get_actual_details(name, filters): return cc_actual_details -def get_cost_center_account_month_map(filters): +def get_dimension_account_month_map(filters): import datetime - cost_center_target_details = get_cost_center_target_details(filters) + dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) cam_map = {} - for ccd in cost_center_target_details: + for ccd in dimension_target_details: actual_details = get_actual_details(ccd.budget_against, filters) for month_id in range(1, 13): From b695fd31d633ea18d9bf786c5244a56dc1704e80 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 17 Dec 2019 18:14:33 +0530 Subject: [PATCH 420/679] fix: Remove trailing whitespace and add semicolon --- erpnext/hr/doctype/employee_advance/employee_advance.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 54a75925daa..ba62853336d 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -46,10 +46,10 @@ frappe.ui.form.on('Employee Advance', { ); } - if (frm.doc.docstatus === 1 + if (frm.doc.docstatus === 1 && (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount)) && frappe.model.can_create("Journal Entry")) { - + frm.add_custom_button(__("Return"), function() { frm.trigger('make_return_entry'); }, __('Create')); @@ -107,7 +107,7 @@ frappe.ui.form.on('Employee Advance', { const doclist = frappe.model.sync(r.message); frappe.set_route('Form', doclist[0].doctype, doclist[0].name); } - }) + }); }, employee: function (frm) { From 4203eb4ee12638c67293152bc0e3dc4f305a991b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 17 Dec 2019 18:13:54 +0530 Subject: [PATCH 421/679] 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 2b936be7985..c4238accac7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -657,8 +657,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 be8c4068082ea59fa8b75f8f268438db200e97a4 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 17 Dec 2019 22:27:23 +0530 Subject: [PATCH 422/679] fix: Update sales register report --- .../report/sales_register/sales_register.py | 235 +++++++++++++++--- 1 file changed, 205 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py index 0e2821ac16d..afdd31df16d 100644 --- a/erpnext/accounts/report/sales_register/sales_register.py +++ b/erpnext/accounts/report/sales_register/sales_register.py @@ -38,32 +38,46 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No cost_center = list(set(invoice_cc_wh_map.get(inv.name, {}).get("cost_center", []))) warehouse = list(set(invoice_cc_wh_map.get(inv.name, {}).get("warehouse", []))) - row = [ - inv.name, inv.posting_date, inv.customer, inv.customer_name - ] + row = { + 'invoice': inv.name, + 'posting_date': inv.posting_date, + 'customer': inv.customer, + 'customer_name': inv.customer_name + } if additional_query_columns: for col in additional_query_columns: - row.append(inv.get(col)) + row.update({ + col: inv.get(col) + }) + + row.update({ + 'customer_group': inv.get("customer_group"), + 'territory': inv.get("territory"), + 'tax_id': inv.get("tax_id"), + 'receivable_account': inv.debit_to, + 'mode_of_payment': ", ".join(mode_of_payments.get(inv.name, [])), + 'project': inv.project, + 'owner': inv.owner, + 'remarks': inv.remarks, + 'sales_order': ", ".join(sales_order), + 'delivery_note': ", ".join(delivery_note), + 'cost_center': ", ".join(cost_center), + 'warehouse': ", ".join(warehouse), + 'currency': company_currency + }) - row +=[ - inv.get("customer_group"), - inv.get("territory"), - inv.get("tax_id"), - inv.debit_to, ", ".join(mode_of_payments.get(inv.name, [])), - inv.project, inv.owner, inv.remarks, - ", ".join(sales_order), ", ".join(delivery_note),", ".join(cost_center), - ", ".join(warehouse), company_currency - ] # map income values base_net_total = 0 for income_acc in income_accounts: income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc)) base_net_total += income_amount - row.append(income_amount) + row.update({ + frappe.scrub(income_acc): income_amount + }) # net total - row.append(base_net_total or inv.base_net_total) + row.update({'net_total': base_net_total or inv.base_net_total}) # tax account total_tax = 0 @@ -72,10 +86,18 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No tax_amount_precision = get_field_precision(frappe.get_meta("Sales Taxes and Charges").get_field("tax_amount"), currency=company_currency) or 2 tax_amount = flt(invoice_tax_map.get(inv.name, {}).get(tax_acc), tax_amount_precision) total_tax += tax_amount - row.append(tax_amount) + row.update({ + frappe.scrub(tax_acc): tax_amount + }) # total tax, grand total, outstanding amount & rounded total - row += [total_tax, inv.base_grand_total, inv.base_rounded_total, inv.outstanding_amount] + + row.update({ + 'tax_total': total_tax, + 'grand_total': inv.base_grand_total, + 'rounded_total': inv.base_rounded_total, + 'outstanding_amount': inv.outstanding_amount + }) data.append(row) @@ -84,19 +106,118 @@ def _execute(filters, additional_table_columns=None, additional_query_columns=No def get_columns(invoice_list, additional_table_columns): """return columns based on filters""" columns = [ - _("Invoice") + ":Link/Sales Invoice:120", _("Posting Date") + ":Date:80", - _("Customer") + ":Link/Customer:120", _("Customer Name") + "::120" + { + 'label': _("Invoice"), + 'fieldname': 'invoice', + 'fieldtype': 'Link', + 'options': 'Sales Invoice', + 'width': 120 + }, + { + 'label': _("Posting Date"), + 'fieldname': 'posting_date', + 'fieldtype': 'Date', + 'width': 80 + }, + { + 'label': _("Customer"), + 'fieldname': 'customer', + 'fieldtype': 'Link', + 'options': 'Customer', + 'width': 120 + }, + { + 'label': _("Customer Name"), + 'fieldname': 'customer_name', + 'fieldtype': 'Data', + 'width': 120 + }, ] if additional_table_columns: columns += additional_table_columns columns +=[ - _("Customer Group") + ":Link/Customer Group:120", _("Territory") + ":Link/Territory:80", - _("Tax Id") + "::80", _("Receivable Account") + ":Link/Account:120", _("Mode of Payment") + "::120", - _("Project") +":Link/Project:80", _("Owner") + "::150", _("Remarks") + "::150", - _("Sales Order") + ":Link/Sales Order:100", _("Delivery Note") + ":Link/Delivery Note:100", - _("Cost Center") + ":Link/Cost Center:100", _("Warehouse") + ":Link/Warehouse:100", + { + 'label': _("Custmer Group"), + 'fieldname': 'customer_group', + 'fieldtype': 'Link', + 'options': 'Customer Group', + 'width': 120 + }, + { + 'label': _("Territory"), + 'fieldname': 'territory', + 'fieldtype': 'Link', + 'options': 'Territory', + 'width': 80 + }, + { + 'label': _("Tax Id"), + 'fieldname': 'tax_id', + 'fieldtype': 'Data', + 'width': 120 + }, + { + 'label': _("Receivable Account"), + 'fieldname': 'receivable_account', + 'fieldtype': 'Link', + 'options': 'Account', + 'width': 80 + }, + { + 'label': _("Mode Of Payment"), + 'fieldname': 'mode_of_payment', + 'fieldtype': 'Data', + 'width': 120 + }, + { + 'label': _("Project"), + 'fieldname': 'project', + 'fieldtype': 'Link', + 'options': 'project', + 'width': 80 + }, + { + 'label': _("Owner"), + 'fieldname': 'owner', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _("Remarks"), + 'fieldname': 'remarks', + 'fieldtype': 'Data', + 'width': 150 + }, + { + 'label': _("Sales Order"), + 'fieldname': 'sales_order', + 'fieldtype': 'Link', + 'options': 'Sales Order', + 'width': 100 + }, + { + 'label': _("Delivery Note"), + 'fieldname': 'delivery_note', + 'fieldtype': 'Link', + 'options': 'Delivery Note', + 'width': 100 + }, + { + 'label': _("Cost Center"), + 'fieldname': 'cost_center', + 'fieldtype': 'Link', + 'options': 'Cost Center', + 'width': 100 + }, + { + 'label': _("Warehouse"), + 'fieldname': 'warehouse', + 'fieldtype': 'Link', + 'options': 'Warehouse', + 'width': 100 + }, { "fieldname": "currency", "label": _("Currency"), @@ -105,7 +226,10 @@ def get_columns(invoice_list, additional_table_columns): } ] - income_accounts = tax_accounts = income_columns = tax_columns = [] + income_accounts = [] + tax_accounts = [] + income_columns = [] + tax_columns = [] if invoice_list: income_accounts = frappe.db.sql_list("""select distinct income_account @@ -119,14 +243,65 @@ def get_columns(invoice_list, additional_table_columns): and parent in (%s) order by account_head""" % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list])) - income_columns = [(account + ":Currency/currency:120") for account in income_accounts] + for account in income_accounts: + income_columns.append({ + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }) + for account in tax_accounts: if account not in income_accounts: - tax_columns.append(account + ":Currency/currency:120") + tax_columns.append({ + "label": account, + "fieldname": frappe.scrub(account), + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }) - columns = columns + income_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \ - [_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120", - _("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"] + net_total_column = [{ + "label": _("Net Total"), + "fieldname": "net_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }] + + total_columns = [ + { + "label": _("Tax Total"), + "fieldname": "tax_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }, + { + "label": _("Grand Total"), + "fieldname": "grand_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }, + { + "label": _("Rounded Total"), + "fieldname": "rounded_total", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + }, + { + "label": _("Outstanding Amount"), + "fieldname": "outstanding_amount", + "fieldtype": "Currency", + "options": 'currency', + "width": 120 + } + ] + + columns = columns + income_columns + net_total_column + tax_columns + total_columns return columns, income_accounts, tax_accounts From f597ba82ea41a93228ed83c3d4fbe3aae4fae28f Mon Sep 17 00:00:00 2001 From: Ben Knowles Date: Tue, 17 Dec 2019 13:07:02 -0600 Subject: [PATCH 423/679] fix: task validation error when adding tasks to projects (#19919) * fix: task validation error when adding tasks to projects When adding a task to a project, if the project didn't have an Expected End Date the validation would fail. This is because passing a None value to getdate() returns today's date, rather than being optional as expected. * update task.py --- 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 e7aa20ebef1b3442d7f962434e8ae285f2ad8933 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 16 Dec 2019 16:52:42 +0530 Subject: [PATCH 424/679] fix: replace sql query by orm in delete_communications and added tests --- .../company/delete_company_transactions.py | 11 ++-- erpnext/setup/doctype/company/test_company.py | 51 +++++++++++++++++++ 2 files changed, 58 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py index 637e65578a1..1503adb504c 100644 --- a/erpnext/setup/doctype/company/delete_company_transactions.py +++ b/erpnext/setup/doctype/company/delete_company_transactions.py @@ -106,7 +106,10 @@ def delete_lead_addresses(company_name): frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads))) def delete_communications(doctype, company_name, company_fieldname): - frappe.db.sql(""" - DELETE FROM `tabCommunication` WHERE reference_doctype = %s AND - EXISTS (SELECT name FROM `tab{0}` WHERE {1} = %s AND `tabCommunication`.reference_name = name) - """.format(doctype, company_fieldname), (doctype, company_name)) + reference_docs = frappe.get_all(doctype, filters={company_fieldname:company_name}) + reference_doc_names = [r.name for r in reference_docs] + + communications = frappe.get_all("Communication", filters={"reference_doctype":doctype,"reference_name":["in", reference_doc_names]}) + communication_names = [c.name for c in communications] + + frappe.delete_doc("Communication", communication_names) diff --git a/erpnext/setup/doctype/company/test_company.py b/erpnext/setup/doctype/company/test_company.py index 8d9c23a37da..1664b660b86 100644 --- a/erpnext/setup/doctype/company/test_company.py +++ b/erpnext/setup/doctype/company/test_company.py @@ -88,6 +88,57 @@ class TestCompany(unittest.TestCase): self.delete_mode_of_payment(template) frappe.delete_doc("Company", template) + def test_delete_communication(self): + from erpnext.setup.doctype.company.delete_company_transactions import delete_communications + company = create_child_company() + lead = create_test_lead_in_company(company) + communication = create_company_communication("Lead", lead) + delete_communications("Lead", "Test Company", "company") + self.assertFalse(frappe.db.exists("Communcation", communication)) + self.assertFalse(frappe.db.exists({"doctype":"Comunication Link", "link_name": communication})) + def delete_mode_of_payment(self, company): frappe.db.sql(""" delete from `tabMode of Payment Account` where company =%s """, (company)) + +def create_company_communication(doctype, docname): + comm = frappe.get_doc({ + "doctype": "Communication", + "communication_type": "Communication", + "content": "Deduplication of Links", + "communication_medium": "Email", + "reference_doctype":doctype, + "reference_name":docname + }) + comm.insert() + +def create_child_company(): + child_company = frappe.db.exists("Company", "Test Company") + if not child_company: + child_company = frappe.get_doc({ + "doctype":"Company", + "company_name":"Test Company", + "abbr":"test_company", + "default_currency":"INR" + }) + child_company.insert() + else: + child_company = frappe.get_doc("Company", child_company) + + return child_company.name + +def create_test_lead_in_company(company): + lead = frappe.db.exists("Lead", "Test Lead in new company") + if not lead: + lead = frappe.get_doc({ + "doctype": "Lead", + "lead_name": "Test Lead in new company", + "scompany": company + }) + lead.insert() + else: + lead = frappe.get_doc("Lead", lead) + lead.company = company + lead.save() + return lead.name + From 1202e64403eb50222e6012347ba76c0f56553c4d Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 18 Dec 2019 11:43:16 +0530 Subject: [PATCH 425/679] fix: Training Event Email Tenplate (#19652) --- .../training_scheduled.html | 51 ++++++++++++++++--- .../training_scheduled.json | 6 ++- .../training_scheduled/training_scheduled.md | 51 ++++++++++++++++--- 3 files changed, 90 insertions(+), 18 deletions(-) diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.html b/erpnext/hr/notification/training_scheduled/training_scheduled.html index b1aeb2c8739..374038ac202 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.html +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.html @@ -1,9 +1,44 @@ -

{{_("Training Event")}}

+ + + + + + + + +
+
+ {{_("Training Event:")}} {{ doc.event_name }} +
+
-

{{ doc.introduction }}

- -

{{_("Details")}}

-{{_("Event Name")}}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }} -
{{_("Event Location")}}: {{ doc.location }} -
{{_("Start Time")}}: {{ doc.start_time }} -
{{_("End Time")}}: {{ doc.end_time }} + + + + + + + + +
+
+ {{ doc.introduction }} +
    +
  • {{_("Event Location")}}: {{ doc.location }}
  • + {% set start = frappe.utils.get_datetime(doc.start_time) %} + {% set end = frappe.utils.get_datetime(doc.end_time) %} + {% if start.date() == end.date() %} +
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • +
  • + {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} +
  • + {% else %} +
  • {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • +
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • + {% endif %} +
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
+
+
\ No newline at end of file diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json index c07e1a6285b..966b8875723 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.json +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json @@ -1,5 +1,7 @@ { "attach_print": 0, + "channel": "Email", + "condition": "", "creation": "2017-08-11 03:13:40.519614", "days_in_advance": 0, "docstatus": 0, @@ -9,8 +11,8 @@ "event": "Submit", "idx": 0, "is_standard": 1, - "message": "

{{_(\"Training Event\")}}

\n\n

{{ doc.introduction }}

\n\n

{{_(\"Details\")}}

\n{{_(\"Event Name\")}}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n
{{_(\"Event Location\")}}: {{ doc.location }}\n
{{_(\"Start Time\")}}: {{ doc.start_time }}\n
{{_(\"End Time\")}}: {{ doc.end_time }}\n", - "modified": "2017-08-13 22:49:42.338881", + "message": "\n \n \n \n \n \n \n \n
\n
\n {{_(\"Training Event:\")}} {{ doc.event_name }}\n
\n
\n\n\n \n \n \n \n \n \n \n
\n
\n
    \n
  • {{ doc.introduction }}
  • \n
  • {{_(\"Event Location\")}}: {{ doc.location }}
  • \n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n
  • {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
  • \n
  • \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
  • \n {% else %}\n
  • {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n
  • {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
  • \n {% endif %}\n
\n {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n
\n
", + "modified": "2019-11-29 15:38:31.805409", "modified_by": "Administrator", "module": "HR", "name": "Training Scheduled", diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md index bcadf7df590..374038ac202 100644 --- a/erpnext/hr/notification/training_scheduled/training_scheduled.md +++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md @@ -1,9 +1,44 @@ -

{{_("Training Event")}}

-

{{ message }}

+ + + + + + + + +
+
+ {{_("Training Event:")}} {{ doc.event_name }} +
+
-

{{_("Details")}}

-{{_("Event Name")}}: {{ name }} -
{{_("Event Location")}}: {{ location }} -
{{_("Start Time")}}: {{ start_time }} -
{{_("End Time")}}: {{ end_time }} -
{{_("Attendance")}}: {{ attendance }} + + + + + + + + +
+
+ {{ doc.introduction }} +
    +
  • {{_("Event Location")}}: {{ doc.location }}
  • + {% set start = frappe.utils.get_datetime(doc.start_time) %} + {% set end = frappe.utils.get_datetime(doc.end_time) %} + {% if start.date() == end.date() %} +
  • {{_("Date")}}: {{ start.strftime("%A, %d %b %Y") }}
  • +
  • + {{_("Timing")}}: {{ start.strftime("%I:%M %p") + ' to ' + end.strftime("%I:%M %p") }} +
  • + {% else %} +
  • {{_("Start Time")}}: {{ start.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • +
  • {{_("End Time")}}: {{ end.strftime("%A, %d %b %Y at %I:%M %p") }} +
  • + {% endif %} +
  • {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
  • +
+
+
\ No newline at end of file From 5150c69ee6af5d21e44eb0c0d4eb2c6967221654 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 12:07:24 +0530 Subject: [PATCH 426/679] 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..aa1b92f9a4c 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 57308cea41b..73ab7e6bb72 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): @@ -238,6 +239,64 @@ def create_contact(contact, party_type, party, email): contact.append('links', dict(link_doctype=party_type, link_name=party)) contact.insert() +@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 84ae2cc5431972e0eb6483ccddd34f32950f5bc2 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 15:44:04 +0530 Subject: [PATCH 427/679] fix: defualt timezone not getting selected --- erpnext/www/book_appointment/index.js | 13 ++++--------- erpnext/www/book_appointment/index.py | 20 +++++++++++--------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index 5a814c6381e..262e31b3e40 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) => { + 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 = 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 e4af7e8e436..fe98c7a0e90 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -25,18 +25,20 @@ 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 + import pytz + 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="-") + # 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 + # 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 + # return translated_dict @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): From 0db86204cf86d0aff2d8c5c16ba3afef2341ac35 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 16:00:58 +0530 Subject: [PATCH 428/679] 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 73ab7e6bb72..9fd37adc13e 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -258,7 +258,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_value("Customer",source_name, 'default_price_list') + if price_list: + target_doc.selling_price_list = price_list + return target_doc @frappe.whitelist() From 5f5c725ef99153b8489c6755cac84515aed66c16 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 16:21:50 +0530 Subject: [PATCH 429/679] add book appointment to the sidebar --- erpnext/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2a5e6d8f49a..c99ae7da5e4 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -180,6 +180,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 e786eea7dbcf78c272ca8d90440d0afc783faf2e Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 16:25:26 +0530 Subject: [PATCH 430/679] remove comments --- erpnext/www/book_appointment/index.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index fe98c7a0e90..7bfac89f308 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -27,18 +27,6 @@ def get_appointment_settings(): def get_timezones(): import pytz 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 2ae79b8ac2c64df86bd33971b6e8ed39ae5d8b41 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Dec 2019 17:48:39 +0530 Subject: [PATCH 431/679] 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 b99c07e6367..3c14819e6fe 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -47,6 +47,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 1be4f27289e..6db849aca77 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 aba58ba50ef19ea3d9dd464b9798f9f736a7f5ca Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 19 Dec 2019 11:02:37 +0530 Subject: [PATCH 432/679] fix: Spacing after commas --- 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 9fd37adc13e..136236c417c 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -243,9 +243,9 @@ def create_contact(contact, party_type, party, email): 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": { @@ -258,7 +258,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_value("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 @@ -267,17 +267,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 9d9a78442e849dd248781f9917361f3e77681852 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Thu, 19 Dec 2019 13:10:57 +0530 Subject: [PATCH 433/679] fix: bad filter query --- .../purchase_invoice/purchase_invoice.js | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index d7e64cf36fd..643de7d300a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -382,21 +382,11 @@ cur_frm.fields_dict['items'].grid.get_field("item_code").get_query = function(do cur_frm.fields_dict['credit_to'].get_query = function(doc) { // filter on Account - if (doc.supplier) { - return { - filters: { - 'account_type': 'Payable', - 'is_group': 0, - 'company': doc.company - } - } - } else { - return { - filters: { - 'report_type': 'Balance Sheet', - 'is_group': 0, - 'company': doc.company - } + return { + filters: { + 'account_type': 'Payable', + 'is_group': 0, + 'company': doc.company } } } From e9cb561d8d1ae27b1d505d820a07424bfb76b9fc Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Thu, 19 Dec 2019 13:18:46 +0530 Subject: [PATCH 434/679] fix: no role has cancelling permission for share transfer doctype --- erpnext/accounts/doctype/share_transfer/share_transfer.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.json b/erpnext/accounts/doctype/share_transfer/share_transfer.json index f17bf04cafd..9e549e06858 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.json +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.json @@ -188,7 +188,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-07 13:31:17.999744", + "modified": "2019-12-19 13:15:59.001301", "modified_by": "Administrator", "module": "Accounts", "name": "Share Transfer", @@ -196,6 +196,7 @@ "permissions": [ { "amend": 1, + "cancel": 1, "create": 1, "delete": 1, "email": 1, From 457ca0fe6cc0e66c25cdf05c85098f3d0483b07c Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 19 Dec 2019 13:34:29 +0530 Subject: [PATCH 435/679] fix: Company None not found in get_valuation_rate --- 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 00d27ef232b..1b9660e6d2c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -372,7 +372,7 @@ class StockEntry(StockController): elif d.t_warehouse and not d.basic_rate: d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse, self.doctype, self.name, d.allow_zero_valuation_rate, - currency=erpnext.get_company_currency(self.company)) + currency=erpnext.get_company_currency(self.company), company=self.company) def set_actual_qty(self): allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock")) From 1e4ea466d3a58c2433006cdccf919a7acd0763fa Mon Sep 17 00:00:00 2001 From: Gavin D'souza Date: Wed, 11 Dec 2019 09:36:17 +0530 Subject: [PATCH 436/679] style: update/standardize rename_doc usage --- .../v11_0/merge_land_unit_with_location.py | 5 ++-- .../v11_0/rename_asset_adjustment_doctype.py | 4 ++-- .../patches/v11_0/rename_health_insurance.py | 3 +-- .../rename_healthcare_doctype_and_fields.py | 3 +-- .../rename_production_order_to_work_order.py | 9 ++++--- .../rename_supplier_type_to_supplier_group.py | 3 +-- .../rename_pricing_rule_child_doctypes.py | 3 +-- erpnext/stock/doctype/item/test_item.py | 3 +-- .../stock/doctype/warehouse/test_warehouse.py | 24 +++++++++++-------- 9 files changed, 27 insertions(+), 30 deletions(-) diff --git a/erpnext/patches/v11_0/merge_land_unit_with_location.py b/erpnext/patches/v11_0/merge_land_unit_with_location.py index 1ea486dfd5d..7845da255a8 100644 --- a/erpnext/patches/v11_0/merge_land_unit_with_location.py +++ b/erpnext/patches/v11_0/merge_land_unit_with_location.py @@ -4,19 +4,18 @@ from __future__ import unicode_literals import frappe -from frappe.model.rename_doc import rename_doc from frappe.model.utils.rename_field import rename_field def execute(): # Rename and reload the Land Unit and Linked Land Unit doctypes if frappe.db.table_exists('Land Unit') and not frappe.db.table_exists('Location'): - rename_doc('DocType', 'Land Unit', 'Location', force=True) + frappe.rename_doc('DocType', 'Land Unit', 'Location', force=True) frappe.reload_doc('assets', 'doctype', 'location') if frappe.db.table_exists('Linked Land Unit') and not frappe.db.table_exists('Linked Location'): - rename_doc('DocType', 'Linked Land Unit', 'Linked Location', force=True) + frappe.rename_doc('DocType', 'Linked Land Unit', 'Linked Location', force=True) frappe.reload_doc('assets', 'doctype', 'linked_location') diff --git a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py index c03ab0b7111..fad0cf7a45e 100644 --- a/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py +++ b/erpnext/patches/v11_0/rename_asset_adjustment_doctype.py @@ -3,9 +3,9 @@ from __future__ import unicode_literals import frappe -from frappe.model.rename_doc import rename_doc + def execute(): if frappe.db.table_exists("Asset Adjustment") and not frappe.db.table_exists("Asset Value Adjustment"): - rename_doc('DocType', 'Asset Adjustment', 'Asset Value Adjustment', force=True) + frappe.rename_doc('DocType', 'Asset Adjustment', 'Asset Value Adjustment', force=True) frappe.reload_doc('assets', 'doctype', 'asset_value_adjustment') \ No newline at end of file diff --git a/erpnext/patches/v11_0/rename_health_insurance.py b/erpnext/patches/v11_0/rename_health_insurance.py index 24d1ddf0314..e605071a297 100644 --- a/erpnext/patches/v11_0/rename_health_insurance.py +++ b/erpnext/patches/v11_0/rename_health_insurance.py @@ -2,9 +2,8 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -from frappe.model.rename_doc import rename_doc import frappe def execute(): - rename_doc('DocType', 'Health Insurance', 'Employee Health Insurance', force=True) + frappe.rename_doc('DocType', 'Health Insurance', 'Employee Health Insurance', force=True) frappe.reload_doc('hr', 'doctype', 'employee_health_insurance') \ No newline at end of file diff --git a/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py b/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py index 8fdac07658f..9705681b33f 100644 --- a/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py +++ b/erpnext/patches/v11_0/rename_healthcare_doctype_and_fields.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals import frappe -from frappe.model.rename_doc import rename_doc from frappe.model.utils.rename_field import rename_field from frappe.modules import scrub, get_doctype_module @@ -37,7 +36,7 @@ doc_rename_map = { def execute(): for dt in doc_rename_map: if frappe.db.exists('DocType', dt): - rename_doc('DocType', dt, doc_rename_map[dt], force=True) + frappe.rename_doc('DocType', dt, doc_rename_map[dt], force=True) for dn in field_rename_map: if frappe.db.exists('DocType', dn): diff --git a/erpnext/patches/v11_0/rename_production_order_to_work_order.py b/erpnext/patches/v11_0/rename_production_order_to_work_order.py index 2c27fbbc9df..2f620f413ba 100644 --- a/erpnext/patches/v11_0/rename_production_order_to_work_order.py +++ b/erpnext/patches/v11_0/rename_production_order_to_work_order.py @@ -2,18 +2,17 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -from frappe.model.rename_doc import rename_doc -from frappe.model.utils.rename_field import rename_field import frappe +from frappe.model.utils.rename_field import rename_field def execute(): - rename_doc('DocType', 'Production Order', 'Work Order', force=True) + frappe.rename_doc('DocType', 'Production Order', 'Work Order', force=True) frappe.reload_doc('manufacturing', 'doctype', 'work_order') - rename_doc('DocType', 'Production Order Item', 'Work Order Item', force=True) + frappe.rename_doc('DocType', 'Production Order Item', 'Work Order Item', force=True) frappe.reload_doc('manufacturing', 'doctype', 'work_order_item') - rename_doc('DocType', 'Production Order Operation', 'Work Order Operation', force=True) + frappe.rename_doc('DocType', 'Production Order Operation', 'Work Order Operation', force=True) frappe.reload_doc('manufacturing', 'doctype', 'work_order_operation') frappe.reload_doc('projects', 'doctype', 'timesheet') diff --git a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py index 52d4621c7b7..c4b3838c71d 100644 --- a/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py +++ b/erpnext/patches/v11_0/rename_supplier_type_to_supplier_group.py @@ -1,6 +1,5 @@ from __future__ import unicode_literals import frappe -from frappe.model.rename_doc import rename_doc from frappe.model.utils.rename_field import rename_field from frappe import _ from frappe.utils.nestedset import rebuild_tree @@ -9,7 +8,7 @@ def execute(): if frappe.db.table_exists("Supplier Group"): frappe.reload_doc('setup', 'doctype', 'supplier_group') elif frappe.db.table_exists("Supplier Type"): - rename_doc("DocType", "Supplier Type", "Supplier Group", force=True) + frappe.rename_doc("DocType", "Supplier Type", "Supplier Group", force=True) frappe.reload_doc('setup', 'doctype', 'supplier_group') frappe.reload_doc("accounts", "doctype", "pricing_rule") frappe.reload_doc("accounts", "doctype", "tax_rule") diff --git a/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py b/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py index 41ac8cff2b4..b9ad622b0ea 100644 --- a/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py +++ b/erpnext/patches/v12_0/rename_pricing_rule_child_doctypes.py @@ -3,7 +3,6 @@ from __future__ import unicode_literals import frappe -from frappe.model.rename_doc import rename_doc doctypes = { 'Price Discount Slab': 'Promotional Scheme Price Discount', @@ -16,6 +15,6 @@ doctypes = { def execute(): for old_doc, new_doc in doctypes.items(): if not frappe.db.table_exists(new_doc) and frappe.db.table_exists(old_doc): - rename_doc('DocType', old_doc, new_doc) + frappe.rename_doc('DocType', old_doc, new_doc) frappe.reload_doc("accounts", "doctype", frappe.scrub(new_doc)) frappe.delete_doc("DocType", old_doc) diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index da53d8d6b15..cbd5e33b146 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -11,7 +11,6 @@ from erpnext.controllers.item_variant import (create_variant, ItemVariantExistsE InvalidItemAttributeValueError, get_variant) from erpnext.stock.doctype.item.item import StockExistsForTemplate, InvalidBarcode from erpnext.stock.doctype.item.item import get_uom_conv_factor -from frappe.model.rename_doc import rename_doc from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.get_item_details import get_item_details @@ -348,7 +347,7 @@ class TestItem(unittest.TestCase): make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC", qty=1, rate=100) - rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True) + frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True) self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1")) diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py index dc39e101ce0..e0483d3ab64 100644 --- a/erpnext/stock/doctype/warehouse/test_warehouse.py +++ b/erpnext/stock/doctype/warehouse/test_warehouse.py @@ -1,18 +1,22 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -from frappe.model.rename_doc import rename_doc -from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry + +import unittest + +import frappe from frappe.utils import cint -from erpnext import set_perpetual_inventory from frappe.test_runner import make_test_records -from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account import erpnext -import frappe -import unittest +from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry +from erpnext import set_perpetual_inventory +from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account + + test_records = frappe.get_test_records('Warehouse') + class TestWarehouse(unittest.TestCase): def setUp(self): if not frappe.get_value('Item', '_Test Item'): @@ -41,7 +45,7 @@ class TestWarehouse(unittest.TestCase): # Rename with abbr if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - _TC"): frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC") - rename_doc("Warehouse", "Test Warehouse for Renaming 1 - _TC", "Test Warehouse for Renaming 2 - _TC") + frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - _TC", "Test Warehouse for Renaming 2 - _TC") self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": "Test Warehouse for Renaming 1 - _TC"})) @@ -50,7 +54,7 @@ class TestWarehouse(unittest.TestCase): if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - _TC"): frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC") - rename_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC", "Test Warehouse for Renaming 3") + frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC", "Test Warehouse for Renaming 3") self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": "Test Warehouse for Renaming 1 - _TC"})) @@ -58,7 +62,7 @@ class TestWarehouse(unittest.TestCase): # Another rename with multiple dashes if frappe.db.exists("Warehouse", "Test - Warehouse - Company - _TC"): frappe.delete_doc("Warehouse", "Test - Warehouse - Company - _TC") - rename_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC", "Test - Warehouse - Company") + frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC", "Test - Warehouse - Company") def test_warehouse_merging(self): set_perpetual_inventory(1) @@ -78,7 +82,7 @@ class TestWarehouse(unittest.TestCase): {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty")) ) - rename_doc("Warehouse", "Test Warehouse for Merging 1 - _TC", + frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - _TC", "Test Warehouse for Merging 2 - _TC", merge=True) self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - _TC")) From c1a1e7d503cf9df43287abe81d3f2d899c6c5ad3 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 19 Dec 2019 20:11:37 +0530 Subject: [PATCH 437/679] style: formatting changes --- erpnext/hub_node/api.py | 4 +- .../js/hub/components/edit_details_dialog.js | 14 +- erpnext/public/js/hub/pages/Item.vue | 181 +++++++++--------- 3 files changed, 101 insertions(+), 98 deletions(-) diff --git a/erpnext/hub_node/api.py b/erpnext/hub_node/api.py index 602d65e81a1..f362539ee7c 100644 --- a/erpnext/hub_node/api.py +++ b/erpnext/hub_node/api.py @@ -116,10 +116,10 @@ def get_valid_items(search_value=''): return valid_items @frappe.whitelist() -def update_item(ref_doctype, ref_doc, data): +def update_item(ref_doc, data): data = json.loads(data) - data.update(dict(doctype=ref_doctype, name=ref_doc)) + data.update(dict(doctype='Hub Item', name=ref_doc)) try: connection = get_hub_connection() connection.update(data) diff --git a/erpnext/public/js/hub/components/edit_details_dialog.js b/erpnext/public/js/hub/components/edit_details_dialog.js index e249ca5d7a4..97c5f83a13a 100644 --- a/erpnext/public/js/hub/components/edit_details_dialog.js +++ b/erpnext/public/js/hub/components/edit_details_dialog.js @@ -1,4 +1,4 @@ -function EditDetailsDialog(primary_action, defaults) { +function edit_details_dialog(params) { let dialog = new frappe.ui.Dialog({ title: __('Update Details'), fields: [ @@ -6,14 +6,14 @@ function EditDetailsDialog(primary_action, defaults) { label: 'Item Name', fieldname: 'item_name', fieldtype: 'Data', - default: defaults.item_name, + default: params.defaults.item_name, reqd: 1 }, { label: 'Hub Category', fieldname: 'hub_category', fieldtype: 'Autocomplete', - default: defaults.hub_category, + default: params.defaults.hub_category, options: [], reqd: 1 }, @@ -21,13 +21,13 @@ function EditDetailsDialog(primary_action, defaults) { label: 'Description', fieldname: 'description', fieldtype: 'Text', - default: defaults.description, + default: params.defaults.description, options: [], reqd: 1 } ], - primary_action_label: primary_action.label || __('Update Details'), - primary_action: primary_action.fn + primary_action_label: params.primary_action.label || __('Update Details'), + primary_action: params.primary_action.fn }); hub.call('get_categories').then(categories => { @@ -38,4 +38,4 @@ function EditDetailsDialog(primary_action, defaults) { return dialog; } -export { EditDetailsDialog }; +export { edit_details_dialog }; diff --git a/erpnext/public/js/hub/pages/Item.vue b/erpnext/public/js/hub/pages/Item.vue index 31cc8d5c8cf..11744781b7a 100644 --- a/erpnext/public/js/hub/pages/Item.vue +++ b/erpnext/public/js/hub/pages/Item.vue @@ -1,10 +1,5 @@