From d683dc250d6f79d6c09cbbe6cc63f12eb5e5a97c Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 18 Apr 2020 00:45:18 +0530 Subject: [PATCH 01/73] Linkedin, Twitter Integration --- .../crm/doctype/linkedin_settings/__init__.py | 0 .../linkedin_settings/linkedin_settings.js | 66 ++++++++ .../linkedin_settings/linkedin_settings.json | 111 +++++++++++++ .../linkedin_settings/linkedin_settings.py | 157 ++++++++++++++++++ .../test_linkedin_settings.py | 10 ++ .../crm/doctype/social_media_post/__init__.py | 0 .../social_media_post/social_media_post.js | 54 ++++++ .../social_media_post/social_media_post.json | 157 ++++++++++++++++++ .../social_media_post/social_media_post.py | 49 ++++++ .../social_media_post_list.js | 10 ++ .../test_social_media_post.py | 10 ++ .../crm/doctype/twitter_settings/__init__.py | 0 .../twitter_settings/test_twitter_settings.py | 10 ++ .../twitter_settings/twitter_settings.js | 52 ++++++ .../twitter_settings/twitter_settings.json | 101 +++++++++++ .../twitter_settings/twitter_settings.py | 90 ++++++++++ erpnext/hooks.py | 3 +- .../doctype/campaign/campaign_dashboard.py | 6 +- requirements.txt | 1 + 19 files changed, 885 insertions(+), 2 deletions(-) create mode 100644 erpnext/crm/doctype/linkedin_settings/__init__.py create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.js create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.json create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.py create mode 100644 erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py create mode 100644 erpnext/crm/doctype/social_media_post/__init__.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.js create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.json create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post_list.js create mode 100644 erpnext/crm/doctype/social_media_post/test_social_media_post.py create mode 100644 erpnext/crm/doctype/twitter_settings/__init__.py create mode 100644 erpnext/crm/doctype/twitter_settings/test_twitter_settings.py create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.js create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.json create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.py diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/crm/doctype/linkedin_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js new file mode 100644 index 0000000000..fd169f8328 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -0,0 +1,66 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('LinkedIn Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + if(frm.doc.session_status=="Active"){ + let d = new Date(frm.doc.modified) + d.setDate(d.getDate()+60); + let dn = new Date() + let days = d.getTime() - dn.getTime(); + days = Math.floor(days/(1000 * 3600 * 24)); + let msg,color; + if(days>0){ + msg = "Your Session will be expire in " + days + " days."; + color = "green"; + } + else{ + msg = "Session is expired. Save doc to login."; + color = "red"; + } + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorization_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json new file mode 100644 index 0000000000..9eacb0011c --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "creation": "2020-01-30 13:36:39.492931", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "column_break_2", + "company_id", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "user_details_section", + "access_token", + "person_urn", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret", + "reqd": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "person_urn", + "fieldtype": "Data", + "hidden": 1, + "label": "Person URN", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "user_details_section", + "fieldtype": "Section Break", + "label": "User Details" + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_id", + "fieldtype": "Data", + "label": "Company ID", + "reqd": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-04-16 23:22:51.966397", + "modified_by": "Administrator", + "module": "CRM", + "name": "LinkedIn 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/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py new file mode 100644 index 0000000000..9326e6d046 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, requests, json +from frappe import _ +from frappe.utils import get_site_url, get_url_to_form, get_link_to_form +from frappe.model.document import Document +from frappe.utils.file_manager import get_file, get_file_path +from six.moves.urllib.parse import urlencode +class LinkedInSettings(Document): + def get_authorization_url(self): + params = urlencode({ + "response_type":"code", + "client_id": self.consumer_key, + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" + }) + + url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) + + return url + + def get_access_token(self, code): + url = "https://www.linkedin.com/oauth/v2/accessToken" + body = { + "grant_type": "authorization_code", + "code": code, + "client_id": self.consumer_key, + "client_secret": self.get_password(fieldname="consumer_secret"), + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + } + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + response = self.http_post(url=url, data=body, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("access_token", response["access_token"]) + + def get_member_profile(self): + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + url = "https://api.linkedin.com/v2/me" + response = requests.get(url=url, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("person_urn", response["id"]) + self.db_set("account_name", response["vanityName"]) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + + def post(self, text, media=None): + if not media: + return self.post_text(text) + else: + media_id = self.upload_image(media) + if media_id: + return self.post_text(text, media_id=media_id) + else: + frappe.log_error("Failed to upload media.","LinkedIn Upload Error") + + + def upload_image(self, media): + media = get_file_path(media) + register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" + + body = { + "registerUploadRequest": { + "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], + "owner": "urn:li:organization:{0}".format(self.company_id), + "serviceRelationships": [{ + "relationshipType": "OWNER", + "identifier": "urn:li:userGeneratedContent" + }] + } + } + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + response = self.http_post(url=register_url, body=body, headers=headers) + if response.status_code == 200: + response = response.json() + asset = response["value"]["asset"] + upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"] + headers['Content-Type']='image/jpeg' + response = self.http_post(upload_url, headers=headers, data=open(media,"rb")) + if response.status_code < 200 and response.status_code > 299: + frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) + return None + return asset + return None + + + def post_text(self, text, media_id=None): + # url = "https://api.linkedin.com/v2/ugcPosts" + url = "https://api.linkedin.com/v2/shares" + headers = { + "X-Restli-Protocol-Version": "2.0.0", + "Authorization": "Bearer {}".format(self.access_token), + "Content-Type": "application/json; charset=UTF-8" + } + body = { + "distribution": { + "linkedInDistributionTarget": {} + }, + "owner":"urn:li:organization:{0}".format(self.company_id), + "subject": "Test Share Subject", + "text": { + "text": text + } + } + if media_id: + body["content"]= { + "contentEntities": [{ + "entity": media_id + }], + "shareMediaCategory": "IMAGE" + } + response = self.http_post(url=url, headers=headers, body=body) + return response + + def http_post(self, url, headers=None, body=None, data=None): + try: + response = requests.post( + url = url, + json = body, + data = data, + headers = headers + ) + if response.status_code not in [201,200]: + raise + except Exception as e: + content = json.loads(response.content) + if response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") + elif response.status_code == 403: + frappe.msgprint(_("You Didn't have permission to access this API")) + frappe.throw(content["message"], title="LinkedIn Error - Access Denied") + else: + frappe.throw(response.reason, title=response.status_code) + return response + +@frappe.whitelist() +def callback(code=None, error=None, error_description=None): + if not error: + linkedin_settings = frappe.get_doc("LinkedIn Settings") + linkedin_settings.get_access_token(code) + linkedin_settings.get_member_profile() + frappe.db.commit() + else: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py new file mode 100644 index 0000000000..9c3ef3f32f --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLinkedInSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/crm/doctype/social_media_post/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js new file mode 100644 index 0000000000..c858885d2b --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -0,0 +1,54 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.ui.form.on('Social Media Post', { + validate: function(frm){ + if(frm.doc.text.length > 280){ + frappe.throw("Length Must be less than 280.") + } + }, + refresh: function(frm){ + if(frm.doc.docstatus === 1){ + if(frm.doc.post_status != "Posted"){ + add_post_btn(frm); + } + else if(frm.doc.post_status == "Posted"){ + frm.set_df_property('sheduled_time', 'read_only', 1); + } + let html = '
'; + if(frm.doc.twitter){ + let color = frm.doc.twitter_post_id ? "green" : "red"; + let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + if(frm.doc.linkedin){ + let color = frm.doc.linkedin_post_id ? "green" : "red"; + let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + html += '
'; + frm.dashboard.set_headline_alert(html); + } + } +}); +var add_post_btn = function(frm){ + frm.add_custom_button(('Post Now'), function(){ + post(frm); + }); +} +var post = function(frm){ + frappe.call({ + method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", + args: { + doctype: frm.doc.doctype, + name: frm.doc.name + }, + callback: function(r) { + frm.reload_doc(); + } + }) + +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json new file mode 100644 index 0000000000..e34f02b8b5 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -0,0 +1,157 @@ +{ + "actions": [], + "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", + "creation": "2020-01-30 11:53:13.872864", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_name", + "sheduled_time", + "post_status", + "column_break_6", + "twitter", + "linkedin", + "twitter_post_id", + "linkedin_post_id", + "content", + "text", + "column_break_14", + "tweet_preview", + "linkedin_section", + "linkedin_post", + "attachments_section", + "image", + "amended_from" + ], + "fields": [ + { + "fieldname": "text", + "fieldtype": "Small Text", + "label": "Tweet", + "reqd": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "default": "0", + "fieldname": "twitter", + "fieldtype": "Check", + "label": "Twitter" + }, + { + "default": "0", + "fieldname": "linkedin", + "fieldtype": "Check", + "label": "LinkedIn" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Social Media Post", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "sheduled_time", + "fieldtype": "Datetime", + "label": "Scheduled Time" + }, + { + "fieldname": "content", + "fieldtype": "Section Break", + "label": "Twitter" + }, + { + "allow_on_submit": 1, + "fieldname": "post_status", + "fieldtype": "Select", + "label": "Post Status", + "options": "\nScheduled\nPosted\nError", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "twitter_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Twitter Post Id", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "linkedin_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "LinkedIn Post Id", + "read_only": 1 + }, + { + "fieldname": "campaign_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign", + "options": "Campaign" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "label": "Share On" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "tweet_preview", + "fieldtype": "HTML" + }, + { + "collapsible": 1, + "fieldname": "linkedin_section", + "fieldtype": "Section Break", + "label": "LinkedIn" + }, + { + "collapsible": 1, + "fieldname": "attachments_section", + "fieldtype": "Section Break", + "label": "Attachments" + }, + { + "fieldname": "linkedin_post", + "fieldtype": "Text", + "label": "Post" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-09 22:35:23.821991", + "modified_by": "Administrator", + "module": "CRM", + "name": "Social Media Post", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py new file mode 100644 index 0000000000..8b3387ebf2 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 +from frappe import _ + +class SocialMediaPost(Document): + def submit(self): + if self.sheduled_time: + self.post_status = "Scheduled" + super(SocialMediaPost, self).submit() + + def post(self): + try: + if self.twitter and not self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + twitter_post = twitter.post(self.text, self.image) + self.db_set("twitter_post_id", twitter_post.id) + if self.linkedin and not self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + linkedin_post = linkedin.post(self.text, self.image) + self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) + self.db_set("post_status", "Posted") + + except Exception as e: + self.db_set("post_status", "Error") + title = _("Error while POSTING {0}").format(self.name) + traceback = frappe.get_traceback() + frappe.log_error(message=traceback , title=title) + +def process_scheduled_social_media_posts(): + import datetime + posts = frappe.get_list("Social Media Post", filters={"status": "Scheduled"}, fields= ["name", "sheduled_time"]) + start = frappe.utils.now_datetime() + end = start + datetime.timedelta(minutes=59) + for post in posts: + post_time = frappe.utils.get_datetime(post.scheduled_time) + if post_time > start and post_time <= end: + post = frappe.get_doc('Social Media Post',post['name']) + post.post() + +@frappe.whitelist() +def publish(doctype, name): + sm_post = frappe.get_doc(doctype, name) + sm_post.post() + frappe.db.commit() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js new file mode 100644 index 0000000000..c60b91a9a0 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Social Media Post'] = { + add_fields: ["status","post_status"], + get_indicator: function(doc) { + return [__(doc.post_status), { + "Scheduled": "orange", + "Posted": "green", + "Error": "red" + }[doc.post_status]]; + } +} diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py new file mode 100644 index 0000000000..ec81ee5871 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/test_social_media_post.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSocialMediaPost(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py new file mode 100644 index 0000000000..3f999c1af4 --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTwitterSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js new file mode 100644 index 0000000000..359196bcae --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -0,0 +1,52 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Twitter Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Active"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + else if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorize_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json new file mode 100644 index 0000000000..fbdfac907e --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "creation": "2020-01-30 10:29:08.562108", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "profile_pic", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "oauth_token", + "oauth_secret", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret Key", + "reqd": 1 + }, + { + "fieldname": "oauth_token", + "fieldtype": "Data", + "hidden": 1, + "label": "OAuth Token", + "read_only": 1 + }, + { + "fieldname": "oauth_secret", + "fieldtype": "Password", + "hidden": 1, + "label": "OAuth Token Secret", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "profile_pic", + "fieldtype": "Attach Image", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + } + ], + "image_field": "profile_pic", + "issingle": 1, + "links": [], + "modified": "2020-04-08 23:56:20.621246", + "modified_by": "Administrator", + "module": "CRM", + "name": "Twitter 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/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py new file mode 100644 index 0000000000..f069a600dd --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, os, tweepy, json +from frappe import _ +from frappe.model.document import Document +from frappe.utils.file_manager import get_file_path +from frappe.utils import get_url_to_form, get_link_to_form +from tweepy.error import TweepError + +class TwitterSettings(Document): + def get_authorize_url(self): + callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) + try: + redirect_url = auth.get_authorization_url() + return redirect_url + except: + frappe.msgprint(_("Error! Failed to get request token.")) + frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) + + + def get_access_token(self, oauth_token, oauth_verifier): + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + auth.request_token = { + 'oauth_token' : oauth_token, + 'oauth_token_secret' : oauth_verifier + } + try: + auth.get_access_token(oauth_verifier) + self.db_set("oauth_token", auth.access_token) + self.db_set("oauth_secret", auth.access_token_secret) + api = self.get_api() + user = api.me() + self.db_set("account_name", user._json["screen_name"]) + profile_pic = (user._json["profile_image_url"]).replace("_normal","") + self.db_set("profile_pic", profile_pic) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") + except TweepError as e: + frappe.msgprint(_("Error! Failed to get access token.")) + frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) + + def get_api(self): + # authentication of consumer key and secret + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + # authentication of access token and secret + auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) + + return tweepy.API(auth) + + def post(self, text, media=None): + if not media: + return self.send_tweet(text) + + if media: + media_id = self.upload_image(media) + return self.send_tweet(text, media_id) + + def upload_image(self, media): + media = get_file_path(media) + api = self.get_api() + media = api.media_upload(media) + return media.media_id + + def send_tweet(self, text, media_id=None): + api = self.get_api() + try: + if media_id: + response = api.update_status(status = text, media_ids = [media_id]) + else: + response = api.update_status(status = text) + return response + + except TweepError as e: + content = json.loads(e.response.content) + content = content["errors"][0] + if e.response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) + +@frappe.whitelist() +def callback(oauth_token, oauth_verifier): + twitter_settings = frappe.get_single("Twitter Settings") + twitter_settings.get_access_token(oauth_token,oauth_verifier) + frappe.db.commit() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2f07e15f1e..deaf12edd6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -270,7 +270,8 @@ auto_cancel_exempted_doctypes= [ scheduler_events = { "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", - "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" + "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", + "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts" ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py index a9d8eca38c..3cef560c32 100644 --- a/erpnext/selling/doctype/campaign/campaign_dashboard.py +++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py @@ -8,6 +8,10 @@ def get_data(): { 'label': _('Email Campaigns'), 'items': ['Email Campaign'] + }, + { + 'label': _('Social Media Campaigns'), + 'items': ['Social Media Post'] } - ], + ] } diff --git a/requirements.txt b/requirements.txt index c277545fab..9da537e493 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ PyGithub==1.44.1 python-stdnum==1.12 Unidecode==1.1.1 WooCommerce==2.1.1 +tweepy==3.8.0 \ No newline at end of file From 22318b2eb68cfbfc5b3f18cf906a07a07a014873 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 18 Apr 2020 00:45:18 +0530 Subject: [PATCH 02/73] Linkedin, Twitter Integration --- .../crm/doctype/linkedin_settings/__init__.py | 0 .../linkedin_settings/linkedin_settings.js | 66 ++++++++ .../linkedin_settings/linkedin_settings.json | 111 +++++++++++++ .../linkedin_settings/linkedin_settings.py | 157 ++++++++++++++++++ .../test_linkedin_settings.py | 10 ++ .../crm/doctype/social_media_post/__init__.py | 0 .../social_media_post/social_media_post.js | 54 ++++++ .../social_media_post/social_media_post.json | 157 ++++++++++++++++++ .../social_media_post/social_media_post.py | 49 ++++++ .../social_media_post_list.js | 10 ++ .../test_social_media_post.py | 10 ++ .../crm/doctype/twitter_settings/__init__.py | 0 .../twitter_settings/test_twitter_settings.py | 10 ++ .../twitter_settings/twitter_settings.js | 52 ++++++ .../twitter_settings/twitter_settings.json | 101 +++++++++++ .../twitter_settings/twitter_settings.py | 90 ++++++++++ erpnext/hooks.py | 3 +- .../doctype/campaign/campaign_dashboard.py | 6 +- requirements.txt | 1 + 19 files changed, 885 insertions(+), 2 deletions(-) create mode 100644 erpnext/crm/doctype/linkedin_settings/__init__.py create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.js create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.json create mode 100644 erpnext/crm/doctype/linkedin_settings/linkedin_settings.py create mode 100644 erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py create mode 100644 erpnext/crm/doctype/social_media_post/__init__.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.js create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.json create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post.py create mode 100644 erpnext/crm/doctype/social_media_post/social_media_post_list.js create mode 100644 erpnext/crm/doctype/social_media_post/test_social_media_post.py create mode 100644 erpnext/crm/doctype/twitter_settings/__init__.py create mode 100644 erpnext/crm/doctype/twitter_settings/test_twitter_settings.py create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.js create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.json create mode 100644 erpnext/crm/doctype/twitter_settings/twitter_settings.py diff --git a/erpnext/crm/doctype/linkedin_settings/__init__.py b/erpnext/crm/doctype/linkedin_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js new file mode 100644 index 0000000000..fd169f8328 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -0,0 +1,66 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('LinkedIn Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + if(frm.doc.session_status=="Active"){ + let d = new Date(frm.doc.modified) + d.setDate(d.getDate()+60); + let dn = new Date() + let days = d.getTime() - dn.getTime(); + days = Math.floor(days/(1000 * 3600 * 24)); + let msg,color; + if(days>0){ + msg = "Your Session will be expire in " + days + " days."; + color = "green"; + } + else{ + msg = "Session is expired. Save doc to login."; + color = "red"; + } + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorization_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json new file mode 100644 index 0000000000..9eacb0011c --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.json @@ -0,0 +1,111 @@ +{ + "actions": [], + "creation": "2020-01-30 13:36:39.492931", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "column_break_2", + "company_id", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "user_details_section", + "access_token", + "person_urn", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret", + "reqd": 1 + }, + { + "fieldname": "access_token", + "fieldtype": "Data", + "hidden": 1, + "label": "Access Token", + "read_only": 1 + }, + { + "fieldname": "person_urn", + "fieldtype": "Data", + "hidden": 1, + "label": "Person URN", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "user_details_section", + "fieldtype": "Section Break", + "label": "User Details" + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "company_id", + "fieldtype": "Data", + "label": "Company ID", + "reqd": 1 + } + ], + "issingle": 1, + "links": [], + "modified": "2020-04-16 23:22:51.966397", + "modified_by": "Administrator", + "module": "CRM", + "name": "LinkedIn 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/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py new file mode 100644 index 0000000000..9326e6d046 --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -0,0 +1,157 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, requests, json +from frappe import _ +from frappe.utils import get_site_url, get_url_to_form, get_link_to_form +from frappe.model.document import Document +from frappe.utils.file_manager import get_file, get_file_path +from six.moves.urllib.parse import urlencode +class LinkedInSettings(Document): + def get_authorization_url(self): + params = urlencode({ + "response_type":"code", + "client_id": self.consumer_key, + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + "scope": "r_emailaddress w_organization_social r_basicprofile r_liteprofile r_organization_social rw_organization_admin w_member_social" + }) + + url = "https://www.linkedin.com/oauth/v2/authorization?{}".format(params) + + return url + + def get_access_token(self, code): + url = "https://www.linkedin.com/oauth/v2/accessToken" + body = { + "grant_type": "authorization_code", + "code": code, + "client_id": self.consumer_key, + "client_secret": self.get_password(fieldname="consumer_secret"), + "redirect_uri": get_site_url(frappe.local.site) + "/?cmd=erpnext.crm.doctype.linkedin_settings.linkedin_settings.callback", + } + headers = { + "Content-Type": "application/x-www-form-urlencoded" + } + + response = self.http_post(url=url, data=body, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("access_token", response["access_token"]) + + def get_member_profile(self): + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + url = "https://api.linkedin.com/v2/me" + response = requests.get(url=url, headers=headers) + response = frappe.parse_json(response.content.decode()) + self.db_set("person_urn", response["id"]) + self.db_set("account_name", response["vanityName"]) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") + + def post(self, text, media=None): + if not media: + return self.post_text(text) + else: + media_id = self.upload_image(media) + if media_id: + return self.post_text(text, media_id=media_id) + else: + frappe.log_error("Failed to upload media.","LinkedIn Upload Error") + + + def upload_image(self, media): + media = get_file_path(media) + register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" + + body = { + "registerUploadRequest": { + "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], + "owner": "urn:li:organization:{0}".format(self.company_id), + "serviceRelationships": [{ + "relationshipType": "OWNER", + "identifier": "urn:li:userGeneratedContent" + }] + } + } + headers = { + "Authorization": "Bearer {}".format(self.access_token) + } + response = self.http_post(url=register_url, body=body, headers=headers) + if response.status_code == 200: + response = response.json() + asset = response["value"]["asset"] + upload_url = response["value"]["uploadMechanism"]["com.linkedin.digitalmedia.uploading.MediaUploadHttpRequest"]["uploadUrl"] + headers['Content-Type']='image/jpeg' + response = self.http_post(upload_url, headers=headers, data=open(media,"rb")) + if response.status_code < 200 and response.status_code > 299: + frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) + return None + return asset + return None + + + def post_text(self, text, media_id=None): + # url = "https://api.linkedin.com/v2/ugcPosts" + url = "https://api.linkedin.com/v2/shares" + headers = { + "X-Restli-Protocol-Version": "2.0.0", + "Authorization": "Bearer {}".format(self.access_token), + "Content-Type": "application/json; charset=UTF-8" + } + body = { + "distribution": { + "linkedInDistributionTarget": {} + }, + "owner":"urn:li:organization:{0}".format(self.company_id), + "subject": "Test Share Subject", + "text": { + "text": text + } + } + if media_id: + body["content"]= { + "contentEntities": [{ + "entity": media_id + }], + "shareMediaCategory": "IMAGE" + } + response = self.http_post(url=url, headers=headers, body=body) + return response + + def http_post(self, url, headers=None, body=None, data=None): + try: + response = requests.post( + url = url, + json = body, + data = data, + headers = headers + ) + if response.status_code not in [201,200]: + raise + except Exception as e: + content = json.loads(response.content) + if response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") + elif response.status_code == 403: + frappe.msgprint(_("You Didn't have permission to access this API")) + frappe.throw(content["message"], title="LinkedIn Error - Access Denied") + else: + frappe.throw(response.reason, title=response.status_code) + return response + +@frappe.whitelist() +def callback(code=None, error=None, error_description=None): + if not error: + linkedin_settings = frappe.get_doc("LinkedIn Settings") + linkedin_settings.get_access_token(code) + linkedin_settings.get_member_profile() + frappe.db.commit() + else: + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") diff --git a/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py new file mode 100644 index 0000000000..9c3ef3f32f --- /dev/null +++ b/erpnext/crm/doctype/linkedin_settings/test_linkedin_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLinkedInSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/social_media_post/__init__.py b/erpnext/crm/doctype/social_media_post/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js new file mode 100644 index 0000000000..c858885d2b --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -0,0 +1,54 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +frappe.ui.form.on('Social Media Post', { + validate: function(frm){ + if(frm.doc.text.length > 280){ + frappe.throw("Length Must be less than 280.") + } + }, + refresh: function(frm){ + if(frm.doc.docstatus === 1){ + if(frm.doc.post_status != "Posted"){ + add_post_btn(frm); + } + else if(frm.doc.post_status == "Posted"){ + frm.set_df_property('sheduled_time', 'read_only', 1); + } + let html = '
'; + if(frm.doc.twitter){ + let color = frm.doc.twitter_post_id ? "green" : "red"; + let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + if(frm.doc.linkedin){ + let color = frm.doc.linkedin_post_id ? "green" : "red"; + let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; + html += '
' + + ' ' + + '
' ; + } + html += '
'; + frm.dashboard.set_headline_alert(html); + } + } +}); +var add_post_btn = function(frm){ + frm.add_custom_button(('Post Now'), function(){ + post(frm); + }); +} +var post = function(frm){ + frappe.call({ + method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", + args: { + doctype: frm.doc.doctype, + name: frm.doc.name + }, + callback: function(r) { + frm.reload_doc(); + } + }) + +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json new file mode 100644 index 0000000000..e34f02b8b5 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -0,0 +1,157 @@ +{ + "actions": [], + "autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}", + "creation": "2020-01-30 11:53:13.872864", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "campaign_name", + "sheduled_time", + "post_status", + "column_break_6", + "twitter", + "linkedin", + "twitter_post_id", + "linkedin_post_id", + "content", + "text", + "column_break_14", + "tweet_preview", + "linkedin_section", + "linkedin_post", + "attachments_section", + "image", + "amended_from" + ], + "fields": [ + { + "fieldname": "text", + "fieldtype": "Small Text", + "label": "Tweet", + "reqd": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "default": "0", + "fieldname": "twitter", + "fieldtype": "Check", + "label": "Twitter" + }, + { + "default": "0", + "fieldname": "linkedin", + "fieldtype": "Check", + "label": "LinkedIn" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Social Media Post", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "sheduled_time", + "fieldtype": "Datetime", + "label": "Scheduled Time" + }, + { + "fieldname": "content", + "fieldtype": "Section Break", + "label": "Twitter" + }, + { + "allow_on_submit": 1, + "fieldname": "post_status", + "fieldtype": "Select", + "label": "Post Status", + "options": "\nScheduled\nPosted\nError", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "twitter_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Twitter Post Id", + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "linkedin_post_id", + "fieldtype": "Data", + "hidden": 1, + "label": "LinkedIn Post Id", + "read_only": 1 + }, + { + "fieldname": "campaign_name", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Campaign", + "options": "Campaign" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break", + "label": "Share On" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "tweet_preview", + "fieldtype": "HTML" + }, + { + "collapsible": 1, + "fieldname": "linkedin_section", + "fieldtype": "Section Break", + "label": "LinkedIn" + }, + { + "collapsible": 1, + "fieldname": "attachments_section", + "fieldtype": "Section Break", + "label": "Attachments" + }, + { + "fieldname": "linkedin_post", + "fieldtype": "Text", + "label": "Post" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-09 22:35:23.821991", + "modified_by": "Administrator", + "module": "CRM", + "name": "Social Media Post", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py new file mode 100644 index 0000000000..8b3387ebf2 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 +from frappe import _ + +class SocialMediaPost(Document): + def submit(self): + if self.sheduled_time: + self.post_status = "Scheduled" + super(SocialMediaPost, self).submit() + + def post(self): + try: + if self.twitter and not self.twitter_post_id: + twitter = frappe.get_doc("Twitter Settings") + twitter_post = twitter.post(self.text, self.image) + self.db_set("twitter_post_id", twitter_post.id) + if self.linkedin and not self.linkedin_post_id: + linkedin = frappe.get_doc("LinkedIn Settings") + linkedin_post = linkedin.post(self.text, self.image) + self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) + self.db_set("post_status", "Posted") + + except Exception as e: + self.db_set("post_status", "Error") + title = _("Error while POSTING {0}").format(self.name) + traceback = frappe.get_traceback() + frappe.log_error(message=traceback , title=title) + +def process_scheduled_social_media_posts(): + import datetime + posts = frappe.get_list("Social Media Post", filters={"status": "Scheduled"}, fields= ["name", "sheduled_time"]) + start = frappe.utils.now_datetime() + end = start + datetime.timedelta(minutes=59) + for post in posts: + post_time = frappe.utils.get_datetime(post.scheduled_time) + if post_time > start and post_time <= end: + post = frappe.get_doc('Social Media Post',post['name']) + post.post() + +@frappe.whitelist() +def publish(doctype, name): + sm_post = frappe.get_doc(doctype, name) + sm_post.post() + frappe.db.commit() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post_list.js b/erpnext/crm/doctype/social_media_post/social_media_post_list.js new file mode 100644 index 0000000000..c60b91a9a0 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/social_media_post_list.js @@ -0,0 +1,10 @@ +frappe.listview_settings['Social Media Post'] = { + add_fields: ["status","post_status"], + get_indicator: function(doc) { + return [__(doc.post_status), { + "Scheduled": "orange", + "Posted": "green", + "Error": "red" + }[doc.post_status]]; + } +} diff --git a/erpnext/crm/doctype/social_media_post/test_social_media_post.py b/erpnext/crm/doctype/social_media_post/test_social_media_post.py new file mode 100644 index 0000000000..ec81ee5871 --- /dev/null +++ b/erpnext/crm/doctype/social_media_post/test_social_media_post.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestSocialMediaPost(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/__init__.py b/erpnext/crm/doctype/twitter_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py new file mode 100644 index 0000000000..3f999c1af4 --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/test_twitter_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTwitterSettings(unittest.TestCase): + pass diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js new file mode 100644 index 0000000000..359196bcae --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -0,0 +1,52 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Twitter Settings', { + onload: function(frm){ + if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.confirm( + 'Session not valid, Do you want to login?', + function(){ + frm.trigger("login"); + }, + function(){ + window.close(); + } + ) + } + }, + refresh: function(frm){ + if(frm.doc.session_status=="Active"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + else if(frm.doc.session_status=="Expired"){ + frm.dashboard.set_headline_alert( + '
' + + '
' + + ' ' + + '
' + + '
' + ); + } + }, + login: function(frm){ + if(frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.call({ + doc: frm.doc, + method: "get_authorize_url", + callback : function(r) { + window.location.href = r.message; + } + }); + } + }, + after_save: function(frm){ + frm.trigger("login"); + } +}); diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json new file mode 100644 index 0000000000..fbdfac907e --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json @@ -0,0 +1,101 @@ +{ + "actions": [], + "creation": "2020-01-30 10:29:08.562108", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_name", + "profile_pic", + "oauth_details", + "consumer_key", + "column_break_5", + "consumer_secret", + "oauth_token", + "oauth_secret", + "session_status" + ], + "fields": [ + { + "fieldname": "account_name", + "fieldtype": "Data", + "label": "Account Name", + "read_only": 1 + }, + { + "fieldname": "oauth_details", + "fieldtype": "Section Break", + "label": "OAuth Credentials" + }, + { + "fieldname": "consumer_key", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Consumer Key", + "reqd": 1 + }, + { + "fieldname": "consumer_secret", + "fieldtype": "Password", + "in_list_view": 1, + "label": "Consumer Secret Key", + "reqd": 1 + }, + { + "fieldname": "oauth_token", + "fieldtype": "Data", + "hidden": 1, + "label": "OAuth Token", + "read_only": 1 + }, + { + "fieldname": "oauth_secret", + "fieldtype": "Password", + "hidden": 1, + "label": "OAuth Token Secret", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "profile_pic", + "fieldtype": "Attach Image", + "hidden": 1, + "read_only": 1 + }, + { + "fieldname": "session_status", + "fieldtype": "Select", + "hidden": 1, + "label": "Session Status", + "options": "Expired\nActive", + "read_only": 1 + } + ], + "image_field": "profile_pic", + "issingle": 1, + "links": [], + "modified": "2020-04-08 23:56:20.621246", + "modified_by": "Administrator", + "module": "CRM", + "name": "Twitter 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/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py new file mode 100644 index 0000000000..f069a600dd --- /dev/null +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe, os, tweepy, json +from frappe import _ +from frappe.model.document import Document +from frappe.utils.file_manager import get_file_path +from frappe.utils import get_url_to_form, get_link_to_form +from tweepy.error import TweepError + +class TwitterSettings(Document): + def get_authorize_url(self): + callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) + try: + redirect_url = auth.get_authorization_url() + return redirect_url + except: + frappe.msgprint(_("Error! Failed to get request token.")) + frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key"))) + + + def get_access_token(self, oauth_token, oauth_verifier): + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + auth.request_token = { + 'oauth_token' : oauth_token, + 'oauth_token_secret' : oauth_verifier + } + try: + auth.get_access_token(oauth_verifier) + self.db_set("oauth_token", auth.access_token) + self.db_set("oauth_secret", auth.access_token_secret) + api = self.get_api() + user = api.me() + self.db_set("account_name", user._json["screen_name"]) + profile_pic = (user._json["profile_image_url"]).replace("_normal","") + self.db_set("profile_pic", profile_pic) + self.db_set("session_status", "Active") + frappe.local.response["type"] = "redirect" + frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") + except TweepError as e: + frappe.msgprint(_("Error! Failed to get access token.")) + frappe.throw(_('Invalid Consumer Key or Consumer Secret Key')) + + def get_api(self): + # authentication of consumer key and secret + auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret")) + # authentication of access token and secret + auth.set_access_token(self.oauth_token, self.get_password(fieldname="oauth_secret")) + + return tweepy.API(auth) + + def post(self, text, media=None): + if not media: + return self.send_tweet(text) + + if media: + media_id = self.upload_image(media) + return self.send_tweet(text, media_id) + + def upload_image(self, media): + media = get_file_path(media) + api = self.get_api() + media = api.media_upload(media) + return media.media_id + + def send_tweet(self, text, media_id=None): + api = self.get_api() + try: + if media_id: + response = api.update_status(status = text, media_ids = [media_id]) + else: + response = api.update_status(status = text) + return response + + except TweepError as e: + content = json.loads(e.response.content) + content = content["errors"][0] + if e.response.status_code == 401: + self.db_set("session_status", "Expired") + frappe.db.commit() + frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason)) + +@frappe.whitelist() +def callback(oauth_token, oauth_verifier): + twitter_settings = frappe.get_single("Twitter Settings") + twitter_settings.get_access_token(oauth_token,oauth_verifier) + frappe.db.commit() diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 2f07e15f1e..deaf12edd6 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -270,7 +270,8 @@ auto_cancel_exempted_doctypes= [ scheduler_events = { "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", - "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" + "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", + "erpnext.crm.doctype.social_media_post.social_media_post.process_scheduled_social_media_posts" ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py index a9d8eca38c..3cef560c32 100644 --- a/erpnext/selling/doctype/campaign/campaign_dashboard.py +++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py @@ -8,6 +8,10 @@ def get_data(): { 'label': _('Email Campaigns'), 'items': ['Email Campaign'] + }, + { + 'label': _('Social Media Campaigns'), + 'items': ['Social Media Post'] } - ], + ] } diff --git a/requirements.txt b/requirements.txt index c277545fab..9da537e493 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,4 @@ PyGithub==1.44.1 python-stdnum==1.12 Unidecode==1.1.1 WooCommerce==2.1.1 +tweepy==3.8.0 \ No newline at end of file From c12a40c923585bc6660afc73e330da63ebbae437 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 18 Apr 2020 01:50:18 +0530 Subject: [PATCH 03/73] Linkedin, Twitter Integration --- .../doctype/social_media_post/social_media_post.js | 2 +- .../social_media_post/social_media_post.json | 14 +++++++++++--- .../doctype/social_media_post/social_media_post.py | 2 +- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index c858885d2b..690e27c79b 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Social Media Post', { validate: function(frm){ if(frm.doc.text.length > 280){ - frappe.throw("Length Must be less than 280.") + frappe.throw(__("Length Must be less than 280.")) } }, refresh: function(frm){ diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.json b/erpnext/crm/doctype/social_media_post/social_media_post.json index e34f02b8b5..f8c23a7e46 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.json +++ b/erpnext/crm/doctype/social_media_post/social_media_post.json @@ -20,6 +20,7 @@ "tweet_preview", "linkedin_section", "linkedin_post", + "column_break_15", "attachments_section", "image", "amended_from" @@ -29,7 +30,7 @@ "fieldname": "text", "fieldtype": "Small Text", "label": "Tweet", - "reqd": 1 + "mandatory_depends_on": "eval:doc.twitter ==1" }, { "fieldname": "image", @@ -64,6 +65,7 @@ "label": "Scheduled Time" }, { + "depends_on": "eval:doc.twitter ==1", "fieldname": "content", "fieldtype": "Section Break", "label": "Twitter" @@ -114,6 +116,7 @@ }, { "collapsible": 1, + "depends_on": "eval:doc.linkedin==1", "fieldname": "linkedin_section", "fieldtype": "Section Break", "label": "LinkedIn" @@ -127,12 +130,17 @@ { "fieldname": "linkedin_post", "fieldtype": "Text", - "label": "Post" + "label": "Post", + "mandatory_depends_on": "eval:doc.linkedin ==1" + }, + { + "fieldname": "column_break_15", + "fieldtype": "Column Break" } ], "is_submittable": 1, "links": [], - "modified": "2020-04-09 22:35:23.821991", + "modified": "2020-04-18 01:28:35.995490", "modified_by": "Administrator", "module": "CRM", "name": "Social Media Post", diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index 8b3387ebf2..672486deac 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -21,7 +21,7 @@ class SocialMediaPost(Document): self.db_set("twitter_post_id", twitter_post.id) if self.linkedin and not self.linkedin_post_id: linkedin = frappe.get_doc("LinkedIn Settings") - linkedin_post = linkedin.post(self.text, self.image) + linkedin_post = linkedin.post(self.linkedin_post, self.image) self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) self.db_set("post_status", "Posted") From 5f7785444877ae26884115ce97cea0cad2273fb2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 21 Apr 2020 14:28:08 +0530 Subject: [PATCH 04/73] Revert "fix : Create Pick List Fos Sales Order List (#20915)" (#21352) This reverts commit 27c6f694359bf211bd2e457af6b2ef59d48ff5a9. --- .../doctype/sales_order/sales_order_list.js | 35 ------------------- 1 file changed, 35 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order_list.js b/erpnext/selling/doctype/sales_order/sales_order_list.js index 288d8847ce..26d96d59f2 100644 --- a/erpnext/selling/doctype/sales_order/sales_order_list.js +++ b/erpnext/selling/doctype/sales_order/sales_order_list.js @@ -47,41 +47,6 @@ frappe.listview_settings['Sales Order'] = { listview.page.add_menu_item(__("Re-open"), function() { listview.call_for_selected_items(method, {"status": "Submitted"}); }); - }, - onload: function(doclist) { - const action = () => { - const selected_docs = doclist.get_checked_items(); - const docnames = doclist.get_checked_items(true); - - if (selected_docs.length > 0) { - for (let doc of selected_docs) { - if (!doc.docstatus) { - frappe.throw(__("Cannot create a Pick List from Draft documents.")); - } - }; - - frappe.new_doc("Pick List") - .then(() => { - frappe.call({ - type: "POST", - method: "frappe.model.mapper.map_docs", - args: { - "method": "erpnext.selling.doctype.sales_order.sales_order.create_pick_list", - "source_names": docnames, - "target_doc": cur_frm.doc - }, - callback: function (r) { - if (!r.exc) { - frappe.model.sync(r.message); - cur_frm.dirty(); - cur_frm.refresh(); - } - } - }); - }) - }; - }; - doclist.page.add_actions_menu_item(__('Create Pick List'), action, false); } }; From 690855d3a19cc4cb045e778a1fc9d195ad626bc1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 22 Apr 2020 02:45:42 +0530 Subject: [PATCH 05/73] fix: unsupported operand type issue in pricing rule --- erpnext/accounts/doctype/pricing_rule/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 100bb1d3e3..b358f56671 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -330,9 +330,9 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): if pr_doc.mixed_conditions: amt = args.get('qty') * args.get("price_list_rate") if args.get("item_code") != row.get("item_code"): - amt = row.get('qty') * (row.get("price_list_rate") or args.get("rate")) + amt = flt(row.get('qty')) * flt(row.get("price_list_rate") or args.get("rate")) - sum_qty += row.get("stock_qty") or args.get("stock_qty") or args.get("qty") + sum_qty += flt(row.get("stock_qty")) or flt(args.get("stock_qty")) or flt(args.get("qty")) sum_amt += amt if pr_doc.is_cumulative: From 8fb863da07af21711d21dcc2d726895762b9a38d Mon Sep 17 00:00:00 2001 From: Chinmay Pai Date: Wed, 22 Apr 2020 11:23:14 +0530 Subject: [PATCH 06/73] chore: remove oldfieldtype from purchase invoice item (#21365) fixes issue where framework expects data field type to be validated with options either set to "Email" or "Phone". removing oldfieldtype works for now, should be fixed inside framework Signed-off-by: Chinmay D. Pai --- .../purchase_invoice_item/purchase_invoice_item.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index c0a47d52c5..b4d1a73f97 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -563,9 +563,6 @@ "fieldname": "item_group", "fieldtype": "Data", "label": "Item Group", - "oldfieldname": "item_group", - "oldfieldtype": "Link", - "options": "Item Group", "print_hide": 1, "read_only": 1 }, @@ -777,7 +774,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-07 18:34:35.104178", + "modified": "2020-04-22 10:37:35.103176", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", @@ -785,4 +782,4 @@ "permissions": [], "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} From 471cf95300093c36f41563ebaac1c10131ca6528 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 22 Apr 2020 11:46:31 +0530 Subject: [PATCH 07/73] Update purchase_invoice_item.json --- .../purchase_invoice_item/purchase_invoice_item.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index b4d1a73f97..52a5be0984 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -550,21 +550,21 @@ }, { "fieldname": "brand", - "fieldtype": "Data", + "fieldtype": "Link", "hidden": 1, "label": "Brand", - "oldfieldname": "brand", - "oldfieldtype": "Data", - "print_hide": 1 + "print_hide": 1, + "options": "Brand" }, { "fetch_from": "item_code.item_group", "fetch_if_empty": 1, "fieldname": "item_group", - "fieldtype": "Data", + "fieldtype": "Link", "label": "Item Group", "print_hide": 1, - "read_only": 1 + "read_only": 1, + "options": "Item Group" }, { "description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges", From 9c494d3e72d95c6b95f379a0d00545a225f35c33 Mon Sep 17 00:00:00 2001 From: Saqib Date: Wed, 22 Apr 2020 11:48:19 +0530 Subject: [PATCH 08/73] fix: asset maintenance fixes (#21277) * fix: asset maintenance fixes * fix: tests --- .../asset_maintenance/asset_maintenance.js | 39 ------------------- .../asset_maintenance/asset_maintenance.py | 7 ++-- .../test_asset_maintenance.py | 6 ++- 3 files changed, 7 insertions(+), 45 deletions(-) diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js index 3c135d466c..001fc26ffe 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js @@ -24,26 +24,6 @@ frappe.ui.form.on('Asset Maintenance', { return indicator; } ); - - frm.set_query('select_serial_no', function(doc){ - return { - asset: frm.doc.asset_name - } - }) - }, - - select_serial_no: (frm) => { - let serial_nos = frm.doc.serial_no || frm.doc.select_serial_no; - if (serial_nos) { - serial_nos = serial_nos.split('\n'); - serial_nos.push(frm.doc.select_serial_no); - - const unique_sn = serial_nos.filter(function(elem, index, self) { - return index === self.indexOf(elem); - }); - - frm.set_value("serial_no", unique_sn.join('\n')); - } }, refresh: (frm) => { @@ -93,25 +73,6 @@ frappe.ui.form.on('Asset Maintenance Task', { }, end_date: (frm, cdt, cdn) => { get_next_due_date(frm, cdt, cdn); - }, - assign_to: (frm, cdt, cdn) => { - var d = locals[cdt][cdn]; - if (frm.doc.__islocal) { - frappe.model.set_value(cdt, cdn, "assign_to", ""); - frappe.model.set_value(cdt, cdn, "assign_to_name", ""); - frappe.throw(__("Please save before assigning task.")); - } - if (d.assign_to) { - return frappe.call({ - method: 'erpnext.assets.doctype.asset_maintenance.asset_maintenance.assign_tasks', - args: { - asset_maintenance_name: frm.doc.name, - assign_to_member: d.assign_to, - maintenance_task: d.maintenance_task, - next_due_date: d.next_due_date - } - }); - } } }); diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py index ecb55dde9a..3f046113a0 100644 --- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.py @@ -16,12 +16,11 @@ class AssetMaintenance(Document): throw(_("Start date should be less than end date for task {0}").format(task.maintenance_task)) if getdate(task.next_due_date) < getdate(nowdate()): task.maintenance_status = "Overdue" + if not task.assign_to and self.docstatus == 0: + throw(_("Row #{}: Please asign task to a member.").format(task.idx)) def on_update(self): for task in self.get('asset_maintenance_tasks'): - if not task.assign_to: - task.db_set("assign_to", self.maintenance_manager) - task.db_set("assign_to_name", self.maintenance_manager_name) assign_tasks(self.name, task.assign_to, task.maintenance_task, task.next_due_date) self.sync_maintenance_tasks() @@ -108,7 +107,7 @@ def update_maintenance_log(asset_maintenance, item_code, item_name, task): @frappe.whitelist() def get_team_members(doctype, txt, searchfield, start, page_len, filters): - return frappe.db.get_values('Maintenance Team Member', {'parent':filters.get("maintenance_team")}) + return frappe.db.get_values('Maintenance Team Member', { 'parent': filters.get("maintenance_team") }) @frappe.whitelist() def get_maintenance_log(asset_name): diff --git a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py index 6c2fd67a9a..392fbdd2af 100644 --- a/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py +++ b/erpnext/assets/doctype/asset_maintenance/test_asset_maintenance.py @@ -125,13 +125,15 @@ def get_maintenance_tasks(): "start_date": nowdate(), "periodicity": "Monthly", "maintenance_type": "Preventive Maintenance", - "maintenance_status": "Planned" + "maintenance_status": "Planned", + "assign_to": "marcus@abc.com" }, {"maintenance_task": "Check Gears", "start_date": nowdate(), "periodicity": "Yearly", "maintenance_type": "Calibration", - "maintenance_status": "Planned" + "maintenance_status": "Planned", + "assign_to": "thalia@abc.com" } ] From 55baccd63aeefafc54120e7bcbfe01730b36176f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 22 Apr 2020 11:50:06 +0530 Subject: [PATCH 09/73] fix: specify column width --- .../customer_acquisition_and_loyalty.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py index 28dd056407..aa57665a81 100644 --- a/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py +++ b/erpnext/selling/report/customer_acquisition_and_loyalty/customer_acquisition_and_loyalty.py @@ -53,10 +53,11 @@ def execute(filters=None): new[1], repeat[1], new[1] + repeat[1]]) return [ - _("Year"), _("Month"), - _("New Customers") + ":Int", - _("Repeat Customers") + ":Int", - _("Total") + ":Int", + _("Year") + "::100", + _("Month") + "::100", + _("New Customers") + ":Int:100", + _("Repeat Customers") + ":Int:100", + _("Total") + ":Int:100", _("New Customer Revenue") + ":Currency:150", _("Repeat Customer Revenue") + ":Currency:150", _("Total Revenue") + ":Currency:150" From eaa956b994bff442d48469ec99459ad43aa5b7a1 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 22 Apr 2020 13:07:12 +0530 Subject: [PATCH 10/73] feat(Healthcare): Rehabilitation Module (#21216) * feat: added rehab sub-module doctypes * feat: rehab module * feat: prescribe procedures in Patient Encounter * feat: create Therapy Plan on Encounter submission * feat: manage item for Therapy Type * feat: book appointments, get prescribed therapies for Therapy Sessions * feat: invoice Therapy Sessions * feat: plan completion progress bar and exercise countsindicators * feat: Motor Assessment Scale * feat: add editable card view for exercise steps * fix: add rehab in healthcare desk page * fix: card deletion not working when child table is hidden * feat: automatically fetch therapies according to Body Part * fix: added tests for Therapy Type and Plan * fix: add exercises according to body parts in Therapy Type * fix: label for Exercise Instructions * fix: exercise cards css * feat: add dashboard for Therapy Plan * feat: Patient Assessment Template and Patient Assessment * feat: add title fields in Therapy Plan and Session * fix: remove Motor Assessment Scale * fix: fetch assessment description * feat: create Patient Assessment from Therapy Session * fix: anti pattern code * fix: update desk page * fix: exercise card rendering * fix(test): filter out disabled Items in test_mapper * fix: get stock uom from Stock Settings for Therapy Type Item creation * fix: multiline SQL query * fix: permissions for DocTypes Co-authored-by: Nabin Hait --- erpnext/config/healthcare.py | 36 +++ erpnext/controllers/tests/test_mapper.py | 2 +- .../desk_page/healthcare/healthcare.json | 67 +++--- .../healthcare/doctype/body_part/__init__.py | 0 .../healthcare/doctype/body_part/body_part.js | 8 + .../doctype/body_part/body_part.json | 45 ++++ .../healthcare/doctype/body_part/body_part.py | 10 + .../doctype/body_part/test_body_part.py | 10 + .../doctype/body_part_link/__init__.py | 0 .../body_part_link/body_part_link.json | 32 +++ .../doctype/body_part_link/body_part_link.py | 10 + .../healthcare/doctype/exercise/__init__.py | 0 .../healthcare/doctype/exercise/exercise.json | 61 +++++ .../healthcare/doctype/exercise/exercise.py | 10 + .../exercise_difficulty_level/__init__.py | 0 .../exercise_difficulty_level.js | 8 + .../exercise_difficulty_level.json | 45 ++++ .../exercise_difficulty_level.py | 10 + .../test_exercise_difficulty_level.py | 10 + .../doctype/exercise_type/__init__.py | 0 .../doctype/exercise_type/exercise_type.js | 180 +++++++++++++++ .../doctype/exercise_type/exercise_type.json | 144 ++++++++++++ .../doctype/exercise_type/exercise_type.py | 15 ++ .../exercise_type/test_exercise_type.py | 10 + .../doctype/exercise_type_step/__init__.py | 0 .../exercise_type_step.json | 44 ++++ .../exercise_type_step/exercise_type_step.py | 10 + .../patient_appointment.js | 81 +++++++ .../patient_appointment.json | 27 ++- .../patient_appointment.py | 35 ++- .../doctype/patient_assessment/__init__.py | 0 .../patient_assessment/patient_assessment.js | 86 +++++++ .../patient_assessment.json | 172 ++++++++++++++ .../patient_assessment/patient_assessment.py | 36 +++ .../test_patient_assessment.py | 10 + .../patient_assessment_detail/__init__.py | 0 .../patient_assessment_detail.json | 32 +++ .../patient_assessment_detail.py | 10 + .../patient_assessment_parameter/__init__.py | 0 .../patient_assessment_parameter.js | 8 + .../patient_assessment_parameter.json | 45 ++++ .../patient_assessment_parameter.py | 10 + .../test_patient_assessment_parameter.py | 10 + .../patient_assessment_sheet/__init__.py | 0 .../patient_assessment_sheet.json | 57 +++++ .../patient_assessment_sheet.py | 10 + .../patient_assessment_template/__init__.py | 0 .../patient_assessment_template.js | 8 + .../patient_assessment_template.json | 109 +++++++++ .../patient_assessment_template.py | 10 + .../test_patient_assessment_template.py | 10 + .../patient_encounter/patient_encounter.js | 4 + .../patient_encounter/patient_encounter.json | 29 ++- .../patient_encounter/patient_encounter.py | 19 ++ .../doctype/therapy_plan/__init__.py | 0 .../doctype/therapy_plan/test_therapy_plan.py | 57 +++++ .../doctype/therapy_plan/therapy_plan.js | 90 ++++++++ .../doctype/therapy_plan/therapy_plan.json | 151 ++++++++++++ .../doctype/therapy_plan/therapy_plan.py | 42 ++++ .../therapy_plan/therapy_plan_dashboard.py | 13 ++ .../doctype/therapy_plan/therapy_plan_list.js | 11 + .../doctype/therapy_plan_detail/__init__.py | 0 .../therapy_plan_detail.json | 48 ++++ .../therapy_plan_detail.py | 10 + .../doctype/therapy_session/__init__.py | 0 .../therapy_session/test_therapy_session.py | 10 + .../therapy_session/therapy_session.js | 60 +++++ .../therapy_session/therapy_session.json | 218 ++++++++++++++++++ .../therapy_session/therapy_session.py | 55 +++++ .../therapy_session_dashboard.py | 13 ++ .../doctype/therapy_type/__init__.py | 0 .../doctype/therapy_type/test_therapy_type.py | 50 ++++ .../doctype/therapy_type/therapy_type.js | 93 ++++++++ .../doctype/therapy_type/therapy_type.json | 211 +++++++++++++++++ .../doctype/therapy_type/therapy_type.py | 122 ++++++++++ erpnext/healthcare/utils.py | 22 +- .../production_plan/production_plan.js | 2 +- erpnext/public/build.json | 48 ++-- erpnext/public/css/erpnext.css | 36 +++ erpnext/public/less/erpnext.less | 46 ++++ 80 files changed, 2928 insertions(+), 65 deletions(-) create mode 100644 erpnext/healthcare/doctype/body_part/__init__.py create mode 100644 erpnext/healthcare/doctype/body_part/body_part.js create mode 100644 erpnext/healthcare/doctype/body_part/body_part.json create mode 100644 erpnext/healthcare/doctype/body_part/body_part.py create mode 100644 erpnext/healthcare/doctype/body_part/test_body_part.py create mode 100644 erpnext/healthcare/doctype/body_part_link/__init__.py create mode 100644 erpnext/healthcare/doctype/body_part_link/body_part_link.json create mode 100644 erpnext/healthcare/doctype/body_part_link/body_part_link.py create mode 100644 erpnext/healthcare/doctype/exercise/__init__.py create mode 100644 erpnext/healthcare/doctype/exercise/exercise.json create mode 100644 erpnext/healthcare/doctype/exercise/exercise.py create mode 100644 erpnext/healthcare/doctype/exercise_difficulty_level/__init__.py create mode 100644 erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.js create mode 100644 erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.json create mode 100644 erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.py create mode 100644 erpnext/healthcare/doctype/exercise_difficulty_level/test_exercise_difficulty_level.py create mode 100644 erpnext/healthcare/doctype/exercise_type/__init__.py create mode 100644 erpnext/healthcare/doctype/exercise_type/exercise_type.js create mode 100644 erpnext/healthcare/doctype/exercise_type/exercise_type.json create mode 100644 erpnext/healthcare/doctype/exercise_type/exercise_type.py create mode 100644 erpnext/healthcare/doctype/exercise_type/test_exercise_type.py create mode 100644 erpnext/healthcare/doctype/exercise_type_step/__init__.py create mode 100644 erpnext/healthcare/doctype/exercise_type_step/exercise_type_step.json create mode 100644 erpnext/healthcare/doctype/exercise_type_step/exercise_type_step.py create mode 100644 erpnext/healthcare/doctype/patient_assessment/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_assessment/patient_assessment.js create mode 100644 erpnext/healthcare/doctype/patient_assessment/patient_assessment.json create mode 100644 erpnext/healthcare/doctype/patient_assessment/patient_assessment.py create mode 100644 erpnext/healthcare/doctype/patient_assessment/test_patient_assessment.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_detail/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.json create mode 100644 erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_parameter/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.js create mode 100644 erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.json create mode 100644 erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_parameter/test_patient_assessment_parameter.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_sheet/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.json create mode 100644 erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_template/__init__.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.js create mode 100644 erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.json create mode 100644 erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.py create mode 100644 erpnext/healthcare/doctype/patient_assessment_template/test_patient_assessment_template.py create mode 100644 erpnext/healthcare/doctype/therapy_plan/__init__.py create mode 100644 erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py create mode 100644 erpnext/healthcare/doctype/therapy_plan/therapy_plan.js create mode 100644 erpnext/healthcare/doctype/therapy_plan/therapy_plan.json create mode 100644 erpnext/healthcare/doctype/therapy_plan/therapy_plan.py create mode 100644 erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py create mode 100644 erpnext/healthcare/doctype/therapy_plan/therapy_plan_list.js create mode 100644 erpnext/healthcare/doctype/therapy_plan_detail/__init__.py create mode 100644 erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json create mode 100644 erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.py create mode 100644 erpnext/healthcare/doctype/therapy_session/__init__.py create mode 100644 erpnext/healthcare/doctype/therapy_session/test_therapy_session.py create mode 100644 erpnext/healthcare/doctype/therapy_session/therapy_session.js create mode 100644 erpnext/healthcare/doctype/therapy_session/therapy_session.json create mode 100644 erpnext/healthcare/doctype/therapy_session/therapy_session.py create mode 100644 erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py create mode 100644 erpnext/healthcare/doctype/therapy_type/__init__.py create mode 100644 erpnext/healthcare/doctype/therapy_type/test_therapy_type.py create mode 100644 erpnext/healthcare/doctype/therapy_type/therapy_type.js create mode 100644 erpnext/healthcare/doctype/therapy_type/therapy_type.json create mode 100644 erpnext/healthcare/doctype/therapy_type/therapy_type.py diff --git a/erpnext/config/healthcare.py b/erpnext/config/healthcare.py index 2b461273ad..da24d11538 100644 --- a/erpnext/config/healthcare.py +++ b/erpnext/config/healthcare.py @@ -214,5 +214,41 @@ def get_data(): "label": _("Lab Test Report") } ] + }, + { + "label": _("Rehabilitation"), + "icon": "icon-cog", + "items": [ + { + "type": "doctype", + "name": "Exercise Type", + "label": _("Exercise Type") + }, + { + "type": "doctype", + "name": "Exercise Difficulty Level", + "label": _("Exercise Difficulty Level") + }, + { + "type": "doctype", + "name": "Therapy Type", + "label": _("Therapy Type") + }, + { + "type": "doctype", + "name": "Therapy Plan", + "label": _("Therapy Plan") + }, + { + "type": "doctype", + "name": "Therapy Session", + "label": _("Therapy Session") + }, + { + "type": "doctype", + "name": "Motor Assessment Scale", + "label": _("Motor Assessment Scale") + } + ] } ] diff --git a/erpnext/controllers/tests/test_mapper.py b/erpnext/controllers/tests/test_mapper.py index d02308d8f2..8839e002a4 100644 --- a/erpnext/controllers/tests/test_mapper.py +++ b/erpnext/controllers/tests/test_mapper.py @@ -13,7 +13,7 @@ class TestMapper(unittest.TestCase): '''Test mapping of multiple source docs on a single target doc''' make_test_records("Item") - items = frappe.get_all("Item", fields = ["name", "item_code"], filters = {'is_sales_item': 1, 'has_variants': 0}) + items = frappe.get_all("Item", fields = ["name", "item_code"], filters = {'is_sales_item': 1, 'has_variants': 0, 'disabled': 0}) customers = frappe.get_all("Customer") if items and customers: # Make source docs (quotations) and a target doc (sales order) diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json index 54798ba08f..24c6d6fc37 100644 --- a/erpnext/healthcare/desk_page/healthcare/healthcare.json +++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json @@ -1,48 +1,53 @@ { "cards": [ { - "icon": "", - "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient\",\n\t\t\"label\": \"Patient\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Practitioner\",\n\t\t\"label\":\"Healthcare Practitioner\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Practitioner Schedule\",\n\t\t\"label\": \"Practitioner Schedule\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Medical Department\",\n\t\t\"label\": \"Medical Department\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Service Unit Type\",\n\t\t\"label\": \"Healthcare Service Unit Type\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Service Unit\",\n\t\t\"label\": \"Healthcare Service Unit\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Medical Code Standard\",\n\t\t\"label\": \"Medical Code Standard\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Medical Code\",\n\t\t\"label\": \"Medical Code\"\n\t}\n]", - "title": "Masters" + "hidden": 0, + "label": "Masters", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient\",\n\t\t\"label\": \"Patient\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Practitioner\",\n\t\t\"label\":\"Healthcare Practitioner\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Practitioner Schedule\",\n\t\t\"label\": \"Practitioner Schedule\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Medical Department\",\n\t\t\"label\": \"Medical Department\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Service Unit Type\",\n\t\t\"label\": \"Healthcare Service Unit Type\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Service Unit\",\n\t\t\"label\": \"Healthcare Service Unit\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Medical Code Standard\",\n\t\t\"label\": \"Medical Code Standard\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Medical Code\",\n\t\t\"label\": \"Medical Code\"\n\t}\n]" }, { - "icon": "", - "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Appointment Type\",\n\t\t\"label\": \"Appointment Type\"\n },\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Clinical Procedure Template\",\n\t\t\"label\": \"Clinical Procedure Template\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Prescription Dosage\",\n\t\t\"label\": \"Prescription Dosage\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Prescription Duration\",\n\t\t\"label\": \"Prescription Duration\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Antibiotic\",\n\t\t\"label\": \"Antibiotic\"\n\t}\n]", - "title": "Consultation Setup" + "hidden": 0, + "label": "Consultation Setup", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Appointment Type\",\n\t\t\"label\": \"Appointment Type\"\n },\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Clinical Procedure Template\",\n\t\t\"label\": \"Clinical Procedure Template\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Prescription Dosage\",\n\t\t\"label\": \"Prescription Dosage\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Prescription Duration\",\n\t\t\"label\": \"Prescription Duration\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Antibiotic\",\n\t\t\"label\": \"Antibiotic\"\n\t}\n]" }, { - "icon": "icon-star", - "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Appointment\",\n\t\t\"label\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Clinical Procedure\",\n\t\t\"label\": \"Clinical Procedure\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Encounter\",\n\t\t\"label\": \"Patient Encounter\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Vital Signs\",\n\t\t\"label\": \"Vital Signs\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Complaint\",\n\t\t\"label\": \"Complaint\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Diagnosis\",\n\t\t\"label\": \"Diagnosis\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Fee Validity\",\n\t\t\"label\": \"Fee Validity\"\n\t}\n]", - "title": "Consultation" + "hidden": 0, + "label": "Consultation", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Appointment\",\n\t\t\"label\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Clinical Procedure\",\n\t\t\"label\": \"Clinical Procedure\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Encounter\",\n\t\t\"label\": \"Patient Encounter\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Vital Signs\",\n\t\t\"label\": \"Vital Signs\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Complaint\",\n\t\t\"label\": \"Complaint\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Diagnosis\",\n\t\t\"label\": \"Diagnosis\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Fee Validity\",\n\t\t\"label\": \"Fee Validity\"\n\t}\n]" }, { - "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Settings\",\n\t\t\"label\": \"Healthcare Settings\",\n\t\t\"onboard\": 1\n\t}\n]", - "title": "Settings" + "hidden": 0, + "label": "Settings", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Healthcare Settings\",\n\t\t\"label\": \"Healthcare Settings\",\n\t\t\"onboard\": 1\n\t}\n]" }, { - "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test Template\",\n\t\t\"label\": \"Lab Test Template\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test Sample\",\n\t\t\"label\": \"Lab Test Sample\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test UOM\",\n\t\t\"label\": \"Lab Test UOM\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Sensitivity\",\n\t\t\"label\": \"Sensitivity\"\n\t}\n]", - "title": "Laboratory Setup" + "hidden": 0, + "label": "Laboratory Setup", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test Template\",\n\t\t\"label\": \"Lab Test Template\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test Sample\",\n\t\t\"label\": \"Lab Test Sample\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test UOM\",\n\t\t\"label\": \"Lab Test UOM\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Sensitivity\",\n\t\t\"label\": \"Sensitivity\"\n\t}\n]" }, { - "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test\",\n\t\t\"label\": \"Lab Test\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Sample Collection\",\n\t\t\"label\": \"Sample Collection\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Dosage Form\",\n\t\t\"label\": \"Dosage Form\"\n\t}\n]", - "title": "Laboratory" + "hidden": 0, + "label": "Laboratory", + "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Lab Test\",\n\t\t\"label\": \"Lab Test\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Sample Collection\",\n\t\t\"label\": \"Sample Collection\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Dosage Form\",\n\t\t\"label\": \"Dosage Form\"\n\t}\n]" }, { - "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]", - "title": "Records and History" + "hidden": 0, + "label": "Rehabilitation and Physiotherapy", + "links": "[\n {\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Exercise Type\",\n\t\t\"label\": \"Exercise Type\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Therapy Type\",\n\t\t\"label\": \"Therapy Type\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Therapy Plan\",\n\t\t\"label\": \"Therapy Plan\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Therapy Session\",\n\t\t\"label\": \"Therapy Session\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Assessment Template\",\n\t\t\"label\": \"Patient Assessment Template\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Assessment\",\n\t\t\"label\": \"Patient Assessment\"\n\t}\n]" }, { - "links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t}\n]", - "title": "Reports" + "hidden": 0, + "label": "Records and History", + "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]" + }, + { + "hidden": 0, + "label": "Reports", + "links": "[\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Patient Appointment Analytics\",\n\t\t\"doctype\": \"Patient Appointment\"\n\t},\n\t{\n\t\t\"type\": \"report\",\n\t\t\"is_query_report\": true,\n\t\t\"name\": \"Lab Test Report\",\n\t\t\"doctype\": \"Lab Test\",\n\t\t\"label\": \"Lab Test Report\"\n\t}\n]" } ], "category": "Domains", - "charts": [ - { - "chart_name": "Patient Appointments", - "label": "Patient Appointments" - } - ], + "charts": [], "charts_label": "", "creation": "2020-03-02 17:23:17.919682", "developer_mode_only": 0, @@ -53,7 +58,7 @@ "idx": 0, "is_standard": 1, "label": "Healthcare", - "modified": "2020-03-26 16:10:44.629795", + "modified": "2020-04-20 11:42:43.889576", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", @@ -64,32 +69,32 @@ "shortcuts": [ { "format": "{} Open", - "is_query_report": 0, + "label": "Patient Appointment", "link_to": "Patient Appointment", "stats_filter": "{\n \"status\": \"Open\"\n}", "type": "DocType" }, { "format": "{} Active", - "is_query_report": 0, + "label": "Patient", "link_to": "Patient", "stats_filter": "{\n \"status\": \"Active\"\n}", "type": "DocType" }, { "format": "{} Vacant", - "is_query_report": 0, + "label": "Healthcare Service Unit", "link_to": "Healthcare Service Unit", "stats_filter": "{\n \"occupancy_status\": \"Vacant\",\n \"is_group\": 0\n}", "type": "DocType" }, { - "is_query_report": 0, + "label": "Healthcare Practitioner", "link_to": "Healthcare Practitioner", "type": "DocType" }, { - "is_query_report": 0, + "label": "Patient History", "link_to": "patient_history", "type": "Page" } diff --git a/erpnext/healthcare/doctype/body_part/__init__.py b/erpnext/healthcare/doctype/body_part/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/body_part/body_part.js b/erpnext/healthcare/doctype/body_part/body_part.js new file mode 100644 index 0000000000..d2f9d09937 --- /dev/null +++ b/erpnext/healthcare/doctype/body_part/body_part.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Body Part', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/healthcare/doctype/body_part/body_part.json b/erpnext/healthcare/doctype/body_part/body_part.json new file mode 100644 index 0000000000..6e3d1d4ce3 --- /dev/null +++ b/erpnext/healthcare/doctype/body_part/body_part.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "autoname": "field:body_part", + "creation": "2020-04-10 12:21:55.036402", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "body_part" + ], + "fields": [ + { + "fieldname": "body_part", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Body Part", + "reqd": 1, + "unique": 1 + } + ], + "links": [], + "modified": "2020-04-10 12:26:44.087985", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Body Part", + "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/healthcare/doctype/body_part/body_part.py b/erpnext/healthcare/doctype/body_part/body_part.py new file mode 100644 index 0000000000..300493a52b --- /dev/null +++ b/erpnext/healthcare/doctype/body_part/body_part.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 BodyPart(Document): + pass diff --git a/erpnext/healthcare/doctype/body_part/test_body_part.py b/erpnext/healthcare/doctype/body_part/test_body_part.py new file mode 100644 index 0000000000..cb3a61150e --- /dev/null +++ b/erpnext/healthcare/doctype/body_part/test_body_part.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestBodyPart(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/body_part_link/__init__.py b/erpnext/healthcare/doctype/body_part_link/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/body_part_link/body_part_link.json b/erpnext/healthcare/doctype/body_part_link/body_part_link.json new file mode 100644 index 0000000000..400b7c6fe8 --- /dev/null +++ b/erpnext/healthcare/doctype/body_part_link/body_part_link.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-04-10 12:23:15.259816", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "body_part" + ], + "fields": [ + { + "fieldname": "body_part", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Body Part", + "options": "Body Part", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-10 12:25:23.101749", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Body Part Link", + "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/healthcare/doctype/body_part_link/body_part_link.py b/erpnext/healthcare/doctype/body_part_link/body_part_link.py new file mode 100644 index 0000000000..0371529769 --- /dev/null +++ b/erpnext/healthcare/doctype/body_part_link/body_part_link.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 BodyPartLink(Document): + pass diff --git a/erpnext/healthcare/doctype/exercise/__init__.py b/erpnext/healthcare/doctype/exercise/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/exercise/exercise.json b/erpnext/healthcare/doctype/exercise/exercise.json new file mode 100644 index 0000000000..2486a5d53a --- /dev/null +++ b/erpnext/healthcare/doctype/exercise/exercise.json @@ -0,0 +1,61 @@ +{ + "actions": [], + "creation": "2020-03-11 09:25:00.968572", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "exercise_type", + "difficulty_level", + "counts_target", + "counts_completed", + "assistance_level" + ], + "fields": [ + { + "fieldname": "exercise_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Exercise Type", + "options": "Exercise Type", + "reqd": 1 + }, + { + "fetch_from": "exercise_type.difficulty_level", + "fieldname": "difficulty_level", + "fieldtype": "Link", + "label": "Difficulty Level", + "options": "Exercise Difficulty Level" + }, + { + "fieldname": "counts_target", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Counts Target" + }, + { + "depends_on": "eval:doc.parenttype==\"Therapy\";", + "fieldname": "counts_completed", + "fieldtype": "Int", + "label": "Counts Completed" + }, + { + "fieldname": "assistance_level", + "fieldtype": "Select", + "label": "Assistance Level", + "options": "\nPassive\nActive Assist\nActive" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-10 13:41:06.662351", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Exercise", + "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/healthcare/doctype/exercise/exercise.py b/erpnext/healthcare/doctype/exercise/exercise.py new file mode 100644 index 0000000000..efd89997fe --- /dev/null +++ b/erpnext/healthcare/doctype/exercise/exercise.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 Exercise(Document): + pass diff --git a/erpnext/healthcare/doctype/exercise_difficulty_level/__init__.py b/erpnext/healthcare/doctype/exercise_difficulty_level/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.js b/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.js new file mode 100644 index 0000000000..ff51c34f3f --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Exercise Difficulty Level', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.json b/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.json new file mode 100644 index 0000000000..a6aed75e7a --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "autoname": "field:difficulty_level", + "creation": "2020-03-29 21:12:55.835941", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "difficulty_level" + ], + "fields": [ + { + "fieldname": "difficulty_level", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Difficulty Level", + "reqd": 1, + "unique": 1 + } + ], + "links": [], + "modified": "2020-03-31 23:14:33.554066", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Exercise Difficulty Level", + "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/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.py b/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.py new file mode 100644 index 0000000000..17e97b8960 --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_difficulty_level/exercise_difficulty_level.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 ExerciseDifficultyLevel(Document): + pass diff --git a/erpnext/healthcare/doctype/exercise_difficulty_level/test_exercise_difficulty_level.py b/erpnext/healthcare/doctype/exercise_difficulty_level/test_exercise_difficulty_level.py new file mode 100644 index 0000000000..80ef3a7de8 --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_difficulty_level/test_exercise_difficulty_level.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestExerciseDifficultyLevel(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/exercise_type/__init__.py b/erpnext/healthcare/doctype/exercise_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/exercise_type/exercise_type.js b/erpnext/healthcare/doctype/exercise_type/exercise_type.js new file mode 100644 index 0000000000..f450c9bccb --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.js @@ -0,0 +1,180 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Exercise Type', { + refresh: function(frm) { + let wrapper = frm.fields_dict.steps_html.wrapper; + + frm.ExerciseEditor = new erpnext.ExerciseEditor(frm, wrapper); + } +}); + +erpnext.ExerciseEditor = Class.extend({ + init: function(frm, wrapper) { + this.wrapper = wrapper; + this.frm = frm; + this.make(frm, wrapper); + }, + + make: function(frm, wrapper) { + $(this.wrapper).empty(); + + this.exercise_toolbar = $('

\ + ').appendTo(this.wrapper); + + this.exercise_cards = $('

').appendTo(this.wrapper); + + let me = this; + + this.exercise_toolbar.find(".btn-add") + .html(__('Add')) + .on("click", function() { + me.show_add_card_dialog(frm); + }); + + if (frm.doc.steps_table.length > 0) { + this.make_cards(frm); + this.make_buttons(frm); + } + }, + + make_cards: function(frm) { + var me = this; + $(me.exercise_cards).empty(); + this.row = $('
').appendTo(me.exercise_cards); + + $.each(frm.doc.steps_table, function(i, step) { + $(repl(` +
+
+
+ ... +

%(title)s

+

%(description)s

+
+ +
+
`, {image_src: step.image, title: step.title, description: step.description, col_id: "col-"+i, card_id: "card-"+i, id: i})).appendTo(me.row); + }); + }, + + make_buttons: function(frm) { + let me = this; + $('.btn-edit').on('click', function() { + let id = $(this).attr('data-id'); + me.show_edit_card_dialog(frm, id); + }); + + $('.btn-del').on('click', function() { + let id = $(this).attr('data-id'); + $('#card-'+id).addClass("zoomOutDelete"); + + setTimeout(() => { + // not using grid_rows[id].remove because + // grid_rows is not defined when the table is hidden + frm.doc.steps_table.pop(id); + frm.refresh_field('steps_table'); + $('#col-'+id).remove(); + }, 300); + }); + }, + + show_add_card_dialog: function(frm) { + let me = this; + let d = new frappe.ui.Dialog({ + title: __('Add Exercise Step'), + fields: [ + { + "label": "Title", + "fieldname": "title", + "fieldtype": "Data", + "reqd": 1 + }, + { + "label": "Attach Image", + "fieldname": "image", + "fieldtype": "Attach Image" + }, + { + "label": "Step Description", + "fieldname": "step_description", + "fieldtype": "Long Text" + } + ], + primary_action: function() { + let data = d.get_values(); + let i = frm.doc.steps_table.length; + $(repl(` +
+
+
+ ... +

%(title)s

+

%(description)s

+
+ +
+
`, {image_src: data.image, title: data.title, description: data.step_description, col_id: "col-"+i, card_id: "card-"+i, id: i})).appendTo(me.row); + let step = frappe.model.add_child(frm.doc, 'Exercise Type Step', 'steps_table'); + step.title = data.title; + step.image = data.image; + step.description = data.step_description; + me.make_buttons(frm); + frm.refresh_field('steps_table'); + d.hide(); + }, + primary_action_label: __('Add') + }); + d.show(); + }, + + show_edit_card_dialog: function(frm, id) { + let new_dialog = new frappe.ui.Dialog({ + title: __("Edit Exercise Step"), + fields: [ + { + "label": "Title", + "fieldname": "title", + "fieldtype": "Data", + "reqd": 1 + }, + { + "label": "Attach Image", + "fieldname": "image", + "fieldtype": "Attach Image" + }, + { + "label": "Step Description", + "fieldname": "step_description", + "fieldtype": "Long Text" + } + ], + primary_action: () => { + let data = new_dialog.get_values(); + $('#card-'+id).find('.card-title').html(data.title); + $('#card-'+id).find('img').attr('src', data.image); + $('#card-'+id).find('.card-text').html(data.step_description); + + frm.doc.steps_table[id].title = data.title; + frm.doc.steps_table[id].image = data.image; + frm.doc.steps_table[id].description = data.step_description; + refresh_field('steps_table'); + new_dialog.hide(); + }, + primary_action_label: __("Save"), + }); + + new_dialog.set_values({ + title: frm.doc.steps_table[id].title, + image: frm.doc.steps_table[id].image, + step_description: frm.doc.steps_table[id].description + }); + new_dialog.show(); + } +}); diff --git a/erpnext/healthcare/doctype/exercise_type/exercise_type.json b/erpnext/healthcare/doctype/exercise_type/exercise_type.json new file mode 100644 index 0000000000..0db9c6e796 --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.json @@ -0,0 +1,144 @@ +{ + "actions": [], + "creation": "2020-03-29 21:37:03.366344", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "exercise_name", + "body_parts", + "column_break_3", + "difficulty_level", + "section_break_5", + "description", + "section_break_7", + "exercise_steps", + "column_break_9", + "exercise_video", + "section_break_11", + "steps_html", + "section_break_13", + "steps_table" + ], + "fields": [ + { + "fieldname": "exercise_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Exercise Name", + "reqd": 1 + }, + { + "fieldname": "difficulty_level", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Difficulty Level", + "options": "Exercise Difficulty Level" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, + { + "fieldname": "description", + "fieldtype": "Long Text", + "label": "Description" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "exercise_steps", + "fieldtype": "Attach", + "label": "Exercise Instructions" + }, + { + "fieldname": "exercise_video", + "fieldtype": "Link", + "label": "Exercise Video", + "options": "Video" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "steps_html", + "fieldtype": "HTML", + "label": "Steps" + }, + { + "fieldname": "steps_table", + "fieldtype": "Table", + "hidden": 1, + "label": "Steps Table", + "options": "Exercise Type Step" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break", + "label": "Exercise Steps" + }, + { + "fieldname": "section_break_13", + "fieldtype": "Section Break" + }, + { + "fieldname": "body_parts", + "fieldtype": "Table MultiSelect", + "label": "Body Parts", + "options": "Body Part Link" + } + ], + "links": [], + "modified": "2020-04-21 13:05:36.555060", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Exercise Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "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/healthcare/doctype/exercise_type/exercise_type.py b/erpnext/healthcare/doctype/exercise_type/exercise_type.py new file mode 100644 index 0000000000..fb635c8578 --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 ExerciseType(Document): + def autoname(self): + if self.difficulty_level: + self.name = ' - '.join(filter(None, [self.exercise_name, self.difficulty_level])) + else: + self.name = self.exercise_name + diff --git a/erpnext/healthcare/doctype/exercise_type/test_exercise_type.py b/erpnext/healthcare/doctype/exercise_type/test_exercise_type.py new file mode 100644 index 0000000000..bf217e893a --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_type/test_exercise_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestExerciseType(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/exercise_type_step/__init__.py b/erpnext/healthcare/doctype/exercise_type_step/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/exercise_type_step/exercise_type_step.json b/erpnext/healthcare/doctype/exercise_type_step/exercise_type_step.json new file mode 100644 index 0000000000..b37ff007cb --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_type_step/exercise_type_step.json @@ -0,0 +1,44 @@ +{ + "actions": [], + "creation": "2020-03-31 23:01:18.761967", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "image", + "description" + ], + "fields": [ + { + "fieldname": "title", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Title", + "reqd": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "label": "Image" + }, + { + "fieldname": "description", + "fieldtype": "Long Text", + "in_list_view": 1, + "label": "Description" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-02 20:39:34.258512", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Exercise Type Step", + "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/healthcare/doctype/exercise_type_step/exercise_type_step.py b/erpnext/healthcare/doctype/exercise_type_step/exercise_type_step.py new file mode 100644 index 0000000000..13d7e5732f --- /dev/null +++ b/erpnext/healthcare/doctype/exercise_type_step/exercise_type_step.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 ExerciseTypeStep(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index efa6b249b3..fa589347ff 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -102,6 +102,13 @@ frappe.ui.form.on('Patient Appointment', { frm: frm, }); }, __('Create')); + } else if (frm.doc.therapy_type) { + frm.add_custom_button(__('Therapy Session'),function(){ + frappe.model.open_mapped_doc({ + method: 'erpnext.healthcare.doctype.therapy_session.therapy_session.create_therapy_session', + frm: frm, + }) + }, 'Create'); } else { frm.add_custom_button(__('Patient Encounter'), function() { frappe.model.open_mapped_doc({ @@ -123,6 +130,16 @@ frappe.ui.form.on('Patient Appointment', { } }, + therapy_type: function(frm) { + if (frm.doc.therapy_type) { + frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => { + if (r.default_duration) { + frm.set_value('duration', r.default_duration) + } + }); + } + }, + get_procedure_from_encounter: function(frm) { get_prescribed_procedure(frm); }, @@ -148,6 +165,26 @@ frappe.ui.form.on('Patient Appointment', { } } }); + }, + + get_prescribed_therapies: function(frm) { + if (frm.doc.patient) { + frappe.call({ + method: "erpnext.healthcare.doctype.patient_appointment.patient_appointment.get_prescribed_therapies", + args: {patient: frm.doc.patient}, + callback: function(r) { + if (r.message) { + show_therapy_types(frm, r.message); + } else { + frappe.msgprint({ + title: __('Not Therapies Prescribed'), + message: __('There are no Therapies prescribed for Patient {0}', [frm.doc.patient.bold()]), + indicator: 'blue' + }); + } + } + }); + } } }); @@ -393,6 +430,50 @@ let show_procedure_templates = function(frm, result){ d.show(); }; +let show_therapy_types = function(frm, result) { + var d = new frappe.ui.Dialog({ + title: __('Prescribed Therapies'), + fields: [ + { + fieldtype: 'HTML', fieldname: 'therapy_type' + } + ] + }); + var html_field = d.fields_dict.therapy_type.$wrapper; + $.each(result, function(x, y){ + var row = $(repl('
\ +
%(encounter)s
%(practitioner)s
%(date)s
\ +
%(therapy)s
\ +

', {therapy:y[0], + name: y[1], encounter:y[2], practitioner:y[3], date:y[4], + department:y[6]? y[6]:'', therapy_plan:y[5]})).appendTo(html_field); + + row.find("a").click(function() { + frm.doc.therapy_type = $(this).attr("data-therapy"); + frm.doc.practitioner = $(this).attr("data-practitioner"); + frm.doc.department = $(this).attr("data-department"); + frm.doc.therapy_plan = $(this).attr("data-therapy-plan"); + frm.refresh_field("therapy_type"); + frm.refresh_field("practitioner"); + frm.refresh_field("department"); + frm.refresh_field("therapy-plan"); + frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => { + if (r.default_duration) { + frm.set_value('duration', r.default_duration) + } + }); + d.hide(); + return false; + }); + }); + d.show(); +}; + let create_vital_signs = function(frm) { if (!frm.doc.patient) { frappe.throw(__('Please select patient')); diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json index 7f9a671d47..57e6c479d1 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json @@ -20,6 +20,9 @@ "procedure_template", "get_procedure_from_encounter", "procedure_prescription", + "therapy_type", + "get_prescribed_therapies", + "therapy_plan", "service_unit", "section_break_12", "practitioner", @@ -269,6 +272,28 @@ "print_hide": 1, "report_hide": 1 }, + { + "depends_on": "eval:doc.patient;", + "fieldname": "therapy_type", + "fieldtype": "Link", + "label": "Therapy", + "options": "Therapy Type", + "set_only_once": 1 + }, + { + "depends_on": "eval:doc.patient && doc.__islocal;", + "fieldname": "get_prescribed_therapies", + "fieldtype": "Button", + "label": "Get Prescribed Therapies" + }, + { + "depends_on": "eval: doc.patient && doc.therapy_type", + "fieldname": "therapy_plan", + "fieldtype": "Link", + "label": "Therapy Plan", + "mandatory_depends_on": "eval: doc.patient && doc.therapy_type", + "options": "Therapy Plan" + }, { "fieldname": "ref_sales_invoice", "fieldtype": "Link", @@ -285,7 +310,7 @@ } ], "links": [], - "modified": "2020-03-27 11:27:33.773195", + "modified": "2020-03-31 16:16:32.116865", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Appointment", diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py index a2d9d0240f..512d44ec37 100755 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py @@ -412,11 +412,36 @@ def get_events(start, end, filters=None): @frappe.whitelist() def get_procedure_prescribed(patient): - return frappe.db.sql("""select pp.name, pp.procedure, pp.parent, ct.practitioner, - ct.encounter_date, pp.practitioner, pp.date, pp.department - from `tabPatient Encounter` ct, `tabProcedure Prescription` pp - where ct.patient=%(patient)s and pp.parent=ct.name and pp.appointment_booked=0 - order by ct.creation desc""", {'patient': patient}) + return frappe.db.sql( + """ + SELECT + pp.name, pp.procedure, pp.parent, ct.practitioner, + ct.encounter_date, pp.practitioner, pp.date, pp.department + FROM + `tabPatient Encounter` ct, `tabProcedure Prescription` pp + WHERE + ct.patient=%(patient)s and pp.parent=ct.name and pp.appointment_booked=0 + ORDER BY + ct.creation desc + """, {'patient': patient} + ) + + +@frappe.whitelist() +def get_prescribed_therapies(patient): + return frappe.db.sql( + """ + SELECT + t.therapy_type, t.name, t.parent, e.practitioner, + e.encounter_date, e.therapy_plan, e.visit_department + FROM + `tabPatient Encounter` e, `tabTherapy Plan Detail` t + WHERE + e.patient=%(patient)s and t.parent=e.name + ORDER BY + e.creation desc + """, {'patient': patient} + ) def update_appointment_status(): diff --git a/erpnext/healthcare/doctype/patient_assessment/__init__.py b/erpnext/healthcare/doctype/patient_assessment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js new file mode 100644 index 0000000000..c7074e88d5 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.js @@ -0,0 +1,86 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Patient Assessment', { + refresh: function(frm) { + if (frm.doc.assessment_template) { + frm.trigger('set_score_range'); + } + + if (!frm.doc.__islocal) { + frm.trigger('show_patient_progress'); + } + }, + + assessment_template: function(frm) { + if (frm.doc.assessment_template) { + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Patient Assessment Template', + name: frm.doc.assessment_template + }, + callback: function(data) { + frm.doc.assessment_sheet = []; + $.each(data.message.parameters, function(_i, e) { + let entry = frm.add_child('assessment_sheet'); + entry.parameter = e.assessment_parameter; + }); + + frm.set_value('scale_min', data.message.scale_min); + frm.set_value('scale_max', data.message.scale_max); + frm.set_value('assessment_description', data.message.assessment_description); + frm.set_value('total_score', data.message.scale_max * data.message.parameters.length); + frm.trigger('set_score_range'); + refresh_field('assessment_sheet'); + } + }); + } + }, + + set_score_range: function(frm) { + let options = []; + for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; i++) { + options.push(i); + } + frappe.meta.get_docfield('Patient Assessment Sheet', 'score', frm.doc.name).options = [''].concat(options); + }, + + calculate_total_score: function(frm, cdt, cdn) { + let row = locals[cdt][cdn]; + let total_score = 0; + $.each(frm.doc.assessment_sheet || [], function(_i, item) { + if (item.score) { + total_score += parseInt(item.score); + } + }); + + frm.set_value('total_score_obtained', total_score); + }, + + show_patient_progress: function(frm) { + let bars = []; + let message = ''; + let added_min = false; + + let title = __('{0} out of {1}', [frm.doc.total_score_obtained, frm.doc.total_score]); + + bars.push({ + 'title': title, + 'width': (frm.doc.total_score_obtained / frm.doc.total_score * 100) + '%', + 'progress_class': 'progress-bar-success' + }); + if (bars[0].width == '0%') { + bars[0].width = '0.5%'; + added_min = 0.5; + } + message = title; + frm.dashboard.add_progress(__('Status'), bars, message); + }, +}); + +frappe.ui.form.on('Patient Assessment Sheet', { + score: function(frm, cdt, cdn) { + frm.events.calculate_total_score(frm, cdt, cdn); + } +}); \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json new file mode 100644 index 0000000000..3952a8153f --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.json @@ -0,0 +1,172 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2020-04-19 22:45:12.356209", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "therapy_session", + "patient", + "assessment_template", + "column_break_4", + "healthcare_practitioner", + "assessment_datetime", + "assessment_description", + "section_break_7", + "assessment_sheet", + "section_break_9", + "total_score_obtained", + "column_break_11", + "total_score", + "scale_min", + "scale_max", + "amended_from" + ], + "fields": [ + { + "fetch_from": "therapy_session.patient", + "fieldname": "patient", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Patient", + "options": "Patient", + "reqd": 1 + }, + { + "fieldname": "assessment_template", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Assessment Template", + "options": "Patient Assessment Template", + "reqd": 1 + }, + { + "fieldname": "therapy_session", + "fieldtype": "Link", + "label": "Therapy Session", + "options": "Therapy Session" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fetch_from": "therapy_session.practitioner", + "fieldname": "healthcare_practitioner", + "fieldtype": "Link", + "label": "Healthcare Practitioner", + "options": "Healthcare Practitioner" + }, + { + "fieldname": "assessment_datetime", + "fieldtype": "Datetime", + "label": "Assessment Datetime" + }, + { + "fieldname": "section_break_7", + "fieldtype": "Section Break" + }, + { + "fieldname": "assessment_sheet", + "fieldtype": "Table", + "label": "Assessment Sheet", + "options": "Patient Assessment Sheet" + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_score", + "fieldtype": "Int", + "label": "Total Score", + "read_only": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_score_obtained", + "fieldtype": "Int", + "label": "Total Score Obtained", + "read_only": 1 + }, + { + "fieldname": "scale_min", + "fieldtype": "Int", + "hidden": 1, + "label": "Scale Min", + "read_only": 1 + }, + { + "fieldname": "scale_max", + "fieldtype": "Int", + "hidden": 1, + "label": "Scale Max", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "HLC-PA-.YYYY.-" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Patient Assessment", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "assessment_description", + "fieldtype": "Small Text", + "label": "Assessment Description" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-21 13:23:09.815007", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient Assessment", + "owner": "Administrator", + "permissions": [ + { + "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 + }, + { + "cancel": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "patient", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py new file mode 100644 index 0000000000..3033a3e6ac --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment/patient_assessment.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 +from frappe.model.mapper import get_mapped_doc + +class PatientAssessment(Document): + def validate(self): + self.set_total_score() + + def set_total_score(self): + total_score = 0 + for entry in self.assessment_sheet: + total_score += int(entry.score) + self.total_score_obtained = total_score + +@frappe.whitelist() +def create_patient_assessment(source_name, target_doc=None): + doc = get_mapped_doc('Therapy Session', source_name, { + 'Therapy Session': { + 'doctype': 'Patient Assessment', + 'field_map': [ + ['therapy_session', 'name'], + ['patient', 'patient'], + ['practitioner', 'practitioner'] + ] + } + }, target_doc) + + return doc + + + diff --git a/erpnext/healthcare/doctype/patient_assessment/test_patient_assessment.py b/erpnext/healthcare/doctype/patient_assessment/test_patient_assessment.py new file mode 100644 index 0000000000..3fda8550f6 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment/test_patient_assessment.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPatientAssessment(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/patient_assessment_detail/__init__.py b/erpnext/healthcare/doctype/patient_assessment_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.json b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.json new file mode 100644 index 0000000000..179f441044 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-04-19 19:33:00.115395", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "assessment_parameter" + ], + "fields": [ + { + "fieldname": "assessment_parameter", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Assessment Parameter", + "options": "Patient Assessment Parameter", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-19 19:33:00.115395", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient Assessment Detail", + "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/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.py b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.py new file mode 100644 index 0000000000..0519599ac0 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_detail/patient_assessment_detail.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 PatientAssessmentDetail(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/__init__.py b/erpnext/healthcare/doctype/patient_assessment_parameter/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.js b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.js new file mode 100644 index 0000000000..2c5d270d57 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Patient Assessment Parameter', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.json b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.json new file mode 100644 index 0000000000..098bdefea7 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "autoname": "field:assessment_parameter", + "creation": "2020-04-15 14:34:46.551042", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "assessment_parameter" + ], + "fields": [ + { + "fieldname": "assessment_parameter", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Assessment Parameter", + "reqd": 1, + "unique": 1 + } + ], + "links": [], + "modified": "2020-04-20 09:22:19.135196", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient Assessment Parameter", + "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/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.py b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.py new file mode 100644 index 0000000000..b8e0074717 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_parameter/patient_assessment_parameter.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 PatientAssessmentParameter(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_assessment_parameter/test_patient_assessment_parameter.py b/erpnext/healthcare/doctype/patient_assessment_parameter/test_patient_assessment_parameter.py new file mode 100644 index 0000000000..e722f9905e --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_parameter/test_patient_assessment_parameter.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPatientAssessmentParameter(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/patient_assessment_sheet/__init__.py b/erpnext/healthcare/doctype/patient_assessment_sheet/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.json b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.json new file mode 100644 index 0000000000..64e4aef7cf --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.json @@ -0,0 +1,57 @@ +{ + "actions": [], + "creation": "2020-04-19 23:07:02.220244", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "parameter", + "score", + "time", + "column_break_4", + "comments" + ], + "fields": [ + { + "fieldname": "parameter", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Parameter", + "options": "Patient Assessment Parameter", + "reqd": 1 + }, + { + "fieldname": "score", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Score", + "reqd": 1 + }, + { + "fieldname": "time", + "fieldtype": "Time", + "label": "Time" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "comments", + "fieldtype": "Small Text", + "label": "Comments" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-20 09:56:28.746619", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient Assessment Sheet", + "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/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.py b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.py new file mode 100644 index 0000000000..40da763013 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_sheet/patient_assessment_sheet.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 PatientAssessmentSheet(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_assessment_template/__init__.py b/erpnext/healthcare/doctype/patient_assessment_template/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.js b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.js new file mode 100644 index 0000000000..40419362a4 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Patient Assessment Template', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.json b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.json new file mode 100644 index 0000000000..de006b1805 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.json @@ -0,0 +1,109 @@ +{ + "actions": [], + "autoname": "field:assessment_name", + "creation": "2020-04-19 19:33:13.204707", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "assessment_name", + "section_break_2", + "parameters", + "assessment_scale_details_section", + "scale_min", + "scale_max", + "column_break_8", + "assessment_description" + ], + "fields": [ + { + "fieldname": "parameters", + "fieldtype": "Table", + "label": "Parameters", + "options": "Patient Assessment Detail" + }, + { + "fieldname": "assessment_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Assessment Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break", + "label": "Assessment Parameters" + }, + { + "fieldname": "assessment_scale_details_section", + "fieldtype": "Section Break", + "label": "Assessment Scale" + }, + { + "fieldname": "scale_min", + "fieldtype": "Int", + "label": "Scale Minimum" + }, + { + "fieldname": "scale_max", + "fieldtype": "Int", + "label": "Scale Maximum" + }, + { + "fieldname": "column_break_8", + "fieldtype": "Column Break" + }, + { + "fieldname": "assessment_description", + "fieldtype": "Small Text", + "label": "Assessment Description" + } + ], + "links": [], + "modified": "2020-04-21 13:14:19.075167", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Patient Assessment Template", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "write": 1 + }, + { + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.py b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.py new file mode 100644 index 0000000000..083cab5d01 --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_template/patient_assessment_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 PatientAssessmentTemplate(Document): + pass diff --git a/erpnext/healthcare/doctype/patient_assessment_template/test_patient_assessment_template.py b/erpnext/healthcare/doctype/patient_assessment_template/test_patient_assessment_template.py new file mode 100644 index 0000000000..86dbd5438c --- /dev/null +++ b/erpnext/healthcare/doctype/patient_assessment_template/test_patient_assessment_template.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestPatientAssessmentTemplate(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index 83c5d2be9c..78e789d359 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js @@ -3,6 +3,10 @@ frappe.ui.form.on('Patient Encounter', { setup: function(frm) { + frm.get_field('therapies').grid.editable_fields = [ + {fieldname: 'therapy_type', columns: 8}, + {fieldname: 'no_of_sessions', columns: 2} + ]; frm.get_field('drug_prescription').grid.editable_fields = [ {fieldname: 'drug_code', columns: 2}, {fieldname: 'drug_name', columns: 2}, diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json index d00e7bc7dd..5f11039e19 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json @@ -42,6 +42,10 @@ "lab_test_prescription", "sb_procedures", "procedure_prescription", + "rehabilitation_section", + "therapy_plan", + "therapies", + "section_break_33", "encounter_comment", "amended_from" ], @@ -256,6 +260,29 @@ "print_hide": 1, "read_only": 1 }, + { + "fieldname": "rehabilitation_section", + "fieldtype": "Section Break", + "label": "Rehabilitation" + }, + { + "fieldname": "therapies", + "fieldtype": "Table", + "label": "Therapies", + "options": "Therapy Plan Detail" + }, + { + "fieldname": "section_break_33", + "fieldtype": "Section Break" + }, + { + "fieldname": "therapy_plan", + "fieldtype": "Link", + "hidden": 1, + "label": "Therapy Plan", + "options": "Therapy Plan", + "read_only": 1 + }, { "fieldname": "appointment_type", "fieldtype": "Link", @@ -291,7 +318,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-02-27 12:42:21.751964", + "modified": "2020-04-14 16:18:08.180457", "modified_by": "Administrator", "module": "Healthcare", "name": "Patient Encounter", diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py index ade4748ece..767643bc73 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals import frappe +from frappe import _ from frappe.model.document import Document from frappe.utils import cstr from frappe import _ @@ -22,6 +23,24 @@ class PatientEncounter(Document): frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open') delete_medical_record(self) + def on_submit(self): + create_therapy_plan(self) + +def create_therapy_plan(encounter): + if len(encounter.therapies): + doc = frappe.new_doc('Therapy Plan') + doc.patient = encounter.patient + doc.start_date = encounter.encounter_date + for entry in encounter.therapies: + doc.append('therapy_plan_details', { + 'therapy_type': entry.therapy_type, + 'no_of_sessions': entry.no_of_sessions + }) + doc.save(ignore_permissions=True) + if doc.get('name'): + encounter.db_set('therapy_plan', doc.name) + frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True) + def insert_encounter_to_medical_record(doc): subject = set_subject_field(doc) medical_record = frappe.new_doc('Patient Medical Record') diff --git a/erpnext/healthcare/doctype/therapy_plan/__init__.py b/erpnext/healthcare/doctype/therapy_plan/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py new file mode 100644 index 0000000000..526bb95b70 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils import getdate +from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type +from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session +from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient + +class TestTherapyPlan(unittest.TestCase): + def test_creation_on_encounter_submission(self): + patient, medical_department, practitioner = create_healthcare_docs() + encounter = create_encounter(patient, medical_department, practitioner) + self.assertTrue(frappe.db.exists('Therapy Plan', encounter.therapy_plan)) + + def test_status(self): + plan = create_therapy_plan() + self.assertEquals(plan.status, 'Not Started') + + session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab') + frappe.get_doc(session).submit() + self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress') + + session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab') + frappe.get_doc(session).submit() + self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed') + + +def create_therapy_plan(): + patient = create_patient() + therapy_type = create_therapy_type() + plan = frappe.new_doc('Therapy Plan') + plan.patient = patient + plan.start_date = getdate() + plan.append('therapy_plan_details', { + 'therapy_type': therapy_type.name, + 'no_of_sessions': 2 + }) + plan.save() + return plan + +def create_encounter(patient, medical_department, practitioner): + encounter = frappe.new_doc('Patient Encounter') + encounter.patient = patient + encounter.practitioner = practitioner + encounter.medical_department = medical_department + therapy_type = create_therapy_type() + encounter.append('therapies', { + 'therapy_type': therapy_type.name, + 'no_of_sessions': 2 + }) + encounter.save() + encounter.submit() + return encounter diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js new file mode 100644 index 0000000000..dea0cfeb84 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.js @@ -0,0 +1,90 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Therapy Plan', { + setup: function(frm) { + frm.get_field('therapy_plan_details').grid.editable_fields = [ + {fieldname: 'therapy_type', columns: 6}, + {fieldname: 'no_of_sessions', columns: 2}, + {fieldname: 'sessions_completed', columns: 2} + ]; + }, + + refresh: function(frm) { + if (!frm.doc.__islocal) { + frm.trigger('show_progress_for_therapies'); + } + + if (!frm.doc.__islocal && frm.doc.status != 'Completed') { + let therapy_types = (frm.doc.therapy_plan_details || []).map(function(d){ return d.therapy_type }); + const fields = [{ + fieldtype: 'Link', + label: __('Therapy Type'), + fieldname: 'therapy_type', + options: 'Therapy Type', + reqd: 1, + get_query: function() { + return { + filters: { 'therapy_type': ['in', therapy_types]} + } + } + }]; + + frm.add_custom_button(__('Therapy Session'), function() { + frappe.prompt(fields, data => { + frappe.call({ + method: 'erpnext.healthcare.doctype.therapy_plan.therapy_plan.make_therapy_session', + args: { + therapy_plan: frm.doc.name, + patient: frm.doc.patient, + therapy_type: data.therapy_type + }, + freeze: true, + callback: function(r) { + if (r.message) { + frappe.model.sync(r.message); + frappe.set_route('Form', r.message.doctype, r.message.name); + } + } + }); + }, __('Select Therapy Type'), __('Create')); + }, __('Create')); + } + }, + + show_progress_for_therapies: function(frm) { + let bars = []; + let message = ''; + let added_min = false; + + // completed sessions + let title = __('{0} sessions completed', [frm.doc.total_sessions_completed]); + if (frm.doc.total_sessions_completed === 1) { + title = __('{0} session completed', [frm.doc.total_sessions_completed]); + } + title += __(' out of {0}', [frm.doc.total_sessions]); + + bars.push({ + 'title': title, + 'width': (frm.doc.total_sessions_completed / frm.doc.total_sessions * 100) + '%', + 'progress_class': 'progress-bar-success' + }); + if (bars[0].width == '0%') { + bars[0].width = '0.5%'; + added_min = 0.5; + } + message = title; + frm.dashboard.add_progress(__('Status'), bars, message); + }, +}); + +frappe.ui.form.on('Therapy Plan Detail', { + no_of_sessions: function(frm) { + let total = 0; + $.each(frm.doc.therapy_plan_details, function(_i, e) { + total += e.no_of_sessions; + }); + frm.set_value('total_sessions', total); + refresh_field('total_sessions'); + } +}); \ No newline at end of file diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json new file mode 100644 index 0000000000..ca78b6618e --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.json @@ -0,0 +1,151 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2020-03-29 20:56:49.758602", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "patient", + "patient_name", + "column_break_4", + "status", + "start_date", + "section_break_3", + "therapy_plan_details", + "title", + "section_break_9", + "total_sessions", + "column_break_11", + "total_sessions_completed" + ], + "fields": [ + { + "fieldname": "patient", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Patient", + "options": "Patient", + "reqd": 1 + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Start Date", + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break" + }, + { + "fieldname": "therapy_plan_details", + "fieldtype": "Table", + "label": "Therapy Plan Details", + "options": "Therapy Plan Detail", + "reqd": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "HLC-THP-.YYYY.-" + }, + { + "fetch_from": "patient.patient_name", + "fieldname": "patient_name", + "fieldtype": "Data", + "label": "Patient Name", + "read_only": 1 + }, + { + "default": "{patient_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_sessions", + "fieldtype": "Int", + "label": "Total Sessions", + "read_only": 1 + }, + { + "fieldname": "column_break_11", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_sessions_completed", + "fieldtype": "Int", + "label": "Total Sessions Completed", + "read_only": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Not Started\nIn Progress\nCompleted\nCancelled", + "read_only": 1 + } + ], + "links": [], + "modified": "2020-04-21 13:13:43.956014", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Therapy Plan", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "patient", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "patient", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py new file mode 100644 index 0000000000..201264f829 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py @@ -0,0 +1,42 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 TherapyPlan(Document): + def validate(self): + self.set_totals() + self.set_status() + + def set_status(self): + if not self.total_sessions_completed: + self.status = 'Not Started' + else: + if self.total_sessions_completed < self.total_sessions: + self.status = 'In Progress' + elif self.total_sessions_completed == self.total_sessions: + self.status = 'Completed' + + def set_totals(self): + total_sessions = sum([int(d.no_of_sessions) for d in self.get('therapy_plan_details')]) + total_sessions_completed = sum([int(d.sessions_completed) for d in self.get('therapy_plan_details')]) + self.db_set('total_sessions', total_sessions) + self.db_set('total_sessions_completed', total_sessions_completed) + + +@frappe.whitelist() +def make_therapy_session(therapy_plan, patient, therapy_type): + therapy_type = frappe.get_doc('Therapy Type', therapy_type) + + therapy_session = frappe.new_doc('Therapy Session') + therapy_session.therapy_plan = therapy_plan + therapy_session.patient = patient + therapy_session.therapy_type = therapy_type.name + therapy_session.duration = therapy_type.default_duration + therapy_session.rate = therapy_type.rate + therapy_session.exercises = therapy_type.exercises + + return therapy_session.as_dict() \ No newline at end of file diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py new file mode 100644 index 0000000000..df647829db --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'therapy_plan', + 'transactions': [ + { + 'label': _('Therapy Sessions'), + 'items': ['Therapy Session'] + } + ] + } diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan_list.js b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_list.js new file mode 100644 index 0000000000..63967aff33 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan_list.js @@ -0,0 +1,11 @@ +frappe.listview_settings['Therapy Plan'] = { + get_indicator: function(doc) { + var colors = { + 'Completed': 'green', + 'In Progress': 'orange', + 'Not Started': 'red', + 'Cancelled': 'grey' + }; + return [__(doc.status), colors[doc.status], 'status,=,' + doc.status]; + } +}; diff --git a/erpnext/healthcare/doctype/therapy_plan_detail/__init__.py b/erpnext/healthcare/doctype/therapy_plan_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json new file mode 100644 index 0000000000..9eb20e2ef3 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.json @@ -0,0 +1,48 @@ +{ + "actions": [], + "creation": "2020-03-29 20:52:57.068731", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "therapy_type", + "no_of_sessions", + "sessions_completed" + ], + "fields": [ + { + "fieldname": "therapy_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Therapy Type", + "options": "Therapy Type", + "reqd": 1 + }, + { + "fieldname": "no_of_sessions", + "fieldtype": "Int", + "in_list_view": 1, + "label": "No of Sessions" + }, + { + "default": "0", + "depends_on": "eval:doc.parenttype=='Therapy Plan';", + "fieldname": "sessions_completed", + "fieldtype": "Int", + "label": "Sessions Completed", + "read_only": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-03-30 22:02:01.740109", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Therapy Plan Detail", + "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/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.py b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.py new file mode 100644 index 0000000000..44211f32e3 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_plan_detail/therapy_plan_detail.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 TherapyPlanDetail(Document): + pass diff --git a/erpnext/healthcare/doctype/therapy_session/__init__.py b/erpnext/healthcare/doctype/therapy_session/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/therapy_session/test_therapy_session.py b/erpnext/healthcare/doctype/therapy_session/test_therapy_session.py new file mode 100644 index 0000000000..75bb8df196 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_session/test_therapy_session.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestTherapySession(unittest.TestCase): + pass diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js new file mode 100644 index 0000000000..bb675752bb --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js @@ -0,0 +1,60 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Therapy Session', { + setup: function(frm) { + frm.get_field('exercises').grid.editable_fields = [ + {fieldname: 'exercise_type', columns: 7}, + {fieldname: 'counts_target', columns: 1}, + {fieldname: 'counts_completed', columns: 1}, + {fieldname: 'assistance_level', columns: 1} + ]; + }, + + refresh: function(frm) { + if (!frm.doc.__islocal) { + let target = 0; + let completed = 0; + $.each(frm.doc.exercises, function(_i, e) { + target += e.counts_target; + completed += e.counts_completed; + }); + frm.dashboard.add_indicator(__('Counts Targetted: {0}', [target]), 'blue'); + frm.dashboard.add_indicator(__('Counts Completed: {0}', [completed]), (completed < target) ? 'orange' : 'green'); + } + + if (frm.doc.docstatus === 1) { + frm.add_custom_button(__('Patient Assessment'),function() { + frappe.model.open_mapped_doc({ + method: 'erpnext.healthcare.doctype.patient_assessment.patient_assessment.create_patient_assessment', + frm: frm, + }) + }, 'Create'); + } + }, + + therapy_type: function(frm) { + if (frm.doc.therapy_type) { + frappe.call({ + 'method': 'frappe.client.get', + args: { + doctype: 'Therapy Type', + name: frm.doc.therapy_type + }, + callback: function(data) { + frm.set_value('duration', data.message.default_duration); + frm.set_value('rate', data.message.rate); + frm.doc.exercises = []; + $.each(data.message.exercises, function(_i, e) { + let exercise = frm.add_child('exercises'); + exercise.exercise_type = e.exercise_type; + exercise.difficulty_level = e.difficulty_level; + exercise.counts_target = e.counts_target; + exercise.assistance_level = e.assistance_level; + }); + refresh_field('exercises'); + } + }); + } + } +}); \ No newline at end of file diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.json b/erpnext/healthcare/doctype/therapy_session/therapy_session.json new file mode 100644 index 0000000000..5ff719672f --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.json @@ -0,0 +1,218 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2020-03-11 08:57:40.669857", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "appointment", + "patient", + "patient_age", + "gender", + "column_break_5", + "therapy_plan", + "therapy_type", + "practitioner", + "department", + "details_section", + "duration", + "rate", + "location", + "company", + "column_break_12", + "service_unit", + "start_date", + "start_time", + "invoiced", + "exercises_section", + "exercises", + "amended_from" + ], + "fields": [ + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "options": "HLC-THP-.YYYY.-" + }, + { + "fieldname": "appointment", + "fieldtype": "Link", + "label": "Appointment", + "options": "Patient Appointment" + }, + { + "fieldname": "patient", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Patient", + "options": "Patient", + "reqd": 1 + }, + { + "fetch_from": "patient.sex", + "fieldname": "gender", + "fieldtype": "Link", + "label": "Gender", + "options": "Gender", + "read_only": 1 + }, + { + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, + { + "fieldname": "practitioner", + "fieldtype": "Link", + "label": "Healthcare Practitioner", + "options": "Healthcare Practitioner" + }, + { + "fieldname": "department", + "fieldtype": "Link", + "label": "Medical Department", + "options": "Medical Department" + }, + { + "fieldname": "details_section", + "fieldtype": "Section Break", + "label": "Details" + }, + { + "fetch_from": "therapy_template.default_duration", + "fieldname": "duration", + "fieldtype": "Int", + "label": "Duration" + }, + { + "fieldname": "location", + "fieldtype": "Select", + "label": "Location", + "options": "\nCenter\nHome\nTele" + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fetch_from": "therapy_template.rate", + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate" + }, + { + "fieldname": "exercises_section", + "fieldtype": "Section Break", + "label": "Exercises" + }, + { + "fieldname": "exercises", + "fieldtype": "Table", + "label": "Exercises", + "options": "Exercise" + }, + { + "depends_on": "eval: doc.therapy_plan", + "fieldname": "therapy_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Therapy Type", + "options": "Therapy Type", + "reqd": 1 + }, + { + "fieldname": "therapy_plan", + "fieldtype": "Link", + "label": "Therapy Plan", + "options": "Therapy Plan", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Therapy Session", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "service_unit", + "fieldtype": "Link", + "label": "Healthcare Service Unit", + "options": "Healthcare Service Unit" + }, + { + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date" + }, + { + "fieldname": "start_time", + "fieldtype": "Time", + "label": "Start Time" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "0", + "fieldname": "invoiced", + "fieldtype": "Check", + "label": "Invoiced", + "read_only": 1 + }, + { + "fieldname": "patient_age", + "fieldtype": "Data", + "label": "Patient Age", + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-21 13:16:46.378798", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Therapy Session", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "quick_entry": 1, + "search_fields": "patient,appointment,therapy_plan,therapy_type", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "patient", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py new file mode 100644 index 0000000000..45d2ee60e6 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 +from frappe.model.mapper import get_mapped_doc + +class TherapySession(Document): + def on_submit(self): + self.update_sessions_count_in_therapy_plan() + + def on_cancel(self): + self.update_sessions_count_in_therapy_plan(on_cancel=True) + + def update_sessions_count_in_therapy_plan(self, on_cancel=False): + therapy_plan = frappe.get_doc('Therapy Plan', self.therapy_plan) + for entry in therapy_plan.therapy_plan_details: + if entry.therapy_type == self.therapy_type: + if on_cancel: + entry.sessions_completed -= 1 + else: + entry.sessions_completed += 1 + therapy_plan.save() + + +@frappe.whitelist() +def create_therapy_session(source_name, target_doc=None): + def set_missing_values(source, target): + therapy_type = frappe.get_doc('Therapy Type', source.therapy_type) + target.exercises = therapy_type.exercises + + doc = get_mapped_doc('Patient Appointment', source_name, { + 'Patient Appointment': { + 'doctype': 'Therapy Session', + 'field_map': [ + ['appointment', 'name'], + ['patient', 'patient'], + ['patient_age', 'patient_age'], + ['gender', 'patient_sex'], + ['therapy_type', 'therapy_type'], + ['therapy_plan', 'therapy_plan'], + ['practitioner', 'practitioner'], + ['department', 'department'], + ['start_date', 'appointment_date'], + ['start_time', 'appointment_time'], + ['service_unit', 'service_unit'], + ['company', 'company'], + ['invoiced', 'invoiced'] + ] + } + }, target_doc, set_missing_values) + + return doc \ No newline at end of file diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py b/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py new file mode 100644 index 0000000000..9de7e29323 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_session/therapy_session_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'therapy_session', + 'transactions': [ + { + 'label': _('Assessments'), + 'items': ['Patient Assessment'] + } + ] + } diff --git a/erpnext/healthcare/doctype/therapy_type/__init__.py b/erpnext/healthcare/doctype/therapy_type/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py new file mode 100644 index 0000000000..03a1be8a4e --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_type/test_therapy_type.py @@ -0,0 +1,50 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest + +class TestTherapyType(unittest.TestCase): + def test_therapy_type_item(self): + therapy_type = create_therapy_type() + self.assertTrue(frappe.db.exists('Item', therapy_type.item)) + + therapy_type.disabled = 1 + therapy_type.save() + self.assertEquals(frappe.db.get_value('Item', therapy_type.item, 'disabled'), 1) + +def create_therapy_type(): + exercise = create_exercise_type() + therapy_type = frappe.db.exists('Therapy Type', 'Basic Rehab') + if not therapy_type: + therapy_type = frappe.new_doc('Therapy Type') + therapy_type.therapy_type = 'Basic Rehab' + therapy_type.default_duration = 30 + therapy_type.is_billable = 1 + therapy_type.rate = 5000 + therapy_type.item_code = 'Basic Rehab' + therapy_type.item_name = 'Basic Rehab' + therapy_type.item_group = 'Services' + therapy_type.append('exercises', { + 'exercise_type': exercise.name, + 'counts_target': 10, + 'assistance_level': 'Passive' + }) + therapy_type.save() + else: + therapy_type = frappe.get_doc('Therapy Type', 'Basic Rehab') + return therapy_type + +def create_exercise_type(): + exercise_type = frappe.db.exists('Exercise Type', 'Sit to Stand') + if not exercise_type: + exercise_type = frappe.new_doc('Exercise Type') + exercise_type.exercise_name = 'Sit to Stand' + exercise_type.append('steps_table', { + 'title': 'Step 1', + 'description': 'Squat and Rise' + }) + exercise_type.save() + return exercise_type \ No newline at end of file diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.js b/erpnext/healthcare/doctype/therapy_type/therapy_type.js new file mode 100644 index 0000000000..7a61b0def0 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.js @@ -0,0 +1,93 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Therapy Type', { + setup: function(frm) { + frm.get_field('exercises').grid.editable_fields = [ + {fieldname: 'exercise_type', columns: 7}, + {fieldname: 'difficulty_level', columns: 1}, + {fieldname: 'counts_target', columns: 1}, + {fieldname: 'assistance_level', columns: 1} + ]; + }, + + refresh: function(frm) { + if (!frm.doc.__islocal) { + cur_frm.add_custom_button(__('Change Item Code'), function() { + change_template_code(frm.doc); + }); + } + }, + + therapy_type: function(frm) { + if (!frm.doc.item_code) + frm.set_value('item_code', frm.doc.therapy_type); + if (!frm.doc.description) + frm.set_value('description', frm.doc.therapy_type); + mark_change_in_item(frm); + }, + + rate: function(frm) { + mark_change_in_item(frm); + }, + + is_billable: function (frm) { + mark_change_in_item(frm); + }, + + item_group: function(frm) { + mark_change_in_item(frm); + }, + + description: function(frm) { + mark_change_in_item(frm); + }, + + medical_department: function(frm) { + mark_change_in_item(frm); + } +}); + +let mark_change_in_item = function(frm) { + if (!frm.doc.__islocal) { + frm.doc.change_in_item = 1; + } +}; + +let change_template_code = function(doc) { + let d = new frappe.ui.Dialog({ + title:__('Change Item Code'), + fields:[ + { + 'fieldtype': 'Data', + 'label': 'Item Code', + 'fieldname': 'item_code', + reqd: 1 + } + ], + primary_action: function() { + let values = d.get_values(); + + if (values) { + frappe.call({ + 'method': 'erpnext.healthcare.doctype.therapy_type.therapy_type.change_item_code_from_therapy', + 'args': {item_code: values.item_code, doc: doc}, + callback: function () { + cur_frm.reload_doc(); + frappe.show_alert({ + message: 'Item Code renamed successfully', + indicator: 'green' + }); + } + }); + } + d.hide(); + }, + primary_action_label: __('Change Item Code') + }); + d.show(); + + d.set_values({ + 'item_code': doc.item_code + }); +}; diff --git a/erpnext/healthcare/doctype/therapy_type/therapy_type.json b/erpnext/healthcare/doctype/therapy_type/therapy_type.json new file mode 100644 index 0000000000..0b3c3caeaa --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.json @@ -0,0 +1,211 @@ +{ + "actions": [], + "autoname": "field:therapy_type", + "creation": "2020-03-29 20:48:31.715063", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "disabled", + "section_break_2", + "therapy_type", + "default_duration", + "medical_department", + "column_break_3", + "is_billable", + "rate", + "healthcare_service_unit", + "item_details_section", + "item", + "item_code", + "item_name", + "item_group", + "column_break_12", + "description", + "section_break_18", + "therapy_for", + "add_exercises", + "section_break_6", + "exercises", + "change_in_item" + ], + "fields": [ + { + "fieldname": "therapy_type", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Therapy Type", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "is_billable", + "fieldtype": "Check", + "label": "Is Billable" + }, + { + "depends_on": "eval:doc.is_billable;", + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "mandatory_depends_on": "eval:doc.is_billable;" + }, + { + "fieldname": "section_break_6", + "fieldtype": "Section Break", + "label": "Exercises" + }, + { + "fieldname": "exercises", + "fieldtype": "Table", + "label": "Exercises", + "options": "Exercise" + }, + { + "fieldname": "default_duration", + "fieldtype": "Int", + "label": "Default Duration (In Minutes)" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "item_details_section", + "fieldtype": "Section Break", + "label": "Item Details" + }, + { + "fieldname": "item", + "fieldtype": "Link", + "label": "Item", + "options": "Item", + "read_only": 1 + }, + { + "fieldname": "item_code", + "fieldtype": "Data", + "label": "Item Code", + "reqd": 1, + "set_only_once": 1 + }, + { + "fieldname": "item_group", + "fieldtype": "Link", + "label": "Item Group", + "options": "Item Group", + "reqd": 1 + }, + { + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name", + "reqd": 1 + }, + { + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "label": "Description" + }, + { + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, + { + "fieldname": "medical_department", + "fieldtype": "Link", + "label": "Medical Department", + "options": "Medical Department" + }, + { + "default": "0", + "fieldname": "change_in_item", + "fieldtype": "Check", + "hidden": 1, + "label": "Change In Item", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, + { + "fieldname": "therapy_for", + "fieldtype": "Table MultiSelect", + "label": "Therapy For", + "options": "Body Part Link" + }, + { + "fieldname": "healthcare_service_unit", + "fieldtype": "Link", + "label": "Healthcare Service Unit", + "options": "Healthcare Service Unit" + }, + { + "depends_on": "eval: doc.therapy_for", + "fieldname": "add_exercises", + "fieldtype": "Button", + "label": "Add Exercises", + "options": "add_exercises" + }, + { + "fieldname": "section_break_18", + "fieldtype": "Section Break" + } + ], + "links": [], + "modified": "2020-04-21 13:09:04.006289", + "modified_by": "Administrator", + "module": "Healthcare", + "name": "Therapy Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Healthcare Administrator", + "share": 1, + "write": 1 + }, + { + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Physician", + "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/healthcare/doctype/therapy_type/therapy_type.py b/erpnext/healthcare/doctype/therapy_type/therapy_type.py new file mode 100644 index 0000000000..ea3d84e7c5 --- /dev/null +++ b/erpnext/healthcare/doctype/therapy_type/therapy_type.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import json +from frappe import _ +from frappe.utils import cint +from frappe.model.document import Document +from frappe.model.rename_doc import rename_doc + +class TherapyType(Document): + def validate(self): + self.enable_disable_item() + + def after_insert(self): + create_item_from_therapy(self) + + def on_update(self): + if self.change_in_item: + self.update_item_and_item_price() + + def enable_disable_item(self): + if self.is_billable: + if self.disabled: + frappe.db.set_value('Item', self.item, 'disabled', 1) + else: + frappe.db.set_value('Item', self.item, 'disabled', 0) + + def update_item_and_item_price(self): + if self.is_billable and self.item: + item_doc = frappe.get_doc('Item', {'item_code': self.item}) + item_doc.item_name = self.item_name + item_doc.item_group = self.item_group + item_doc.description = self.description + item_doc.disabled = 0 + item_doc.ignore_mandatory = True + item_doc.save(ignore_permissions=True) + + if self.rate: + item_price = frappe.get_doc('Item Price', {'item_code': self.item}) + item_price.item_name = self.item_name + item_price.price_list_name = self.rate + item_price.ignore_mandatory = True + item_price.save() + + elif not self.is_billable and self.item: + frappe.db.set_value('Item', self.item, 'disabled', 1) + + self.db_set('change_in_item', 0) + + def add_exercises(self): + exercises = self.get_exercises_for_body_parts() + last_idx = max([cint(d.idx) for d in self.get('exercises')] or [0,]) + for i, d in enumerate(exercises): + ch = self.append('exercises', {}) + ch.exercise_type = d.parent + ch.idx = last_idx + i + 1 + + def get_exercises_for_body_parts(self): + body_parts = [entry.body_part for entry in self.therapy_for] + + exercises = frappe.db.sql( + """ + SELECT DISTINCT + b.parent, e.name, e.difficulty_level + FROM + `tabExercise Type` e, `tabBody Part Link` b + WHERE + b.body_part IN %(body_parts)s AND b.parent=e.name + """, {'body_parts': body_parts}, as_dict=1) + + return exercises + + +def create_item_from_therapy(doc): + disabled = doc.disabled + if doc.is_billable and not doc.disabled: + disabled = 0 + + uom = frappe.db.exists('UOM', 'Unit') or frappe.db.get_single_value('Stock Settings', 'stock_uom') + + item = frappe.get_doc({ + 'doctype': 'Item', + 'item_code': doc.item_code, + 'item_name': doc.item_name, + 'item_group': doc.item_group, + 'description': doc.description, + 'is_sales_item': 1, + 'is_service_item': 1, + 'is_purchase_item': 0, + 'is_stock_item': 0, + 'show_in_website': 0, + 'is_pro_applicable': 0, + 'disabled': disabled, + 'stock_uom': uom + }).insert(ignore_permissions=True, ignore_mandatory=True) + + make_item_price(item.name, doc.rate) + doc.db_set('item', item.name) + + +def make_item_price(item, item_price): + price_list_name = frappe.db.get_value('Price List', {'selling': 1}) + frappe.get_doc({ + 'doctype': 'Item Price', + 'price_list': price_list_name, + 'item_code': item, + 'price_list_rate': item_price + }).insert(ignore_permissions=True, ignore_mandatory=True) + +@frappe.whitelist() +def change_item_code_from_therapy(item_code, doc): + doc = frappe._dict(json.loads(doc)) + + if frappe.db.exists('Item', {'item_code': item_code}): + frappe.throw(_('Item with Item Code {0} already exists').format(item_code)) + else: + rename_doc('Item', doc.item, item_code, ignore_permissions=True) + frappe.db.set_value('Therapy Type', doc.name, 'item_code', item_code) + return diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 246242ad84..9a32c737cf 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -30,8 +30,9 @@ def get_healthcare_services_to_invoice(patient): lab_tests = get_lab_tests_to_invoice(patient) clinical_procedures = get_clinical_procedures_to_invoice(patient) inpatient_services = get_inpatient_services_to_invoice(patient) + therapy_sessions = get_therapy_sessions_to_invoice(patient) - items_to_invoice += encounters + lab_tests + clinical_procedures + inpatient_services + items_to_invoice += encounters + lab_tests + clinical_procedures + inpatient_services + therapy_sessions return items_to_invoice def validate_customer_created(patient): @@ -243,6 +244,25 @@ def get_inpatient_services_to_invoice(patient): return services_to_invoice +def get_therapy_sessions_to_invoice(patient): + therapy_sessions_to_invoice = [] + therapy_sessions = frappe.get_list( + 'Therapy Session', + fields='*', + filters={'patient': patient.name, 'invoiced': False} + ) + for therapy in therapy_sessions: + if not therapy.appointment: + if therapy.therapy_type and frappe.db.get_value('Therapy Type', therapy.therapy_type, 'is_billable'): + therapy_sessions_to_invoice.append({ + 'reference_type': 'Therapy Session', + 'reference_name': therapy.name, + 'service': frappe.db.get_value('Therapy Type', therapy.therapy_type, 'item') + }) + + return therapy_sessions_to_invoice + + def get_service_item_and_practitioner_charge(doc): is_inpatient = doc.inpatient_record if is_inpatient: diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js index 96e5cd57c3..b49b0ba0f7 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.js +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js @@ -188,7 +188,7 @@ frappe.ui.form.on('Production Plan', { }, get_items_for_mr: function(frm) { - const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', + const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type']; frappe.call({ method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests", diff --git a/erpnext/public/build.json b/erpnext/public/build.json index 1490374d2d..e94d1ffe5c 100644 --- a/erpnext/public/build.json +++ b/erpnext/public/build.json @@ -1,18 +1,18 @@ { - "css/erpnext.css": [ - "public/less/erpnext.less", - "public/less/hub.less", - "public/less/call_popup.less" - ], - "css/marketplace.css": [ - "public/less/hub.less" - ], - "js/erpnext-web.min.js": [ - "public/js/website_utils.js", - "public/js/shopping_cart.js" - ], + "css/erpnext.css": [ + "public/less/erpnext.less", + "public/less/hub.less", + "public/less/call_popup.less" + ], + "css/marketplace.css": [ + "public/less/hub.less" + ], + "js/erpnext-web.min.js": [ + "public/js/website_utils.js", + "public/js/shopping_cart.js" + ], "css/erpnext-web.css": [ - "public/scss/website.scss" + "public/scss/website.scss" ], "js/marketplace.min.js": [ "public/js/hub/marketplace.js" @@ -47,15 +47,15 @@ "public/js/templates/item_quick_entry.html", "public/js/utils/item_quick_entry.js", "public/js/utils/customer_quick_entry.js", - "public/js/education/student_button.html", - "public/js/education/assessment_result_tool.html", - "public/js/hub/hub_factory.js", - "public/js/call_popup/call_popup.js", - "public/js/utils/dimension_tree_filter.js" - ], - "js/item-dashboard.min.js": [ - "stock/dashboard/item_dashboard.html", - "stock/dashboard/item_dashboard_list.html", - "stock/dashboard/item_dashboard.js" - ] + "public/js/education/student_button.html", + "public/js/education/assessment_result_tool.html", + "public/js/hub/hub_factory.js", + "public/js/call_popup/call_popup.js", + "public/js/utils/dimension_tree_filter.js" + ], + "js/item-dashboard.min.js": [ + "stock/dashboard/item_dashboard.html", + "stock/dashboard/item_dashboard_list.html", + "stock/dashboard/item_dashboard.js" + ] } diff --git a/erpnext/public/css/erpnext.css b/erpnext/public/css/erpnext.css index c55e422151..6e4efcb668 100644 --- a/erpnext/public/css/erpnext.css +++ b/erpnext/public/css/erpnext.css @@ -370,3 +370,39 @@ body[data-route="pos"] .collapse-btn { .leaderboard .list-item_content { padding-right: 45px; } +.exercise-card { + box-shadow: 0 1px 3px rgba(0,0,0,0.30); + border-radius: 2px; + padding: 6px 6px 6px 8px; + margin-top: 10px; + height: 100% !important; +} +.exercise-card .card-img-top { + width: 100%; + height: 15vw; + object-fit: cover; +} +.exercise-card .btn-edit { + position: absolute; + bottom: 10px; + left: 20px; +} +.exercise-card .btn-del { + position: absolute; + bottom: 10px; + left: 50px; +} +.exercise-card .card-body { + margin-bottom: 10px; +} +.exercise-card .card-footer { + padding: 10px; +} +.exercise-row { + height: 100% !important; + display: flex; + flex-wrap: wrap; +} +.exercise-col { + padding: 10px; +} diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less index abe48685f0..8685837d33 100644 --- a/erpnext/public/less/erpnext.less +++ b/erpnext/public/less/erpnext.less @@ -458,4 +458,50 @@ body[data-route="pos"] { .list-item_content { padding-right: 45px; } +} + +// Healthcare + +.exercise-card { + box-shadow: 0 1px 3px rgba(0,0,0,0.30); + border-radius: 2px; + padding: 6px 6px 6px 8px; + margin-top: 10px; + height: 100% !important; + + .card-img-top { + width: 100%; + height: 15vw; + object-fit: cover; + } + + .btn-edit { + position: absolute; + bottom: 10px; + left: 20px; + } + + .btn-del { + position: absolute; + bottom: 10px; + left: 50px; + } + + .card-body { + margin-bottom: 10px; + } + + .card-footer { + padding: 10px; + } +} + +.exercise-row { + height: 100% !important; + display: flex; + flex-wrap: wrap; +} + +.exercise-col { + padding: 10px; } \ No newline at end of file From 28deddeb6f1eb5faca18821bb94daee243082d36 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 23 Apr 2020 09:44:58 +0530 Subject: [PATCH 11/73] fix: incorrect out value in stock balance due to precision issue (#21379) --- erpnext/stock/report/stock_balance/stock_balance.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index ff03381389..ab87ee114d 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -170,6 +170,8 @@ def get_item_warehouse_map(filters, sle): from_date = getdate(filters.get("from_date")) to_date = getdate(filters.get("to_date")) + float_precision = cint(frappe.db.get_default("float_precision")) or 3 + for d in sle: key = (d.company, d.item_code, d.warehouse) if key not in iwb_map: @@ -184,7 +186,7 @@ def get_item_warehouse_map(filters, sle): qty_dict = iwb_map[(d.company, d.item_code, d.warehouse)] if d.voucher_type == "Stock Reconciliation": - qty_diff = flt(d.qty_after_transaction) - qty_dict.bal_qty + qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) else: qty_diff = flt(d.actual_qty) @@ -195,7 +197,7 @@ def get_item_warehouse_map(filters, sle): qty_dict.opening_val += value_diff elif d.posting_date >= from_date and d.posting_date <= to_date: - if qty_diff > 0: + if flt(qty_diff, float_precision) >= 0: qty_dict.in_qty += qty_diff qty_dict.in_val += value_diff else: @@ -206,16 +208,15 @@ def get_item_warehouse_map(filters, sle): qty_dict.bal_qty += qty_diff qty_dict.bal_val += value_diff - iwb_map = filter_items_with_no_transactions(iwb_map) + iwb_map = filter_items_with_no_transactions(iwb_map, float_precision) return iwb_map -def filter_items_with_no_transactions(iwb_map): +def filter_items_with_no_transactions(iwb_map, float_precision): for (company, item, warehouse) in sorted(iwb_map): qty_dict = iwb_map[(company, item, warehouse)] no_transactions = True - float_precision = cint(frappe.db.get_default("float_precision")) or 3 for key, val in iteritems(qty_dict): val = flt(val, float_precision) qty_dict[key] = val From e8a651bc00ef72a1d81c8e5c6dccf44570b54211 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 23 Apr 2020 09:45:09 +0530 Subject: [PATCH 12/73] fix: BOM stock report (#21380) --- .../report/bom_stock_report/bom_stock_report.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py index 65f4d08459..75ebcbc971 100644 --- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py +++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py @@ -18,10 +18,10 @@ def get_columns(): """return columns""" columns = [ _("Item") + ":Link/Item:150", - _("Description") + "::500", - _("Qty per BOM Line") + ":Float:100", - _("Required Qty") + ":Float:100", - _("In Stock Qty") + ":Float:100", + _("Description") + "::300", + _("BOM Qty") + ":Float:160", + _("Required Qty") + ":Float:120", + _("In Stock Qty") + ":Float:120", _("Enough Parts to Build") + ":Float:200", ] @@ -59,13 +59,14 @@ def get_bom_stock(filters): bom_item.item_code, bom_item.description , bom_item.{qty_field}, - bom_item.{qty_field} * {qty_to_produce}, + bom_item.{qty_field} * {qty_to_produce} / bom.quantity, sum(ledger.actual_qty) as actual_qty, - sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce}))) + sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity))) FROM - {table} AS bom_item + `tabBOM` AS bom INNER JOIN {table} AS bom_item + ON bom.name = bom_item.parent LEFT JOIN `tabBin` AS ledger - ON bom_item.item_code = ledger.item_code + ON bom_item.item_code = ledger.item_code {conditions} WHERE bom_item.parent = '{bom}' and bom_item.parenttype='BOM' From 6fa6caf46cd983294ccb065f30a15fbb10949cd7 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 23 Apr 2020 09:46:29 +0530 Subject: [PATCH 13/73] fix: patch and validation message to fix target warehouse issue (#21371) --- erpnext/controllers/selling_controller.py | 9 +++ erpnext/patches.txt | 1 + ...ock_ledger_entries_for_target_warehouse.py | 71 +++++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 2b21ee8aa4..90ba8b3644 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -46,6 +46,7 @@ class SellingController(StockController): set_default_income_account_for_item(self) self.set_customer_address() self.validate_for_duplicate_items() + self.validate_target_warehouse() def set_missing_values(self, for_validate=False): @@ -403,6 +404,14 @@ class SellingController(StockController): else: chk_dupl_itm.append(f) + def validate_target_warehouse(self): + items = self.get("items") + (self.get("packed_items") or []) + + for d in items: + if d.get("target_warehouse") and d.get("warehouse") == d.get("target_warehouse"): + warehouse = frappe.bold(d.get("target_warehouse")) + frappe.throw(_("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same") + .format(d.idx, warehouse, warehouse)) def validate_items(self): # validate items to see if they have is_sales_item enabled diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9ef0b8d510..8478c1020d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -667,3 +667,4 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse diff --git a/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py new file mode 100644 index 0000000000..13e935b2d3 --- /dev/null +++ b/erpnext/patches/v12_0/repost_stock_ledger_entries_for_target_warehouse.py @@ -0,0 +1,71 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe + +def execute(): + warehouse_perm = frappe.get_all("User Permission", + fields=["count(*) as p_count", "is_default", "user"], filters={"allow": "Warehouse"}, group_by="user") + + if not warehouse_perm: + return + + execute_patch = False + for perm_data in warehouse_perm: + if perm_data.p_count == 1 or (perm_data.p_count > 1 and frappe.get_all("User Permission", + filters = {"user": perm_data.user, "allow": "warehouse", "is_default": 1}, limit=1)): + execute_patch = True + break + + if not execute_patch: return + + for doctype in ["Sales Invoice", "Delivery Note"]: + if not frappe.get_meta(doctype + ' Item').get_field("target_warehouse").hidden: continue + + cond = "" + if doctype == "Sales Invoice": + cond = " AND parent_doc.update_stock = 1" + + data = frappe.db.sql(""" SELECT parent_doc.name as name, child_doc.name as child_name + FROM + `tab{doctype}` parent_doc, `tab{doctype} Item` child_doc + WHERE + parent_doc.name = child_doc.parent AND parent_doc.docstatus < 2 + AND child_doc.target_warehouse is not null AND child_doc.target_warehouse != '' + AND child_doc.creation > '2020-04-16' {cond} + """.format(doctype=doctype, cond=cond), as_dict=1) + + if data: + names = [d.child_name for d in data] + frappe.db.sql(""" UPDATE `tab{0} Item` set target_warehouse = null + WHERE name in ({1}) """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + + frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null + WHERE parenttype = '{0}' and parent_detail_docname in ({1}) + """.format(doctype, ','.join(["%s"] * len(names) )), tuple(names)) + + parent_names = list(set([d.name for d in data])) + + for d in parent_names: + doc = frappe.get_doc(doctype, d) + if doc.docstatus != 1: continue + + doc.docstatus = 2 + doc.update_stock_ledger() + 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() + doc.make_gl_entries() + + if frappe.get_meta('Sales Order Item').get_field("target_warehouse").hidden: + frappe.db.sql(""" UPDATE `tabSales Order Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 """) + + frappe.db.sql(""" UPDATE `tabPacked Item` set target_warehouse = null + WHERE creation > '2020-04-16' and docstatus < 2 and parenttype = 'Sales Order' """) + + + From 168babfebc2c1e7023747bf9f2a5d4f6edef7782 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 23 Apr 2020 09:48:50 +0530 Subject: [PATCH 14/73] fix: bom update cost issue (#21372) * fix: bom update cost is not working * added test case for bom cost --- erpnext/manufacturing/doctype/bom/bom.py | 3 +- .../bom_update_tool/bom_update_tool.py | 7 ++-- .../bom_update_tool/test_bom_update_tool.py | 32 ++++++++++++++++++- .../production_plan/test_production_plan.py | 6 ++-- 4 files changed, 42 insertions(+), 6 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 6ccd12aed3..a83d193b6a 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -246,12 +246,13 @@ class BOM(WebsiteGenerator): if rate: d.rate = rate d.amount = flt(d.rate) * flt(d.qty) + d.db_update() if self.docstatus == 1: self.flags.ignore_validate_update_after_submit = True self.calculate_cost() if save: - self.save() + self.db_update() self.update_exploded_items() # update parent BOMs 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 2758a42371..e6c10ad12b 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -82,7 +82,7 @@ def enqueue_replace_bom(args): @frappe.whitelist() def enqueue_update_cost(): - frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost") + frappe.enqueue("erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_cost", timeout=40000) frappe.msgprint(_("Queued for updating latest price in all Bill of Materials. It may take a few minutes.")) def update_latest_price_in_all_boms(): @@ -98,6 +98,9 @@ def replace_bom(args): doc.replace_bom() def update_cost(): + frappe.db.auto_commit_on_many_writes = 1 bom_list = get_boms_in_bottom_up_order() for bom in bom_list: - frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) \ No newline at end of file + frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True) + + frappe.db.auto_commit_on_many_writes = 0 \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py index 154addf14e..ac9a409bcb 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/test_bom_update_tool.py @@ -5,6 +5,9 @@ from __future__ import unicode_literals import unittest import frappe +from erpnext.stock.doctype.item.test_item import create_item +from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom +from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost test_records = frappe.get_test_records('BOM') @@ -27,4 +30,31 @@ class TestBOMUpdateTool(unittest.TestCase): # reverse, as it affects other testcases update_tool.current_bom = bom_doc.name update_tool.new_bom = current_bom - update_tool.replace_bom() \ No newline at end of file + update_tool.replace_bom() + + def test_bom_cost(self): + for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: + item_doc = create_item(item, valuation_rate=100) + if item_doc.valuation_rate != 100.00: + frappe.db.set_value("Item", item_doc.name, "valuation_rate", 100) + + bom_no = frappe.db.get_value('BOM', {'item': 'BOM Cost Test Item 1'}, "name") + if not bom_no: + doc = make_bom(item = 'BOM Cost Test Item 1', + raw_materials =['BOM Cost Test Item 2', 'BOM Cost Test Item 3'], currency="INR") + else: + doc = frappe.get_doc("BOM", bom_no) + + self.assertEquals(doc.total_cost, 200) + + frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 200) + update_cost() + + doc.load_from_db() + self.assertEquals(doc.total_cost, 300) + + frappe.db.set_value("Item", "BOM Cost Test Item 2", "valuation_rate", 100) + update_cost() + + doc.load_from_db() + self.assertEquals(doc.total_cost, 200) diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py index f70c9cc43f..26f580db33 100644 --- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py @@ -192,9 +192,10 @@ def make_bom(**args): args = frappe._dict(args) bom = frappe.get_doc({ - 'doctype': "BOM", + 'doctype': 'BOM', 'is_default': 1, 'item': args.item, + 'currency': args.currency or 'USD', 'quantity': args.quantity or 1, 'company': args.company or '_Test Company' }) @@ -211,4 +212,5 @@ def make_bom(**args): }) bom.insert(ignore_permissions=True) - bom.submit() \ No newline at end of file + bom.submit() + return bom \ No newline at end of file From fa8396feb5114f9fd6b0739cc1052eaf908eb1ec Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 23 Apr 2020 10:34:57 +0530 Subject: [PATCH 15/73] fix: Budget against accounting dimensions (#21268) * fix: Budget warning against custom accounting dimension * fix: Codacy Co-authored-by: Nabin Hait --- erpnext/accounts/doctype/budget/budget.py | 66 +++++++++++-------- .../accounts/doctype/budget/test_budget.py | 37 ++++++----- 2 files changed, 61 insertions(+), 42 deletions(-) diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py index 084514cbfa..d93b6ffbaf 100644 --- a/erpnext/accounts/doctype/budget/budget.py +++ b/erpnext/accounts/doctype/budget/budget.py @@ -9,6 +9,7 @@ from frappe.utils import flt, getdate, add_months, get_last_day, fmt_money, nowd from frappe.model.naming import make_autoname from erpnext.accounts.utils import get_fiscal_year from frappe.model.document import Document +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions class BudgetError(frappe.ValidationError): pass class DuplicateBudgetError(frappe.ValidationError): pass @@ -98,30 +99,32 @@ def validate_expense_against_budget(args): if not (args.get('account') and args.get('cost_center')) and args.item_code: args.cost_center, args.account = get_item_details(args) - if not (args.cost_center or args.project) and not args.account: + if not args.account: return - for budget_against in ['project', 'cost_center']: + for budget_against in ['project', 'cost_center'] + get_accounting_dimensions(): if (args.get(budget_against) and args.account and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})): - if args.project and budget_against == 'project': - condition = "and b.project=%s" % frappe.db.escape(args.project) - args.budget_against_field = "Project" + doctype = frappe.unscrub(budget_against) - elif args.cost_center and budget_against == 'cost_center': - cc_lft, cc_rgt = frappe.db.get_value("Cost Center", args.cost_center, ["lft", "rgt"]) - condition = """and exists(select name from `tabCost Center` - where lft<=%s and rgt>=%s and name=b.cost_center)""" % (cc_lft, cc_rgt) - args.budget_against_field = "Cost Center" + if frappe.get_cached_value('DocType', doctype, 'is_tree'): + lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) + condition = """and exists(select name from `tab%s` + where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec + args.is_tree = True + else: + condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against))) + args.is_tree = False - args.budget_against = args.get(budget_against) + args.budget_against_field = budget_against + args.budget_against_doctype = doctype budget_records = frappe.db.sql(""" select b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution, ifnull(b.applicable_on_material_request, 0) as for_material_request, - ifnull(applicable_on_purchase_order,0) as for_purchase_order, + ifnull(applicable_on_purchase_order, 0) as for_purchase_order, ifnull(applicable_on_booking_actual_expenses,0) as for_actual_expenses, b.action_if_annual_budget_exceeded, b.action_if_accumulated_monthly_budget_exceeded, b.action_if_annual_budget_exceeded_on_mr, b.action_if_accumulated_monthly_budget_exceeded_on_mr, @@ -132,9 +135,7 @@ def validate_expense_against_budget(args): b.name=ba.parent and b.fiscal_year=%s and ba.account=%s and b.docstatus=1 {condition} - """.format(condition=condition, - budget_against_field=frappe.scrub(args.get("budget_against_field"))), - (args.fiscal_year, args.account), as_dict=True) + """.format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec if budget_records: validate_budget_records(args, budget_records) @@ -230,10 +231,10 @@ def get_ordered_amount(args, budget): def get_other_condition(args, budget, for_doc): condition = "expense_account = '%s'" % (args.expense_account) - budget_against_field = frappe.scrub(args.get("budget_against_field")) + budget_against_field = args.get("budget_against_field") if budget_against_field and args.get(budget_against_field): - condition += " and child.%s = '%s'" %(budget_against_field, args.get(budget_against_field)) + condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field)) if args.get('fiscal_year'): date_field = 'schedule_date' if for_doc == 'Material Request' else 'transaction_date' @@ -246,19 +247,30 @@ def get_other_condition(args, budget, for_doc): return condition def get_actual_expense(args): + if not args.budget_against_doctype: + args.budget_against_doctype = frappe.unscrub(args.budget_against_field) + + budget_against_field = args.get('budget_against_field') condition1 = " and gle.posting_date <= %(month_end_date)s" \ if args.get("month_end_date") else "" - if args.budget_against_field == "Cost Center": - lft_rgt = frappe.db.get_value(args.budget_against_field, - args.budget_against, ["lft", "rgt"], as_dict=1) + + if args.is_tree: + lft_rgt = frappe.db.get_value(args.budget_against_doctype, + args.get(budget_against_field), ["lft", "rgt"], as_dict=1) + args.update(lft_rgt) - condition2 = """and exists(select name from `tabCost Center` - where lft>=%(lft)s and rgt<=%(rgt)s and name=gle.cost_center)""" - elif args.budget_against_field == "Project": - condition2 = "and exists(select name from `tabProject` where name=gle.project and gle.project = %(budget_against)s)" + condition2 = """and exists(select name from `tab{doctype}` + where lft>=%(lft)s and rgt<=%(rgt)s + and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec + budget_against_field=budget_against_field) + else: + condition2 = """and exists(select name from `tab{doctype}` + where name=gle.{budget_against} and + gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype, + budget_against = budget_against_field) - return flt(frappe.db.sql(""" + amount = flt(frappe.db.sql(""" select sum(gle.debit) - sum(gle.credit) from `tabGL Entry` gle where gle.account=%(account)s @@ -267,7 +279,9 @@ def get_actual_expense(args): and gle.company=%(company)s and gle.docstatus=1 {condition2} - """.format(condition1=condition1, condition2=condition2), (args))[0][0]) + """.format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec + + return amount def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget): distribution = {} diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py index 33aefd67d1..9c19791d29 100644 --- a/erpnext/accounts/doctype/budget/test_budget.py +++ b/erpnext/accounts/doctype/budget/test_budget.py @@ -13,7 +13,7 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ class TestBudget(unittest.TestCase): def test_monthly_budget_crossed_ignore(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -26,7 +26,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_crossed_stop1(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -41,7 +41,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_exception_approver_role(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -114,7 +114,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_crossed_stop2(self): - set_total_expense_zero("2013-02-28", "Project") + set_total_expense_zero("2013-02-28", "project") budget = make_budget(budget_against="Project") @@ -129,7 +129,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_yearly_budget_crossed_stop1(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -141,7 +141,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_yearly_budget_crossed_stop2(self): - set_total_expense_zero("2013-02-28", "Project") + set_total_expense_zero("2013-02-28", "project") budget = make_budget(budget_against="Project") @@ -153,7 +153,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_on_cancellation1(self): - set_total_expense_zero("2013-02-28", "Cost Center") + set_total_expense_zero("2013-02-28", "cost_center") budget = make_budget(budget_against="Cost Center") @@ -177,7 +177,7 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_on_cancellation2(self): - set_total_expense_zero("2013-02-28", "Project") + set_total_expense_zero("2013-02-28", "project") budget = make_budget(budget_against="Project") @@ -201,8 +201,8 @@ class TestBudget(unittest.TestCase): budget.cancel() def test_monthly_budget_against_group_cost_center(self): - set_total_expense_zero("2013-02-28", "Cost Center") - set_total_expense_zero("2013-02-28", "Cost Center", "_Test Cost Center 2 - _TC") + set_total_expense_zero("2013-02-28", "cost_center") + set_total_expense_zero("2013-02-28", "cost_center", "_Test Cost Center 2 - _TC") budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC") frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop") @@ -241,25 +241,30 @@ class TestBudget(unittest.TestCase): def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None): - if budget_against_field == "Project": + if budget_against_field == "project": budget_against = "_Test Project" else: budget_against = budget_against_CC or "_Test Cost Center - _TC" - existing_expense = get_actual_expense(frappe._dict({ + + args = frappe._dict({ "account": "_Test Account Cost for Goods Sold - _TC", "cost_center": "_Test Cost Center - _TC", "monthly_end_date": posting_date, "company": "_Test Company", "fiscal_year": "_Test Fiscal Year 2013", "budget_against_field": budget_against_field, - "budget_against": budget_against - })) + }) + + if not args.get(budget_against_field): + args[budget_against_field] = budget_against + + existing_expense = get_actual_expense(args) if existing_expense: - if budget_against_field == "Cost Center": + if budget_against_field == "cost_center": make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date="2013-02-28", submit=True) - elif budget_against_field == "Project": + elif budget_against_field == "project": make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date="2013-02-28") From b1e68bd022f4ccd4860220a1a1fcb6fee90f1016 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 23 Apr 2020 12:19:30 +0530 Subject: [PATCH 16/73] fix: Rename bank account type doctypes (#21179) * fix: Rename bank account type doctypes Co-authored-by: Saqib --- .../account_subtype/account_subtype.json | 134 ------------------ .../account_subtype/test_account_subtype.js | 23 --- .../doctype/account_type/account_type.js | 8 -- .../doctype/account_type/account_type.json | 134 ------------------ .../doctype/account_type/test_account_type.py | 9 -- .../doctype/bank_account/bank_account.json | 6 +- .../__init__.py | 0 .../bank_account_subtype.js} | 2 +- .../bank_account_subtype.json | 134 ++++++++++++++++++ .../bank_account_subtype.py} | 2 +- .../test_bank_account_subtype.js} | 6 +- .../test_bank_account_subtype.py} | 2 +- .../__init__.py | 0 .../bank_account_type/bank_account_type.js | 8 ++ .../bank_account_type/bank_account_type.json | 68 +++++++++ .../bank_account_type.py} | 5 +- .../test_bank_account_type.py | 10 ++ .../doctype/plaid_settings/plaid_settings.py | 8 +- .../plaid_settings/test_plaid_settings.py | 12 +- erpnext/patches.txt | 1 + .../v12_0/rename_account_type_doctype.py | 7 + 21 files changed, 250 insertions(+), 329 deletions(-) delete mode 100644 erpnext/accounts/doctype/account_subtype/account_subtype.json delete mode 100644 erpnext/accounts/doctype/account_subtype/test_account_subtype.js delete mode 100644 erpnext/accounts/doctype/account_type/account_type.js delete mode 100644 erpnext/accounts/doctype/account_type/account_type.json delete mode 100644 erpnext/accounts/doctype/account_type/test_account_type.py rename erpnext/accounts/doctype/{account_subtype => bank_account_subtype}/__init__.py (100%) rename erpnext/accounts/doctype/{account_subtype/account_subtype.js => bank_account_subtype/bank_account_subtype.js} (77%) create mode 100644 erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.json rename erpnext/accounts/doctype/{account_type/account_type.py => bank_account_subtype/bank_account_subtype.py} (86%) rename erpnext/accounts/doctype/{account_type/test_account_type.js => bank_account_subtype/test_bank_account_subtype.js} (69%) rename erpnext/accounts/doctype/{account_subtype/test_account_subtype.py => bank_account_subtype/test_bank_account_subtype.py} (78%) rename erpnext/accounts/doctype/{account_type => bank_account_type}/__init__.py (100%) create mode 100644 erpnext/accounts/doctype/bank_account_type/bank_account_type.js create mode 100644 erpnext/accounts/doctype/bank_account_type/bank_account_type.json rename erpnext/accounts/doctype/{account_subtype/account_subtype.py => bank_account_type/bank_account_type.py} (60%) create mode 100644 erpnext/accounts/doctype/bank_account_type/test_bank_account_type.py create mode 100644 erpnext/patches/v12_0/rename_account_type_doctype.py diff --git a/erpnext/accounts/doctype/account_subtype/account_subtype.json b/erpnext/accounts/doctype/account_subtype/account_subtype.json deleted file mode 100644 index 6b1f2a2526..0000000000 --- a/erpnext/accounts/doctype/account_subtype/account_subtype.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:account_subtype", - "beta": 0, - "creation": "2018-10-25 15:46:08.054586", - "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, - "fieldname": "account_subtype", - "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": "Account Subtype", - "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": 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-10-25 15:47:03.841390", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Account Subtype", - "name_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, - "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, - "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, - "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": 0, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/account_subtype/test_account_subtype.js b/erpnext/accounts/doctype/account_subtype/test_account_subtype.js deleted file mode 100644 index 5646763bbd..0000000000 --- a/erpnext/accounts/doctype/account_subtype/test_account_subtype.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: Account Subtype", function (assert) { - let done = assert.async(); - - // number of asserts - assert.expect(1); - - frappe.run_serially([ - // insert a new Account Subtype - () => frappe.tests.make('Account Subtype', [ - // values to be set - {key: 'value'} - ]), - () => { - assert.equal(cur_frm.doc.key, 'value'); - }, - () => done() - ]); - -}); diff --git a/erpnext/accounts/doctype/account_type/account_type.js b/erpnext/accounts/doctype/account_type/account_type.js deleted file mode 100644 index 858b56c077..0000000000 --- a/erpnext/accounts/doctype/account_type/account_type.js +++ /dev/null @@ -1,8 +0,0 @@ -// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Account Type', { - refresh: function() { - - } -}); diff --git a/erpnext/accounts/doctype/account_type/account_type.json b/erpnext/accounts/doctype/account_type/account_type.json deleted file mode 100644 index 6b8f724b40..0000000000 --- a/erpnext/accounts/doctype/account_type/account_type.json +++ /dev/null @@ -1,134 +0,0 @@ -{ - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:account_type", - "beta": 0, - "creation": "2018-10-25 15:45:45.789963", - "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, - "fieldname": "account_type", - "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": "Account Type", - "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": 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-10-25 15:46:51.042604", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Account Type", - "name_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, - "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, - "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, - "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": 0, - "track_seen": 0, - "track_views": 0 -} \ No newline at end of file diff --git a/erpnext/accounts/doctype/account_type/test_account_type.py b/erpnext/accounts/doctype/account_type/test_account_type.py deleted file mode 100644 index 824c2f66ae..0000000000 --- a/erpnext/accounts/doctype/account_type/test_account_type.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 TestAccountType(unittest.TestCase): - pass diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json index aa9c434db0..65a0a5138c 100644 --- a/erpnext/accounts/doctype/bank_account/bank_account.json +++ b/erpnext/accounts/doctype/bank_account/bank_account.json @@ -64,13 +64,13 @@ "fieldname": "account_type", "fieldtype": "Link", "label": "Account Type", - "options": "Account Type" + "options": "Bank Account Type" }, { "fieldname": "account_subtype", "fieldtype": "Link", "label": "Account Subtype", - "options": "Account Subtype" + "options": "Bank Account Subtype" }, { "fieldname": "column_break_7", @@ -200,7 +200,7 @@ } ], "links": [], - "modified": "2020-01-30 20:42:26.458316", + "modified": "2020-04-06 21:00:45.379804", "modified_by": "Administrator", "module": "Accounts", "name": "Bank Account", diff --git a/erpnext/accounts/doctype/account_subtype/__init__.py b/erpnext/accounts/doctype/bank_account_subtype/__init__.py similarity index 100% rename from erpnext/accounts/doctype/account_subtype/__init__.py rename to erpnext/accounts/doctype/bank_account_subtype/__init__.py diff --git a/erpnext/accounts/doctype/account_subtype/account_subtype.js b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.js similarity index 77% rename from erpnext/accounts/doctype/account_subtype/account_subtype.js rename to erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.js index 30144adeea..f0456651c8 100644 --- a/erpnext/accounts/doctype/account_subtype/account_subtype.js +++ b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.js @@ -1,7 +1,7 @@ // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on('Account Subtype', { +frappe.ui.form.on('Bank Account Subtype', { refresh: function() { } diff --git a/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.json b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.json new file mode 100644 index 0000000000..f875db8ca1 --- /dev/null +++ b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.json @@ -0,0 +1,134 @@ +{ + "allow_copy": 0, + "allow_events_in_timeline": 0, + "allow_guest_to_view": 0, + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:account_subtype", + "beta": 0, + "creation": "2018-10-25 15:46:08.054586", + "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, + "fieldname": "account_subtype", + "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": "Account Subtype", + "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": 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-10-25 15:47:03.841390", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Bank Account Subtype", + "name_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, + "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, + "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, + "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": 0, + "track_seen": 0, + "track_views": 0 +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/account_type/account_type.py b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.py similarity index 86% rename from erpnext/accounts/doctype/account_type/account_type.py rename to erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.py index 3e6429318b..ab52c4af77 100644 --- a/erpnext/accounts/doctype/account_type/account_type.py +++ b/erpnext/accounts/doctype/bank_account_subtype/bank_account_subtype.py @@ -5,5 +5,5 @@ from __future__ import unicode_literals from frappe.model.document import Document -class AccountType(Document): +class BankAccountSubtype(Document): pass diff --git a/erpnext/accounts/doctype/account_type/test_account_type.js b/erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.js similarity index 69% rename from erpnext/accounts/doctype/account_type/test_account_type.js rename to erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.js index 76e434f4ab..f59999845a 100644 --- a/erpnext/accounts/doctype/account_type/test_account_type.js +++ b/erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.js @@ -2,15 +2,15 @@ // rename this file from _test_[name] to test_[name] to activate // and remove above this line -QUnit.test("test: Account Type", function (assert) { +QUnit.test("test: Bank Account Subtype", function (assert) { let done = assert.async(); // number of asserts assert.expect(1); frappe.run_serially([ - // insert a new Account Type - () => frappe.tests.make('Account Type', [ + // insert a new Bank Account Subtype + () => frappe.tests.make('Bank Account Subtype', [ // values to be set {key: 'value'} ]), diff --git a/erpnext/accounts/doctype/account_subtype/test_account_subtype.py b/erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.py similarity index 78% rename from erpnext/accounts/doctype/account_subtype/test_account_subtype.py rename to erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.py index c37b5b9db7..ca3addc979 100644 --- a/erpnext/accounts/doctype/account_subtype/test_account_subtype.py +++ b/erpnext/accounts/doctype/bank_account_subtype/test_bank_account_subtype.py @@ -5,5 +5,5 @@ from __future__ import unicode_literals import unittest -class TestAccountSubtype(unittest.TestCase): +class TestBankAccountSubtype(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/account_type/__init__.py b/erpnext/accounts/doctype/bank_account_type/__init__.py similarity index 100% rename from erpnext/accounts/doctype/account_type/__init__.py rename to erpnext/accounts/doctype/bank_account_type/__init__.py diff --git a/erpnext/accounts/doctype/bank_account_type/bank_account_type.js b/erpnext/accounts/doctype/bank_account_type/bank_account_type.js new file mode 100644 index 0000000000..4cfabe3d1d --- /dev/null +++ b/erpnext/accounts/doctype/bank_account_type/bank_account_type.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Bank Account Type', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/accounts/doctype/bank_account_type/bank_account_type.json b/erpnext/accounts/doctype/bank_account_type/bank_account_type.json new file mode 100644 index 0000000000..5a297cc2f9 --- /dev/null +++ b/erpnext/accounts/doctype/bank_account_type/bank_account_type.json @@ -0,0 +1,68 @@ +{ + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:account_type", + "creation": "2018-10-25 15:45:45.789963", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "account_type" + ], + "fields": [ + { + "fieldname": "account_type", + "fieldtype": "Data", + "label": "Account Type", + "unique": 1 + } + ], + "links": [], + "modified": "2020-04-10 21:13:09.137898", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Bank Account Type", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/account_subtype/account_subtype.py b/erpnext/accounts/doctype/bank_account_type/bank_account_type.py similarity index 60% rename from erpnext/accounts/doctype/account_subtype/account_subtype.py rename to erpnext/accounts/doctype/bank_account_type/bank_account_type.py index 46c45cc733..b7dc0e0dc3 100644 --- a/erpnext/accounts/doctype/account_subtype/account_subtype.py +++ b/erpnext/accounts/doctype/bank_account_type/bank_account_type.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- -# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors +# Copyright (c) 2020, 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 AccountSubtype(Document): +class BankAccountType(Document): pass diff --git a/erpnext/accounts/doctype/bank_account_type/test_bank_account_type.py b/erpnext/accounts/doctype/bank_account_type/test_bank_account_type.py new file mode 100644 index 0000000000..f04725a2e5 --- /dev/null +++ b/erpnext/accounts/doctype/bank_account_type/test_bank_account_type.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestBankAccountType(unittest.TestCase): + pass diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index 7083950c56..b4a5bd11a0 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -67,11 +67,11 @@ def add_bank_accounts(response, bank, company): frappe.throw(_("Please setup a default bank account for company {0}").format(company)) for account in response["accounts"]: - acc_type = frappe.db.get_value("Account Type", account["type"]) + acc_type = frappe.db.get_value("Bank Account Type", account["type"]) if not acc_type: add_account_type(account["type"]) - acc_subtype = frappe.db.get_value("Account Subtype", account["subtype"]) + acc_subtype = frappe.db.get_value("Bank Account Subtype", account["subtype"]) if not acc_subtype: add_account_subtype(account["subtype"]) @@ -106,7 +106,7 @@ def add_bank_accounts(response, bank, company): def add_account_type(account_type): try: frappe.get_doc({ - "doctype": "Account Type", + "doctype": "Bank Account Type", "account_type": account_type }).insert() except Exception: @@ -116,7 +116,7 @@ def add_account_type(account_type): def add_account_subtype(account_subtype): try: frappe.get_doc({ - "doctype": "Account Subtype", + "doctype": "Bank Account Subtype", "account_subtype": account_subtype }).insert() except Exception: diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index 29e8fa4fec..1a063d6b6f 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -23,11 +23,11 @@ class TestPlaidSettings(unittest.TestCase): for ba in frappe.get_all("Bank Account"): frappe.get_doc("Bank Account", ba.name).delete() - for at in frappe.get_all("Account Type"): - frappe.get_doc("Account Type", at.name).delete() + for at in frappe.get_all("Bank Account Type"): + frappe.get_doc("Bank Account Type", at.name).delete() - for ast in frappe.get_all("Account Subtype"): - frappe.get_doc("Account Subtype", ast.name).delete() + for ast in frappe.get_all("Bank Account Subtype"): + frappe.get_doc("Bank Account Subtype", ast.name).delete() def test_plaid_disabled(self): frappe.db.set_value("Plaid Settings", None, "enabled", 0) @@ -35,11 +35,11 @@ class TestPlaidSettings(unittest.TestCase): def test_add_account_type(self): add_account_type("brokerage") - self.assertEqual(frappe.get_doc("Account Type", "brokerage").name, "brokerage") + self.assertEqual(frappe.get_doc("Bank Account Type", "brokerage").name, "brokerage") def test_add_account_subtype(self): add_account_subtype("loan") - self.assertEqual(frappe.get_doc("Account Subtype", "loan").name, "loan") + self.assertEqual(frappe.get_doc("Bank Account Subtype", "loan").name, "loan") def test_default_bank_account(self): if not frappe.db.exists("Bank", "Citi"): diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8478c1020d..765f911bef 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -662,6 +662,7 @@ erpnext.patches.v12_0.create_irs_1099_field_united_states erpnext.patches.v12_0.move_bank_account_swift_number_to_bank erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom +erpnext.patches.v12_0.rename_account_type_doctype erpnext.patches.v12_0.recalculate_requested_qty_in_bin erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity diff --git a/erpnext/patches/v12_0/rename_account_type_doctype.py b/erpnext/patches/v12_0/rename_account_type_doctype.py new file mode 100644 index 0000000000..ffb4e937b1 --- /dev/null +++ b/erpnext/patches/v12_0/rename_account_type_doctype.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.rename_doc('DocType', 'Account Type', 'Bank Account Type', force=True) + frappe.rename_doc('DocType', 'Account Subtype', 'Bank Account Subtype', force=True) + frappe.reload_doc('accounts', 'doctype', 'bank_account') \ No newline at end of file From 2daf6dad2e17e201e5ca14df5b9cade1f4cd698c Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 23 Apr 2020 13:51:29 +0530 Subject: [PATCH 17/73] fix: Allow creation of loan security pledge from Loan Application for old prices --- .../doctype/loan_application/loan_application.js | 2 +- .../doctype/loan_security_pledge/loan_security_pledge.py | 3 ++- .../doctype/loan_security_price/loan_security_price.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.js b/erpnext/loan_management/doctype/loan_application/loan_application.js index aba5f4260c..6cf47bf85c 100644 --- a/erpnext/loan_management/doctype/loan_application/loan_application.js +++ b/erpnext/loan_management/doctype/loan_application/loan_application.js @@ -31,7 +31,7 @@ frappe.ui.form.on('Loan Application', { add_toolbar_buttons: function(frm) { if (frm.doc.status == "Approved") { - if (frm.doc.is_secured) { + if (frm.doc.is_secured_loan) { frappe.db.get_value("Loan Security Pledge", {"loan_application": frm.doc.name, "docstatus": 1}, "name", (r) => { if (!r) { frm.add_custom_button(__('Loan Security Pledge'), function() { diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index b405ccae55..eb6135868d 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -30,7 +30,8 @@ class LoanSecurityPledge(Document): if not pledge.qty and not pledge.amount: frappe.throw(_("Qty or Amount is mandatroy for loan security")) - pledge.loan_security_price = get_loan_security_price(pledge.loan_security) + if not (self.loan_application and pledge.loan_security_price): + pledge.loan_security_price = get_loan_security_price(pledge.loan_security) if not pledge.qty: pledge.qty = cint(pledge.amount/pledge.loan_security_price) diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py index 2855b52610..32d81afed5 100644 --- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py +++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.py @@ -37,7 +37,7 @@ def get_loan_security_price(loan_security, valid_time=None): }, 'loan_security_price') if not loan_security_price: - frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) + frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(loan_security))) else: return loan_security_price From 39911b40f0df220121ac4bd3289b2b156cb4aa2f Mon Sep 17 00:00:00 2001 From: Anupam K Date: Wed, 22 Apr 2020 18:37:15 +0530 Subject: [PATCH 18/73] setting end date in email campaign --- .../doctype/email_campaign/email_campaign.py | 2 +- erpnext/patches.txt | 1 + erpnext/patches/v13_0/__init__.py | 0 ...e_end_date_and_status_in_email_campaign.py | 24 +++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/__init__.py create mode 100644 erpnext/patches/v13_0/update_end_date_and_status_in_email_campaign.py diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 00a4bd1a32..8f60ecf621 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -27,7 +27,7 @@ class EmailCampaign(Document): for entry in campaign.get("campaign_schedules"): send_after_days.append(entry.send_after_days) try: - end_date = add_days(getdate(self.start_date), max(send_after_days)) + self.end_date = add_days(getdate(self.start_date), max(send_after_days)) except ValueError: frappe.throw(_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name)) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9ef0b8d510..d17503be9d 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -667,3 +667,4 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v13_0.update_end_date_and_status_in_email_campaign \ No newline at end of file diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/patches/v13_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v13_0/update_end_date_and_status_in_email_campaign.py new file mode 100644 index 0000000000..db71a735de --- /dev/null +++ b/erpnext/patches/v13_0/update_end_date_and_status_in_email_campaign.py @@ -0,0 +1,24 @@ +from __future__ import unicode_literals +import frappe +from frappe.utils import add_days, getdate, today + +def execute(): + if frappe.db.exists('DocType', 'Email Campaign'): + email_campaign = frappe.get_all('Email Campaign') + for campaign in email_campaign: + doc = frappe.get_doc("Email Campaign",campaign["name"]) + send_after_days = [] + + camp = frappe.get_doc("Campaign", doc.campaign_name) + for entry in camp.get("campaign_schedules"): + send_after_days.append(entry.send_after_days) + if send_after_days: + end_date = add_days(getdate(doc.start_date), max(send_after_days)) + doc.db_set("end_date", end_date) + today_date = getdate(today()) + if doc.start_date > today_date: + doc.db_set("status", "Scheduled") + elif end_date >= today_date: + doc.db_set("status", "In Progress") + elif end_date < today_date: + doc.db_set("status", "Completed") \ No newline at end of file From 1e4c28e99d2b675408bb8b3cda743852b6aedc66 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 23 Apr 2020 15:15:52 +0530 Subject: [PATCH 19/73] fix: quotation have expired status even if sales order exists --- erpnext/patches.txt | 1 + .../v12_0/fix_quotation_expired_status.py | 37 +++++++++++++++++++ .../selling/doctype/quotation/quotation.py | 23 +++++++++--- 3 files changed, 55 insertions(+), 6 deletions(-) create mode 100644 erpnext/patches/v12_0/fix_quotation_expired_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9ef0b8d510..39ae8e7447 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -667,3 +667,4 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.fix_quotation_expired_status diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py new file mode 100644 index 0000000000..a0320feb7b --- /dev/null +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -0,0 +1,37 @@ +import frappe + + +def execute(): + # fixes status of quotations which have status 'Expired' despite having valid sales order created + + # filter out submitted expired quotations which has sales order created + cond = "qo.docstatus = 1 and qo.status = 'Expired'" + invalid_so_against_quo = """ + SELECT + so.name FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so_item.docstatus = 1 and so.docstatus = 1 + and so_item.parent = so.name + and so_item.prevdoc_docname = qo.name + and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired + + frappe.db.sql( + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({invalid_so_against_quo})""" + .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo), + (nowdate()) + ) + + valid_so_against_quo = """ + SELECT + so.name FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so_item.docstatus = 1 and so.docstatus = 1 + and so_item.parent = so.name + and so_item.prevdoc_docname = qo.name + and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired + + frappe.db.sql( + """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and not exists({valid_so_against_quo})""" + .format(cond=cond, valid_so_against_quo=valid_so_against_quo), + (nowdate()) + ) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 7c47b8ac51..7cfec5a046 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -193,12 +193,23 @@ 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 - `status` not in ('Ordered', 'Expired', 'Lost', 'Cancelled') AND `valid_till` < %s - """, (nowdate())) + # filter out submitted non expired quotations whose validity has been ended + cond = "qo.docstatus = 1 and qo.status != 'Expired' and qo.valid_till < %s" + # check if those QUO have SO against it + so_against_quo = """ + SELECT + so.name FROM `tabSales Order` so, `tabSales Order Item` so_item + WHERE + so_item.docstatus = 1 and so.docstatus = 1 + and so_item.parent = so.name + and so_item.prevdoc_docname = qo.name""" + + # if not exists any SO, set status as Expired + frappe.db.sql( + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({so_against_quo})""" + .format(cond=cond, so_against_quo=so_against_quo), + (nowdate()) + ) @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): From 0f7bac26e615e83066fc077108caf144c25c9472 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 23 Apr 2020 15:29:12 +0530 Subject: [PATCH 20/73] setting end date in email campaign --- erpnext/patches.txt | 2 +- .../update_end_date_and_status_in_email_campaign.py | 0 erpnext/patches/v13_0/__init__.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename erpnext/patches/{v13_0 => v12_0}/update_end_date_and_status_in_email_campaign.py (100%) delete mode 100644 erpnext/patches/v13_0/__init__.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d17503be9d..361c1d5689 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -667,4 +667,4 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list -erpnext.patches.v13_0.update_end_date_and_status_in_email_campaign \ No newline at end of file +erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign \ No newline at end of file diff --git a/erpnext/patches/v13_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py similarity index 100% rename from erpnext/patches/v13_0/update_end_date_and_status_in_email_campaign.py rename to erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 525204bc50a2100300aac0b83324c1b3f8c50ec1 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Thu, 23 Apr 2020 16:07:36 +0530 Subject: [PATCH 21/73] feat: Payment allocation based on payment terms (#20945) * feat: Payment allocation based on payment terms * fix: Add desccription for checkbox Co-authored-by: Nabin Hait --- .../doctype/payment_entry/payment_entry.js | 17 +- .../doctype/payment_entry/payment_entry.py | 78 ++++- .../payment_entry/test_payment_entry.py | 62 +++- .../payment_entry_reference.json | 298 ++--------------- .../payment_schedule/payment_schedule.json | 305 +++++------------- .../payment_terms_template.json | 220 ++++--------- .../accounts_receivable.py | 17 +- erpnext/controllers/accounts_controller.py | 2 +- 8 files changed, 328 insertions(+), 671 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index b7c97a776e..05f01475c1 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -106,6 +106,21 @@ frappe.ui.form.on('Payment Entry', { }; }); + frm.set_query('payment_term', 'references', function(frm, cdt, cdn) { + const child = locals[cdt][cdn]; + if (in_list(['Purchase Invoice', 'Sales Invoice'], child.reference_doctype) && child.reference_name) { + let payment_term_list = frappe.get_list('Payment Schedule', {'parent': child.reference_name}); + + payment_term_list = payment_term_list.map(pt => pt.payment_term); + + return { + filters: { + 'name': ['in', payment_term_list] + } + } + } + }); + frm.set_query("reference_name", "references", function(doc, cdt, cdn) { const child = locals[cdt][cdn]; const filters = {"docstatus": 1, "company": doc.company}; @@ -1033,4 +1048,4 @@ frappe.ui.form.on('Payment Entry', { }); } }, -}) +}) \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index a453e95b2b..b53e68ff73 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -71,9 +71,9 @@ class PaymentEntry(AccountsController): self.update_outstanding_amounts() self.update_advance_paid() self.update_expense_claim() + self.update_payment_schedule() self.set_status() - def on_cancel(self): self.setup_party_account_field() self.make_gl_entries(cancel=1) @@ -81,6 +81,7 @@ class PaymentEntry(AccountsController): self.update_advance_paid() self.update_expense_claim() self.delink_advance_entry_references() + self.update_payment_schedule(cancel=1) self.set_payment_req_status() self.set_status() @@ -94,10 +95,10 @@ class PaymentEntry(AccountsController): def validate_duplicate_entry(self): reference_names = [] for d in self.get("references"): - if (d.reference_doctype, d.reference_name) in reference_names: + if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names: frappe.throw(_("Row #{0}: Duplicate entry in References {1} {2}") .format(d.idx, d.reference_doctype, d.reference_name)) - reference_names.append((d.reference_doctype, d.reference_name)) + reference_names.append((d.reference_doctype, d.reference_name, d.payment_term)) def set_bank_account_data(self): if self.bank_account: @@ -285,6 +286,36 @@ class PaymentEntry(AccountsController): frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry") .format(d.reference_name, dr_or_cr)) + def update_payment_schedule(self, cancel=0): + invoice_payment_amount_map = {} + invoice_paid_amount_map = {} + + for reference in self.get('references'): + if reference.payment_term and reference.reference_name: + key = (reference.payment_term, reference.reference_name) + invoice_payment_amount_map.setdefault(key, 0.0) + invoice_payment_amount_map[key] += reference.allocated_amount + + if not invoice_paid_amount_map.get(reference.reference_name): + payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name}, + fields=['paid_amount', 'payment_amount', 'payment_term']) + for term in payment_schedule: + invoice_key = (term.payment_term, reference.reference_name) + invoice_paid_amount_map.setdefault(invoice_key, {}) + invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount + + for key, amount in iteritems(invoice_payment_amount_map): + if cancel: + frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s + WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + else: + outstanding = invoice_paid_amount_map.get(key)['outstanding'] + if amount > outstanding: + frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0])) + + frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s + WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0])) + def set_status(self): if self.docstatus == 2: self.status = 'Cancelled' @@ -1012,15 +1043,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked(): frappe.msgprint(_('{0} is on hold till {1}').format(doc.name, doc.release_date)) else: - pe.append("references", { - 'reference_doctype': dt, - 'reference_name': dn, - "bill_no": doc.get("bill_no"), - "due_date": doc.get("due_date"), - 'total_amount': grand_total, - 'outstanding_amount': outstanding_amount, - 'allocated_amount': outstanding_amount - }) + if (doc.doctype in ('Sales Invoice', 'Purchase Invoice') + and frappe.get_value('Payment Terms Template', + {'name': doc.payment_terms_template}, 'allocate_payment_based_on_payment_terms')): + + for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount): + pe.append('references', reference) + else: + pe.append("references", { + 'reference_doctype': dt, + 'reference_name': dn, + "bill_no": doc.get("bill_no"), + "due_date": doc.get("due_date"), + 'total_amount': grand_total, + 'outstanding_amount': outstanding_amount, + 'allocated_amount': outstanding_amount + }) pe.setup_party_account_field() pe.set_missing_values() @@ -1029,6 +1067,22 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.set_amounts() return pe +def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount): + references = [] + for payment_term in payment_schedule: + references.append({ + 'reference_doctype': dt, + 'reference_name': dn, + 'bill_no': doc.get('bill_no'), + 'due_date': doc.get('due_date'), + 'total_amount': grand_total, + 'outstanding_amount': outstanding_amount, + 'payment_term': payment_term.payment_term, + 'allocated_amount': flt(payment_term.payment_amount - payment_term.paid_amount, + payment_term.precision('payment_amount')) + }) + + return references def get_paid_amount(dt, dn, party_type, party, account, due_date): if party_type=="Customer": diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py index a25e0e32c8..4c7d933476 100644 --- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py @@ -171,6 +171,32 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(flt(outstanding_amount), 100) self.assertEqual(status, 'Unpaid') + def test_payment_entry_against_payment_terms(self): + si = create_sales_invoice(do_not_save=1, qty=1, rate=200) + create_payment_terms_template() + si.payment_terms_template = 'Test Receivable Template' + + si.append('taxes', { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 18 + }) + si.save() + + si.submit() + + pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC") + pe.submit() + si.load_from_db() + + self.assertEqual(pe.references[0].payment_term, 'Basic Amount Receivable') + self.assertEqual(pe.references[1].payment_term, 'Tax Receivable') + self.assertEqual(si.payment_schedule[0].paid_amount, 200.0) + self.assertEqual(si.payment_schedule[1].paid_amount, 36.0) + + def test_payment_against_purchase_invoice_to_check_status(self): pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC", currency="USD", conversion_rate=50) @@ -609,4 +635,38 @@ class TestPaymentEntry(unittest.TestCase): self.assertEqual(expected_party_account_balance, party_account_balance) accounts_settings.allow_cost_center_in_entry_of_bs_account = 0 - accounts_settings.save() \ No newline at end of file + accounts_settings.save() + +def create_payment_terms_template(): + + create_payment_term('Basic Amount Receivable') + create_payment_term('Tax Receivable') + + if not frappe.db.exists('Payment Terms Template', 'Test Receivable Template'): + payment_term_template = frappe.get_doc({ + 'doctype': 'Payment Terms Template', + 'template_name': 'Test Receivable Template', + 'allocate_payment_based_on_payment_terms': 1, + 'terms': [{ + 'doctype': 'Payment Terms Template Detail', + 'payment_term': 'Basic Amount Receivable', + 'invoice_portion': 84.746, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 1 + }, + { + 'doctype': 'Payment Terms Template Detail', + 'payment_term': 'Tax Receivable', + 'invoice_portion': 15.254, + 'credit_days_based_on': 'Day(s) after invoice date', + 'credit_days': 2 + }] + }).insert() + + +def create_payment_term(name): + if not frappe.db.exists('Payment Term', name): + frappe.get_doc({ + 'doctype': 'Payment Term', + 'payment_term_name': name + }).insert() \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json index b6a664393e..8f5e9fbc28 100644 --- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json +++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json @@ -1,343 +1,107 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, + "actions": [], "creation": "2016-06-01 16:55:32.196722", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "reference_doctype", + "reference_name", + "due_date", + "bill_no", + "payment_term", + "column_break_4", + "total_amount", + "outstanding_amount", + "allocated_amount", + "exchange_rate" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "reference_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": "Type", - "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": 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": 2, - "fetch_if_empty": 0, "fieldname": "reference_name", "fieldtype": "Dynamic Link", - "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": "Name", - "length": 0, - "no_copy": 0, "options": "reference_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": 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, - "fetch_if_empty": 0, "fieldname": "due_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": "Due Date", - "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": "", - "fetch_if_empty": 0, "fieldname": "bill_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": "Supplier Invoice No", - "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 + "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_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 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "total_amount", "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": "Total Amount", - "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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, - "fetch_if_empty": 0, "fieldname": "outstanding_amount", "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": "Outstanding", - "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": 2, - "fetch_if_empty": 0, "fieldname": "allocated_amount", "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": "Allocated", - "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": "Allocated" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "depends_on": "eval:(doc.reference_doctype=='Purchase Invoice')", - "fetch_if_empty": 0, "fieldname": "exchange_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, - "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 + }, + { + "fieldname": "payment_term", + "fieldtype": "Link", + "label": "Payment Term", + "options": "Payment Term" } ], - "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": "2019-05-01 13:24:56.586677", + "links": [], + "modified": "2020-03-13 12:07:19.362539", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry Reference", - "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, - "track_views": 0 + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index 1b38904f6d..d363cf161b 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -1,243 +1,82 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "", - "beta": 0, - "creation": "2017-08-10 15:38:00.080575", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2017-08-10 15:38:00.080575", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "payment_term", + "description", + "due_date", + "invoice_portion", + "payment_amount", + "mode_of_payment", + "paid_amount" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "payment_term", - "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": "Payment Term", - "length": 0, - "no_copy": 0, - "options": "Payment Term", - "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 - }, + "columns": 2, + "fieldname": "payment_term", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payment Term", + "options": "Payment Term", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_from": "", - "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, - "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 - }, + "columns": 2, + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "due_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": "Due 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 - }, + "columns": 2, + "fieldname": "due_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Due Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fetch_from": "", - "fieldname": "invoice_portion", - "fieldtype": "Percent", - "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": "Invoice Portion", - "length": 0, - "no_copy": 0, - "options": "", - "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 - }, + "columns": 2, + "fieldname": "invoice_portion", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "Invoice Portion", + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 2, - "fieldname": "payment_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": "Payment Amount", - "length": 0, - "no_copy": 0, - "options": "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 - }, + "columns": 2, + "fieldname": "payment_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Payment Amount", + "options": "currency", + "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": "paid_amount", + "fieldtype": "Currency", + "label": "Paid Amount" } - ], - "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-09-06 17:35:44.580209", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment 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, - "track_views": 0 + ], + "istable": 1, + "links": [], + "modified": "2020-03-13 17:58:24.729526", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Schedule", + "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/accounts/doctype/payment_terms_template/payment_terms_template.json b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json index 7a3483d6c3..c4a2a88818 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.json @@ -1,164 +1,84 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:template_name", - "beta": 0, - "creation": "2017-08-10 15:34:28.058054", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:template_name", + "creation": "2017-08-10 15:34:28.058054", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "template_name", + "allocate_payment_based_on_payment_terms", + "terms" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "template_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": "Template 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, - "unique": 0 - }, + "fieldname": "template_name", + "fieldtype": "Data", + "label": "Template Name", + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "terms", - "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 Terms", - "length": 0, - "no_copy": 0, - "options": "Payment Terms Template Detail", - "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": "terms", + "fieldtype": "Table", + "label": "Payment Terms", + "options": "Payment Terms Template Detail", + "reqd": 1 + }, + { + "default": "0", + "description": "If this checkbox is checked, paid amount will be splitted and allocated as per the amounts in payment schedule against each payment term", + "fieldname": "allocate_payment_based_on_payment_terms", + "fieldtype": "Check", + "label": "Allocate Payment Based On Payment Terms" } - ], - "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-01-24 11:13:31.158613", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Payment Terms Template", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-04-01 15:35:18.112619", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Payment Terms Template", + "owner": "Administrator", "permissions": [ { - "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": "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, - "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": "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 - }, + }, { - "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": "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 } - ], - "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/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 240b0d8381..e9c286fcf0 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -344,26 +344,28 @@ class ReceivablePayableReport(object): def allocate_outstanding_based_on_payment_terms(self, row): self.get_payment_terms(row) for term in row.payment_terms: - term.outstanding = term.invoiced # update "paid" and "oustanding" for this term - self.allocate_closing_to_term(row, term, 'paid') + if not term.paid: + self.allocate_closing_to_term(row, term, 'paid') # update "credit_note" and "oustanding" for this term if term.outstanding: self.allocate_closing_to_term(row, term, 'credit_note') + row.payment_terms = sorted(row.payment_terms, key=lambda x: x['due_date']) + def get_payment_terms(self, row): # build payment_terms for row payment_terms_details = frappe.db.sql(""" select si.name, si.party_account_currency, si.currency, si.conversion_rate, - ps.due_date, ps.payment_amount, ps.description + ps.due_date, ps.payment_amount, ps.description, ps.paid_amount from `tab{0}` si, `tabPayment Schedule` ps where si.name = ps.parent and si.name = %s - order by ps.due_date + order by ps.paid_amount desc, due_date """.format(row.voucher_type), row.voucher_no, as_dict = 1) @@ -389,11 +391,14 @@ class ReceivablePayableReport(object): "invoiced": invoiced, "invoice_grand_total": row.invoiced, "payment_term": d.description, - "paid": 0.0, + "paid": d.paid_amount, "credit_note": 0.0, - "outstanding": 0.0 + "outstanding": invoiced - d.paid_amount })) + if d.paid_amount: + row['paid'] -= d.paid_amount + def allocate_closing_to_term(self, row, term, key): if row[key]: if row[key] > term.outstanding: diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4045250c33..3e97f76f7b 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -819,7 +819,7 @@ class AccountsController(TransactionBase): else: for d in self.get("payment_schedule"): if d.invoice_portion: - d.payment_amount = grand_total * flt(d.invoice_portion) / 100 + d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount')) def set_due_date(self): due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] From 92b933f4acd14520a35ca80c395b1c682278a14e Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Thu, 23 Apr 2020 16:15:18 +0530 Subject: [PATCH 22/73] feat: Upload Attendance (#20947) Co-authored-by: Nabin Hait --- .../upload_attendance/upload_attendance.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py index 1707e3578b..f75bb4155e 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py @@ -9,6 +9,8 @@ from frappe.utils import cstr, add_days, date_diff, getdate from frappe import _ from frappe.utils.csvutils import UnicodeWriter from frappe.model.document import Document +from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from erpnext.hr.utils import get_holidays_for_employee class UploadAttendance(Document): pass @@ -48,6 +50,7 @@ def add_data(w, args): def get_data(args): dates = get_dates(args) employees = get_active_employees() + holidays = get_holidays_for_employees([employee.name for employee in employees], args["from_date"], args["to_date"]) existing_attendance_records = get_existing_attendance_records(args) data = [] for date in dates: @@ -63,6 +66,9 @@ def get_data(args): and getdate(employee.date_of_joining) <= getdate(date) \ and getdate(employee.relieving_date) >= getdate(date): existing_attendance = existing_attendance_records[tuple([getdate(date), employee.name])] + + employee_holiday_list = get_holiday_list_for_employee(employee.name) + row = [ existing_attendance and existing_attendance.name or "", employee.name, employee.employee_name, date, @@ -70,9 +76,22 @@ def get_data(args): existing_attendance and existing_attendance.leave_type or "", employee.company, existing_attendance and existing_attendance.naming_series or get_naming_series(), ] + if date in holidays[employee_holiday_list]: + row[4] = "Holiday" data.append(row) + return data +def get_holidays_for_employees(employees, from_date, to_date): + holidays = {} + for employee in employees: + holiday_list = get_holiday_list_for_employee(employee) + holiday = get_holidays_for_employee(employee, getdate(from_date), getdate(to_date)) + if holiday_list not in holidays: + holidays[holiday_list] = holiday + + return holidays + def writedata(w, data): for row in data: w.writerow(row) @@ -123,6 +142,11 @@ def upload(): frappe.enqueue(import_attendances, rows=rows, now=True if len(rows) < 200 else False) def import_attendances(rows): + + def remove_holidays(rows): + rows = [ row for row in rows if row[4] != "Holiday"] + return + from frappe.modules import scrub rows = list(filter(lambda x: x and any(x), rows)) @@ -133,6 +157,8 @@ def import_attendances(rows): ret = [] error = False + rows = remove_holidays(rows) + from frappe.utils.csvutils import check_record, import_doc for i, row in enumerate(rows): From 0f2c64cfefb76ad11b2e322c54563abd0a274fd3 Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Thu, 23 Apr 2020 19:19:09 +0530 Subject: [PATCH 23/73] fix: travis --- erpnext/patches/v12_0/fix_quotation_expired_status.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index a0320feb7b..0e4419ac50 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -1,5 +1,5 @@ import frappe - +from frappe.utils import nowdate def execute(): # fixes status of quotations which have status 'Expired' despite having valid sales order created From f7aa6a835795100dd9c4d1aabee1adfad47ea4ed Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 23 Apr 2020 19:27:50 +0530 Subject: [PATCH 24/73] chore: Stock Entry Form Cleanup --- erpnext/stock/doctype/stock_entry/stock_entry.js | 4 +--- erpnext/stock/doctype/stock_entry/stock_entry.json | 13 +++++++++++-- .../stock_entry_detail/stock_entry_detail.json | 7 ++++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index d1048fc195..6272e01a60 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -215,9 +215,7 @@ frappe.ui.form.on('Stock Entry', { source_doctype: "Material Request", target: frm, date_field: "schedule_date", - setters: { - company: frm.doc.company, - }, + setters: {}, get_query_filters: { docstatus: 1, material_request_type: ["in", ["Material Transfer", "Material Issue"]], diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json index bdd0bd0de1..704ae41bc5 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.json +++ b/erpnext/stock/doctype/stock_entry/stock_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2013-04-09 11:43:55", @@ -12,7 +13,6 @@ "stock_entry_type", "outgoing_stock_entry", "purpose", - "company", "work_order", "purchase_order", "delivery_note_no", @@ -20,6 +20,7 @@ "pick_list", "purchase_receipt_no", "col2", + "company", "posting_date", "posting_time", "set_posting_time", @@ -65,6 +66,7 @@ "dimension_col_break", "printing_settings", "select_print_heading", + "print_settings_col_break", "letter_head", "more_info", "is_opening", @@ -291,6 +293,7 @@ "fieldtype": "Section Break" }, { + "description": "Sets 'Source Warehouse' in each row of the items table.", "fieldname": "from_warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -320,6 +323,7 @@ "fieldtype": "Column Break" }, { + "description": "Sets 'Target Warehouse' in each row of the items table.", "fieldname": "to_warehouse", "fieldtype": "Link", "in_list_view": 1, @@ -622,12 +626,17 @@ "label": "Pick List", "options": "Pick List", "read_only": 1 + }, + { + "fieldname": "print_settings_col_break", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, - "modified": "2019-09-27 14:38:20.801420", + "links": [], + "modified": "2020-04-23 12:56:52.881752", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry", diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json index a848c80cf2..c16a41c24f 100644 --- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json +++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json @@ -14,12 +14,12 @@ "t_warehouse", "sec_break1", "item_code", - "item_group", "col_break2", "item_name", "section_break_8", "description", "column_break_10", + "item_group", "image", "image_view", "quantity_and_rate", @@ -178,6 +178,7 @@ "bold": 1, "fieldname": "basic_rate", "fieldtype": "Currency", + "in_list_view": 1, "label": "Basic Rate (as per Stock UOM)", "oldfieldname": "incoming_rate", "oldfieldtype": "Currency", @@ -420,6 +421,7 @@ "options": "Item" }, { + "collapsible": 1, "fieldname": "reference_section", "fieldtype": "Section Break", "label": "Reference" @@ -466,7 +468,6 @@ "fetch_from": "item_code.item_group", "fieldname": "item_group", "fieldtype": "Data", - "in_list_view": 1, "label": "Item Group" }, { @@ -495,7 +496,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-19 12:34:09.836295", + "modified": "2020-04-23 19:19:28.539769", "modified_by": "Administrator", "module": "Stock", "name": "Stock Entry Detail", From e1e98fe1168a4ce803818835bb4dea7fad194866 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 23 Apr 2020 20:18:52 +0530 Subject: [PATCH 25/73] fix: Issues on qty trigger in Stock Entry Detail --- 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 d1048fc195..aba938663b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -511,9 +511,10 @@ frappe.ui.form.on('Stock Entry', { item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item)); - item.valuation_rate = flt(flt(item.basic_rate) - + (flt(item.additional_cost) / flt(item.transfer_qty)), - precision("valuation_rate", item)); + if (flt(item.transfer_qty)) { + item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)), + precision("valuation_rate", item)); + } } refresh_field('items'); @@ -539,9 +540,8 @@ frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { - frm.events.set_serial_no(frm, cdt, cdn, () => { - frm.events.set_basic_rate(frm, cdt, cdn); - }); + frm.events.set_basic_rate(frm, cdt, cdn); + frm.events.set_serial_no(frm, cdt, cdn); }, conversion_factor: function(frm, cdt, cdn) { From e01723e360f0d7141b13ad0b7d5390bee02b14f5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Thu, 23 Apr 2020 22:49:28 +0530 Subject: [PATCH 26/73] Review changes --- .../linkedin_settings/linkedin_settings.js | 42 ++++++++++--------- .../linkedin_settings/linkedin_settings.py | 23 ++++++---- .../social_media_post/social_media_post.js | 35 ++++++++-------- .../social_media_post/social_media_post.py | 2 +- .../twitter_settings/twitter_settings.js | 37 ++++++++-------- .../twitter_settings/twitter_settings.py | 20 ++++++--- 6 files changed, 90 insertions(+), 69 deletions(-) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index b888e721bb..b05b6021af 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -3,7 +3,7 @@ frappe.ui.form.on('LinkedIn Settings', { onload: function(frm){ - if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.confirm( __('Session not valid, Do you want to login?'), function(){ @@ -12,45 +12,49 @@ frappe.ui.form.on('LinkedIn Settings', { function(){ window.close(); } - ) + ); } }, refresh: function(frm){ - if(frm.doc.session_status=="Expired"){ + if (frm.doc.session_status=="Expired"){ + let msg = __("Session Not Active. Save doc to login."); frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' + `
+
+ +
+
` ); } - if(frm.doc.session_status=="Active"){ - let d = new Date(frm.doc.modified) + + if (frm.doc.session_status=="Active"){ + let d = new Date(frm.doc.modified); d.setDate(d.getDate()+60); - let dn = new Date() + let dn = new Date(); let days = d.getTime() - dn.getTime(); days = Math.floor(days/(1000 * 3600 * 24)); let msg,color; - if(days>0){ + + if (days>0){ msg = __("Your Session will be expire in ") + days + __(" days."); color = "green"; } - else{ + else { msg = __("Session is expired. Save doc to login."); color = "red"; } + frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' + `
+
+ +
+
` ); } }, login: function(frm){ - if(frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.call({ doc: frm.doc, method: "get_authorization_url", diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py index 9326e6d046..5df35df3dd 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py @@ -9,6 +9,7 @@ from frappe.utils import get_site_url, get_url_to_form, get_link_to_form from frappe.model.document import Document from frappe.utils.file_manager import get_file, get_file_path from six.moves.urllib.parse import urlencode + class LinkedInSettings(Document): def get_authorization_url(self): params = urlencode({ @@ -46,9 +47,12 @@ class LinkedInSettings(Document): url = "https://api.linkedin.com/v2/me" response = requests.get(url=url, headers=headers) response = frappe.parse_json(response.content.decode()) - self.db_set("person_urn", response["id"]) - self.db_set("account_name", response["vanityName"]) - self.db_set("session_status", "Active") + + frappe.db.set_value(self.doctype, self.name, { + "person_urn": response["id"], + "account_name": response["vanityName"], + "session_status": "Active" + }) frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings") @@ -57,6 +61,7 @@ class LinkedInSettings(Document): return self.post_text(text) else: media_id = self.upload_image(media) + if media_id: return self.post_text(text, media_id=media_id) else: @@ -66,7 +71,6 @@ class LinkedInSettings(Document): def upload_image(self, media): media = get_file_path(media) register_url = "https://api.linkedin.com/v2/assets?action=registerUpload" - body = { "registerUploadRequest": { "recipes": ["urn:li:digitalmediaRecipe:feedshare-image"], @@ -81,6 +85,7 @@ class LinkedInSettings(Document): "Authorization": "Bearer {}".format(self.access_token) } response = self.http_post(url=register_url, body=body, headers=headers) + if response.status_code == 200: response = response.json() asset = response["value"]["asset"] @@ -91,11 +96,10 @@ class LinkedInSettings(Document): frappe.throw(_("Error While Uploading Image"), title="{0} {1}".format(response.status_code, response.reason)) return None return asset + return None - def post_text(self, text, media_id=None): - # url = "https://api.linkedin.com/v2/ugcPosts" url = "https://api.linkedin.com/v2/shares" headers = { "X-Restli-Protocol-Version": "2.0.0", @@ -112,6 +116,7 @@ class LinkedInSettings(Document): "text": text } } + if media_id: body["content"]= { "contentEntities": [{ @@ -119,6 +124,7 @@ class LinkedInSettings(Document): }], "shareMediaCategory": "IMAGE" } + response = self.http_post(url=url, headers=headers, body=body) return response @@ -132,17 +138,20 @@ class LinkedInSettings(Document): ) if response.status_code not in [201,200]: raise + except Exception as e: content = json.loads(response.content) + if response.status_code == 401: self.db_set("session_status", "Expired") frappe.db.commit() - frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") + frappe.throw(content["message"], title="LinkedIn Error - Unauthorized") elif response.status_code == 403: frappe.msgprint(_("You Didn't have permission to access this API")) frappe.throw(content["message"], title="LinkedIn Error - Access Denied") else: frappe.throw(response.reason, title=response.status_code) + return response @frappe.whitelist() diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index 517b3b4614..c2a17d8bec 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -2,44 +2,45 @@ // For license information, please see license.txt frappe.ui.form.on('Social Media Post', { validate: function(frm){ - if(frm.doc.twitter==0 && frm.doc.linkedin==0){ + if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){ frappe.throw(__("Select atleast one Social Media from Share on.")) } - if(frm.doc.scheduled_time) { + if (frm.doc.scheduled_time) { let scheduled_time = new Date(frm.doc.scheduled_time); let date_time = new Date(); - if(scheduled_time.getTime() < date_time.getTime()){ + if (scheduled_time.getTime() < date_time.getTime()){ frappe.throw(__("Invalid Scheduled Time")); } } - if(frm.doc.text?.length > 280){ + if (frm.doc.text?.length > 280){ frappe.throw(__("Length Must be less than 280.")) } }, refresh: function(frm){ - if(frm.doc.docstatus === 1){ - if(frm.doc.post_status != "Posted"){ + if (frm.doc.docstatus === 1){ + if (frm.doc.post_status != "Posted"){ add_post_btn(frm); } - else if(frm.doc.post_status == "Posted"){ + else if (frm.doc.post_status == "Posted"){ frm.set_df_property('sheduled_time', 'read_only', 1); } - let html = '
'; - if(frm.doc.twitter){ + + let html=''; + if (frm.doc.twitter){ let color = frm.doc.twitter_post_id ? "green" : "red"; let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted"; - html += '
' + - ' ' + - '
' ; + html += `
+ +
` ; } - if(frm.doc.linkedin){ + if (frm.doc.linkedin){ let color = frm.doc.linkedin_post_id ? "green" : "red"; let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted"; - html += '
' + - ' ' + - '
' ; + html += `
+ +
` ; } - html += '
'; + html = `
${html}
`; frm.dashboard.set_headline_alert(html); } } diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.py b/erpnext/crm/doctype/social_media_post/social_media_post.py index 2cca60892b..ed1b583944 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.py +++ b/erpnext/crm/doctype/social_media_post/social_media_post.py @@ -33,7 +33,7 @@ class SocialMediaPost(Document): self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1]) self.db_set("post_status", "Posted") - except Exception as e: + except: self.db_set("post_status", "Error") title = _("Error while POSTING {0}").format(self.name) traceback = frappe.get_traceback() diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index be5ae9d710..eae25206b3 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Twitter Settings', { onload: function(frm){ - if(frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.confirm( __('Session not valid, Do you want to login?'), function(){ @@ -12,31 +12,30 @@ frappe.ui.form.on('Twitter Settings', { function(){ window.close(); } - ) + ); } }, refresh: function(frm){ - if(frm.doc.session_status=="Active"){ - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); + let msg,color; + if (frm.doc.session_status == "Active"){ + msg = __("Session Active"); + color = 'green'; } - else if(frm.doc.session_status=="Expired"){ - frm.dashboard.set_headline_alert( - '
' + - '
' + - ' ' + - '
' + - '
' - ); + else { + msg = __("Session Not Active. Save doc to login."); + color = 'red'; } + + frm.dashboard.set_headline_alert( + `
+
+ +
+
` + ); }, login: function(frm){ - if(frm.doc.consumer_key && frm.doc.consumer_secret){ + if (frm.doc.consumer_key && frm.doc.consumer_secret){ frappe.call({ doc: frm.doc, method: "get_authorize_url", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py index f069a600dd..64f53b5eb0 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py @@ -14,6 +14,7 @@ class TwitterSettings(Document): def get_authorize_url(self): callback_url = "{0}/?cmd=erpnext.crm.doctype.twitter_settings.twitter_settings.callback".format(frappe.utils.get_url()) auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url) + try: redirect_url = auth.get_authorization_url() return redirect_url @@ -28,16 +29,21 @@ class TwitterSettings(Document): 'oauth_token' : oauth_token, 'oauth_token_secret' : oauth_verifier } + try: auth.get_access_token(oauth_verifier) - self.db_set("oauth_token", auth.access_token) - self.db_set("oauth_secret", auth.access_token_secret) api = self.get_api() user = api.me() - self.db_set("account_name", user._json["screen_name"]) profile_pic = (user._json["profile_image_url"]).replace("_normal","") - self.db_set("profile_pic", profile_pic) - self.db_set("session_status", "Active") + + frappe.db.set_value(self.doctype, self.name, { + "oauth_token" : auth.access_token, + "oauth_secret" : auth.access_token_secret, + "account_name" : user._json["screen_name"], + "profile_pic" : profile_pic, + "session_status" : "Active" + }) + frappe.local.response["type"] = "redirect" frappe.local.response["location"] = get_url_to_form("Twitter Settings","Twitter Settings") except TweepError as e: @@ -64,6 +70,7 @@ class TwitterSettings(Document): media = get_file_path(media) api = self.get_api() media = api.media_upload(media) + return media.media_id def send_tweet(self, text, media_id=None): @@ -73,6 +80,7 @@ class TwitterSettings(Document): response = api.update_status(status = text, media_ids = [media_id]) else: response = api.update_status(status = text) + return response except TweepError as e: @@ -87,4 +95,4 @@ class TwitterSettings(Document): def callback(oauth_token, oauth_verifier): twitter_settings = frappe.get_single("Twitter Settings") twitter_settings.get_access_token(oauth_token,oauth_verifier) - frappe.db.commit() + frappe.db.commit() From 22d2970339cf1f4e46829a892c34b4a80dff025e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 24 Apr 2020 13:47:44 +0530 Subject: [PATCH 27/73] fix: query --- erpnext/patches/v12_0/fix_quotation_expired_status.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index 0e4419ac50..fcc7094b55 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -17,8 +17,7 @@ def execute(): frappe.db.sql( """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({invalid_so_against_quo})""" - .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo), - (nowdate()) + .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo) ) valid_so_against_quo = """ @@ -32,6 +31,5 @@ def execute(): frappe.db.sql( """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and not exists({valid_so_against_quo})""" - .format(cond=cond, valid_so_against_quo=valid_so_against_quo), - (nowdate()) + .format(cond=cond, valid_so_against_quo=valid_so_against_quo) ) From 40140c63dc2490078d22895d7908d302fd04d5cc Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 24 Apr 2020 14:51:14 +0530 Subject: [PATCH 28/73] fix: rate gets overwritten when pricing rule is set --- erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 1 + erpnext/public/js/controllers/transaction.js | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index e13fcb96df..19f571fb30 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -237,6 +237,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa 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), + 'price_or_product_discount': pricing_rule.price_or_product_discount, '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'))) }) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 3443abc734..afe4d8c612 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1407,12 +1407,12 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ for(var k in args) { let data = args[k]; - + debugger; if (data && data.apply_rule_on_other_items) { me.frm.doc.items.forEach(d => { if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { for(var k in data) { - if (in_list(fields, k) && data[k]) { + if (in_list(fields, k) && data[k] && (data.price_or_product_discount === 'price' || k === 'pricing_rules')) { frappe.model.set_value(d.doctype, d.name, k, data[k]); } } From 8c70e8d4df15b9863d343ec3da06fafbb6af18ff Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Fri, 24 Apr 2020 16:41:48 +0530 Subject: [PATCH 29/73] fix: remove debugger --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index afe4d8c612..42964474b0 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1407,7 +1407,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ for(var k in args) { let data = args[k]; - debugger; + if (data && data.apply_rule_on_other_items) { me.frm.doc.items.forEach(d => { if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { From 5460691c8f08d728a99f05acacde0154d9fe7300 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 24 Apr 2020 23:55:33 +0530 Subject: [PATCH 30/73] fix: make target warehouse field mandatory while saving the work order --- .../doctype/production_plan/production_plan.py | 1 + erpnext/manufacturing/doctype/work_order/work_order.js | 3 +-- erpnext/manufacturing/doctype/work_order/work_order.json | 6 ++++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 358a5429d9..c3f27cd3de 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -346,6 +346,7 @@ class ProductionPlan(Document): if not wo.fg_warehouse: wo.fg_warehouse = warehouse.get('fg_warehouse') try: + wo.flags.ignore_mandatory = True wo.insert() return wo.name except OverProductionError: diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index d541866f8b..43145170f7 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -313,7 +313,7 @@ frappe.ui.form.on("Work Order", { "Work in Progress": "progress-bar-warning", "Completed": "progress-bar-success" }; - + let bars = []; let message = ''; let title = ''; @@ -404,7 +404,6 @@ frappe.ui.form.on("Work Order", { }, before_submit: function(frm) { - frm.toggle_reqd(["fg_warehouse", "wip_warehouse"], true); frm.fields_dict.required_items.grid.toggle_reqd("source_warehouse", true); frm.toggle_reqd("transfer_material_against", frm.doc.operations && frm.doc.operations.length > 0); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json index e6990fd985..00a67a03d6 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.json +++ b/erpnext/manufacturing/doctype/work_order/work_order.json @@ -231,6 +231,7 @@ "fieldname": "wip_warehouse", "fieldtype": "Link", "label": "Work-in-Progress Warehouse", + "mandatory_depends_on": "eval:!doc.skip_transfer || doc.from_wip_warehouse", "options": "Warehouse" }, { @@ -238,7 +239,8 @@ "fieldname": "fg_warehouse", "fieldtype": "Link", "label": "Target Warehouse", - "options": "Warehouse" + "options": "Warehouse", + "reqd": 1 }, { "fieldname": "column_break_12", @@ -481,7 +483,7 @@ "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-01-31 12:46:23.636033", + "modified": "2020-04-24 19:32:43.323054", "modified_by": "Administrator", "module": "Manufacturing", "name": "Work Order", From 7b91617303852ab7339a9a110da8a01275804415 Mon Sep 17 00:00:00 2001 From: Diksha Jadhav Date: Sat, 25 Apr 2020 00:02:32 +0530 Subject: [PATCH 31/73] feat(accounting): show actual qty for warehouse in sales invoice --- erpnext/accounts/doctype/sales_invoice/sales_invoice.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 6f78db2ccc..a6113cd2bb 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -32,6 +32,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.script_manager.trigger("is_pos"); me.frm.refresh_fields(); } + erpnext.queries.setup_warehouse_query(this.frm); }, refresh: function(doc, dt, dn) { From 524eb6ce9b411f4a8c64bdef1626cf298fc2c5f5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Sat, 25 Apr 2020 00:35:26 +0530 Subject: [PATCH 32/73] freeze screen while posting and login --- erpnext/crm/doctype/linkedin_settings/linkedin_settings.js | 1 + erpnext/crm/doctype/social_media_post/social_media_post.js | 2 ++ erpnext/crm/doctype/twitter_settings/twitter_settings.js | 1 + 3 files changed, 4 insertions(+) diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js index b05b6021af..50b98e9ce1 100644 --- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js +++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js @@ -55,6 +55,7 @@ frappe.ui.form.on('LinkedIn Settings', { }, login: function(frm){ if (frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.dom.freeze(); frappe.call({ doc: frm.doc, method: "get_authorization_url", diff --git a/erpnext/crm/doctype/social_media_post/social_media_post.js b/erpnext/crm/doctype/social_media_post/social_media_post.js index c2a17d8bec..3a14f2d2e9 100644 --- a/erpnext/crm/doctype/social_media_post/social_media_post.js +++ b/erpnext/crm/doctype/social_media_post/social_media_post.js @@ -51,6 +51,7 @@ var add_post_btn = function(frm){ }); } var post = function(frm){ + frappe.dom.freeze(); frappe.call({ method: "erpnext.crm.doctype.social_media_post.social_media_post.publish", args: { @@ -59,6 +60,7 @@ var post = function(frm){ }, callback: function(r) { frm.reload_doc(); + frappe.dom.unfreeze(); } }) diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index eae25206b3..8f9c419062 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -36,6 +36,7 @@ frappe.ui.form.on('Twitter Settings', { }, login: function(frm){ if (frm.doc.consumer_key && frm.doc.consumer_secret){ + frappe.dom.freeze(); frappe.call({ doc: frm.doc, method: "get_authorize_url", From 8952ac1ab45d41be516514680f2313d93100a7a3 Mon Sep 17 00:00:00 2001 From: Saqib Date: Sat, 25 Apr 2020 09:44:21 +0530 Subject: [PATCH 33/73] fix: custom buttons are hidden if order type of SO is customised (#21397) --- .../selling/doctype/sales_order/sales_order.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 61aa608dd3..3c1ffe9596 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -148,9 +148,14 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + + const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1; + const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1; + // order type has been customised then show all the action buttons + const order_is_a_custom_sale = ["Sales", "Shopping Cart", "Maintenance"].indexOf(doc.order_type) === -1; // delivery note - if(flt(doc.per_delivered, 6) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { + if(flt(doc.per_delivered, 6) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery) { this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create')); this.frm.add_custom_button(__('Work Order'), () => this.make_work_order(), __('Create')); } @@ -161,8 +166,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } // material request - if(!doc.order_type || ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 - && flt(doc.per_delivered, 6) < 100) { + if(!doc.order_type || (order_is_a_sale || order_is_a_custom_sale) && flt(doc.per_delivered, 6) < 100) { this.frm.add_custom_button(__('Material Request'), () => this.make_material_request(), __('Create')); this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create')); } @@ -171,14 +175,13 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( this.frm.add_custom_button(__('Purchase Order'), () => this.make_purchase_order(), __('Create')); // maintenance - if(flt(doc.per_delivered, 2) < 100 && - ["Sales", "Shopping Cart"].indexOf(doc.order_type)===-1) { + if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) { this.frm.add_custom_button(__('Maintenance Visit'), () => this.make_maintenance_visit(), __('Create')); this.frm.add_custom_button(__('Maintenance Schedule'), () => this.make_maintenance_schedule(), __('Create')); } // project - if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { + if(flt(doc.per_delivered, 2) < 100 && (order_is_a_sale || order_is_a_custom_sale) && allow_delivery) { this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create')); } From c20de8ac8d8534c980ebe6bc6707530dc6878785 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Sat, 25 Apr 2020 13:15:45 +0530 Subject: [PATCH 34/73] fix: Loan Disbursement against account --- .../doctype/loan_disbursement/loan_disbursement.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py index 2918486ebd..c9e36a84dd 100644 --- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py +++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py @@ -84,7 +84,7 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ "account": loan_details.loan_account, - "against": loan_details.applicant, + "against": loan_details.payment_account, "debit": self.disbursed_amount, "debit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", @@ -100,7 +100,7 @@ class LoanDisbursement(AccountsController): gle_map.append( self.get_gl_dict({ "account": loan_details.payment_account, - "against": loan_details.applicant, + "against": loan_details.loan_account, "credit": self.disbursed_amount, "credit_in_account_currency": self.disbursed_amount, "against_voucher_type": "Loan", From 454b1cf48d6fec079f48a8cede0598490171bef9 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Sat, 25 Apr 2020 14:21:24 +0500 Subject: [PATCH 35/73] Hook Fix in the scheduler events (#21410) --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 6b6d1e2258..447cc0656f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -270,7 +270,7 @@ auto_cancel_exempted_doctypes= [ scheduler_events = { "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", - "erpnext.healthcare_healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" + "erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder" ], "hourly": [ 'erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.trigger_emails', From ad16e61bc2188786c45717ad48758c5a0107600c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sat, 25 Apr 2020 14:52:05 +0530 Subject: [PATCH 36/73] fix: Valid warehouse in woocommerce syncing and other small fixes (#21407) * fix: Valid warehouse in woocommerce syncing * fix: dmall fixes in gross & net profit report * fix: company is required for getting party details * fix: None issue while getting raw material rate based on last purchase rate --- .../doctype/payment_entry/payment_entry.js | 2 +- .../gross_and_net_profit_report.py | 32 ++++++++++++------- .../connectors/woocommerce_connection.py | 6 +++- erpnext/manufacturing/doctype/bom/bom.py | 2 +- 4 files changed, 27 insertions(+), 15 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 05f01475c1..a378a51cdf 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -302,7 +302,7 @@ frappe.ui.form.on('Payment Entry', { frm.set_value("contact_email", ""); frm.set_value("contact_person", ""); } - if(frm.doc.payment_type && frm.doc.party_type && frm.doc.party) { + if(frm.doc.payment_type && frm.doc.party_type && frm.doc.party && frm.doc.company) { if(!frm.doc.posting_date) { frappe.msgprint(__("Please select Posting Date before selecting Party")) frm.set_value("party", ""); diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index 6550981a14..260f35f270 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -8,7 +8,6 @@ from frappe.utils import flt from erpnext.accounts.report.financial_statements import (get_period_list, get_columns, get_data) import copy - def execute(filters=None): period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, filters.periodicity, filters.accumulated_values, filters.company) @@ -27,17 +26,19 @@ def execute(filters=None): gross_income = get_revenue(income, period_list) - gross_expense = get_revenue(expense, period_list) if(len(gross_income)==0 and len(gross_expense)== 0): - data.append({"account_name": "'" + _("Nothing is included in gross") + "'", - "account": "'" + _("Nothing is included in gross") + "'"}) - + data.append({ + "account_name": "'" + _("Nothing is included in gross") + "'", + "account": "'" + _("Nothing is included in gross") + "'" + }) return columns, data - data.append({"account_name": "'" + _("Included in Gross Profit") + "'", - "account": "'" + _("Included in Gross Profit") + "'"}) + data.append({ + "account_name": "'" + _("Included in Gross Profit") + "'", + "account": "'" + _("Included in Gross Profit") + "'" + }) data.append({}) data.extend(gross_income or []) @@ -111,7 +112,6 @@ def set_total(node, value, complete_list, totals): def get_profit(gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False): - profit_loss = { "account_name": "'" + _(profit_type) + "'", "account": "'" + _(profit_type) + "'", @@ -123,7 +123,9 @@ def get_profit(gross_income, gross_expense, period_list, company, profit_type, c for period in period_list: key = period if consolidated else period.key - profit_loss[key] = flt(gross_income[0].get(key, 0)) - flt(gross_expense[0].get(key, 0)) + gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0 + gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0 + profit_loss[key] = gross_income_for_period - gross_expense_for_period if profit_loss[key]: has_value=True @@ -143,12 +145,18 @@ def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expe for period in period_list: key = period if consolidated else period.key - total_income = flt(gross_income[0].get(key, 0)) + flt(non_gross_income[0].get(key, 0)) - total_expense = flt(gross_expense[0].get(key, 0)) + flt(non_gross_expense[0].get(key, 0)) + gross_income_for_period = flt(gross_income[0].get(key, 0)) if gross_income else 0 + non_gross_income_for_period = flt(non_gross_income[0].get(key, 0)) if non_gross_income else 0 + + gross_expense_for_period = flt(gross_expense[0].get(key, 0)) if gross_expense else 0 + non_gross_expense_for_period = flt(non_gross_expense[0].get(key, 0)) if non_gross_expense else 0 + + total_income = gross_income_for_period + non_gross_income_for_period + total_expense = gross_expense_for_period + non_gross_expense_for_period profit_loss[key] = flt(total_income) - flt(total_expense) if profit_loss[key]: has_value=True if has_value: - return profit_loss + return profit_loss \ No newline at end of file diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py index 4422d23e38..618865200c 100644 --- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py +++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py @@ -144,6 +144,10 @@ def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_lang): company_abbr = frappe.db.get_value('Company', woocommerce_settings.company, 'abbr') + default_warehouse = _("Stores - {0}", sys_lang).format(company_abbr) + if not frappe.db.exists("Warehouse", default_warehouse): + frappe.throw(_("Please set Warehouse in Woocommerce Settings")) + for item in order.get("line_items"): woocomm_item_id = item.get("product_id") found_item = frappe.get_doc("Item", {"woocommerce_id": woocomm_item_id}) @@ -158,7 +162,7 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l "uom": woocommerce_settings.uom or _("Nos", sys_lang), "qty": item.get("quantity"), "rate": item.get("price"), - "warehouse": woocommerce_settings.warehouse or _("Stores - {0}", sys_lang).format(company_abbr) + "warehouse": woocommerce_settings.warehouse or default_warehouse }) add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index a83d193b6a..b1fc4deae9 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -193,7 +193,7 @@ class BOM(WebsiteGenerator): if self.rm_cost_as_per == 'Valuation Rate': rate = self.get_valuation_rate(arg) * (arg.get("conversion_factor") or 1) elif self.rm_cost_as_per == 'Last Purchase Rate': - rate = (arg.get('last_purchase_rate') \ + rate = flt(arg.get('last_purchase_rate') \ or frappe.db.get_value("Item", arg['item_code'], "last_purchase_rate")) \ * (arg.get("conversion_factor") or 1) elif self.rm_cost_as_per == "Price List": From 8b1c71ea44e895bbf1f2eab19a0660510d892305 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Sun, 26 Apr 2020 09:38:18 +0530 Subject: [PATCH 37/73] fix: work order creation message against material request (#21413) --- .../material_request/material_request.js | 1 + .../material_request/material_request.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index b97da693b4..eb298a60aa 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -299,6 +299,7 @@ frappe.ui.form.on('Material Request', { args: { "material_request": frm.doc.name }, + freeze: true, callback: function(r) { if(r.message.length) { frm.reload_doc(); diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py index 2d9855713c..739d7492ca 100644 --- a/erpnext/stock/doctype/material_request/material_request.py +++ b/erpnext/stock/doctype/material_request/material_request.py @@ -8,7 +8,7 @@ from __future__ import unicode_literals import frappe import json -from frappe.utils import cstr, flt, getdate, new_line_sep, nowdate, add_days +from frappe.utils import cstr, flt, getdate, new_line_sep, nowdate, add_days, get_link_to_form from frappe import msgprint, _ from frappe.model.mapper import get_mapped_doc from erpnext.stock.stock_balance import update_bin_qty, get_indented_qty @@ -522,15 +522,22 @@ def raise_work_orders(material_request): work_orders.append(wo_order.name) else: - errors.append(_("Row {0}: Bill of Materials not found for the Item {1}").format(d.idx, d.item_code)) + errors.append(_("Row {0}: Bill of Materials not found for the Item {1}") + .format(d.idx, get_link_to_form("Item", d.item_code))) if work_orders: - message = ["""%s""" % \ - (p, p) for p in work_orders] - msgprint(_("The following Work Orders were created:") + '\n' + new_line_sep(message)) + work_orders_list = [get_link_to_form("Work Order", d) for d in work_orders] + + if len(work_orders) > 1: + msgprint(_("The following {0} were created: {1}") + .format(frappe.bold(_("Work Orders")), '
' + ', '.join(work_orders_list))) + else: + msgprint(_("The {0} {1} created sucessfully") + .format(frappe.bold(_("Work Order")), work_orders_list[0])) if errors: - frappe.throw(_("Work Order cannot be created for following reason:") + '\n' + new_line_sep(errors)) + frappe.throw(_("Work Order cannot be created for following reason:
{0}") + .format(new_line_sep(errors))) return work_orders From 7e73473a65981cca27a90cc08a4c685b518e1cbd Mon Sep 17 00:00:00 2001 From: Saqib Date: Sun, 26 Apr 2020 09:40:57 +0530 Subject: [PATCH 38/73] fix: (ux) set jv voucher type depending on mode of payment (#21411) --- .../hr/doctype/employee_advance/employee_advance.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index f10e3b6ce2..f0663aefa8 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -136,9 +136,18 @@ def make_bank_entry(dt, dn): def make_return_entry(employee, company, employee_advance_name, return_amount, advance_account, mode_of_payment=None): return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) + + mode_of_payment_type = '' + if mode_of_payment: + mode_of_payment_type = frappe.get_cached_value('Mode of Payment', mode_of_payment, 'type') + if mode_of_payment_type not in ["Cash", "Bank"]: + # if mode of payment is General then it unset the type + mode_of_payment_type = None + je = frappe.new_doc('Journal Entry') je.posting_date = nowdate() - je.voucher_type = 'Bank Entry' + # if mode of payment is Bank then voucher type is Bank Entry + je.voucher_type = '{} Entry'.format(mode_of_payment_type) if mode_of_payment_type else 'Cash Entry' je.company = company je.remark = 'Return against Employee Advance: ' + employee_advance_name From de330293931c58684294a827ee15815f88197821 Mon Sep 17 00:00:00 2001 From: marination Date: Sun, 26 Apr 2020 17:16:21 +0530 Subject: [PATCH 39/73] fix: Remove callback outside if condition --- 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 aba938663b..496a865af7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -312,10 +312,9 @@ frappe.ui.form.on('Stock Entry', { callback: function(r) { if (!r.exe && r.message){ frappe.model.set_value(cdt, cdn, "serial_no", r.message); - - if (callback) { - callback(); - } + } + if (callback) { + callback(); } } }); @@ -540,8 +539,9 @@ frappe.ui.form.on('Stock Entry', { frappe.ui.form.on('Stock Entry Detail', { qty: function(frm, cdt, cdn) { - frm.events.set_basic_rate(frm, cdt, cdn); - frm.events.set_serial_no(frm, cdt, cdn); + frm.events.set_serial_no(frm, cdt, cdn, () => { + frm.events.set_basic_rate(frm, cdt, cdn); + }); }, conversion_factor: function(frm, cdt, cdn) { From 2fea0735392faf37214fce318ac74abaf7bd449e Mon Sep 17 00:00:00 2001 From: Saqib Ansari Date: Sun, 26 Apr 2020 17:30:23 +0530 Subject: [PATCH 40/73] fix: query logic --- erpnext/patches/v12_0/fix_quotation_expired_status.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v12_0/fix_quotation_expired_status.py b/erpnext/patches/v12_0/fix_quotation_expired_status.py index fcc7094b55..c8708d8013 100644 --- a/erpnext/patches/v12_0/fix_quotation_expired_status.py +++ b/erpnext/patches/v12_0/fix_quotation_expired_status.py @@ -1,5 +1,4 @@ import frappe -from frappe.utils import nowdate def execute(): # fixes status of quotations which have status 'Expired' despite having valid sales order created @@ -16,7 +15,7 @@ def execute(): and qo.valid_till < so.transaction_date""" # check if SO was created after quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and not exists({invalid_so_against_quo})""" + """UPDATE `tabQuotation` qo SET qo.status = 'Expired' WHERE {cond} and exists({invalid_so_against_quo})""" .format(cond=cond, invalid_so_against_quo=invalid_so_against_quo) ) @@ -30,6 +29,6 @@ def execute(): and qo.valid_till >= so.transaction_date""" # check if SO was created before quotation expired frappe.db.sql( - """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and not exists({valid_so_against_quo})""" + """UPDATE `tabQuotation` qo SET qo.status = 'Closed' WHERE {cond} and exists({valid_so_against_quo})""" .format(cond=cond, valid_so_against_quo=valid_so_against_quo) ) From 58ee6c1e080d2e4dc754febd36291504bf56b645 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 17:45:57 +0530 Subject: [PATCH 41/73] feat: Income tax slab (#21399) * Feat: Multiple tax as per new taxation rule * patch:for multiple tax slab, fix: payroll and exemption validation * Test: Fixture * feat: income tax slab with other charges and tax exempted deduction components * fix: added missing init file * fix: Patch fixed * fix: Patch fixed * fix: test fixes * fix: validate duplicate exemption declaration * fix: payment entry test case Co-authored-by: Anurag Mishra --- .../doctype/sales_invoice/sales_invoice.js | 10 +- .../doctype/employee_other_income/__init__.py | 0 .../employee_other_income.js | 8 + .../employee_other_income.json | 138 ++++ .../employee_other_income.py | 10 + .../test_employee_other_income.py | 10 + .../employee_tax_exemption_declaration.json | 746 ++++-------------- .../employee_tax_exemption_declaration.py | 20 +- ...test_employee_tax_exemption_declaration.py | 2 +- ...ployee_tax_exemption_proof_submission.json | 14 +- ...employee_tax_exemption_proof_submission.py | 6 +- .../hr/doctype/income_tax_slab/__init__.py | 0 .../income_tax_slab/income_tax_slab.js | 6 + .../income_tax_slab/income_tax_slab.json | 160 ++++ .../income_tax_slab/income_tax_slab.py | 10 + .../income_tax_slab/test_income_tax_slab.py | 10 + .../income_tax_slab_other_charges/__init__.py | 0 .../income_tax_slab_other_charges.json | 75 ++ .../income_tax_slab_other_charges.py | 10 + .../payroll_period/payroll_period.json | 467 ++--------- .../doctype/payroll_period/payroll_period.py | 5 +- .../salary_component/salary_component.json | 525 ++++++------ .../salary_component/test_records.json | 4 - .../salary_component/test_salary_component.py | 1 - .../doctype/salary_detail/salary_detail.json | 15 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 142 +++- .../doctype/salary_slip/test_salary_slip.py | 76 +- .../salary_structure/salary_structure.js | 1 + .../salary_structure/salary_structure.py | 23 +- .../salary_structure/test_salary_structure.py | 20 +- .../salary_structure_assignment.js | 10 + .../salary_structure_assignment.json | 13 +- erpnext/hr/utils.py | 13 + erpnext/patches.txt | 3 +- .../v11_0/set_salary_component_properties.py | 3 +- erpnext/patches/v13_0/__init__.py | 0 ..._from_payroll_period_to_income_tax_slab.py | 99 +++ erpnext/regional/india/setup.py | 18 +- 38 files changed, 1298 insertions(+), 1375 deletions(-) create mode 100644 erpnext/hr/doctype/employee_other_income/__init__.py create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.js create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.json create mode 100644 erpnext/hr/doctype/employee_other_income/employee_other_income.py create mode 100644 erpnext/hr/doctype/employee_other_income/test_employee_other_income.py create mode 100644 erpnext/hr/doctype/income_tax_slab/__init__.py create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.js create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.json create mode 100644 erpnext/hr/doctype/income_tax_slab/income_tax_slab.py create mode 100644 erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json create mode 100644 erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py create mode 100644 erpnext/patches/v13_0/__init__.py create mode 100644 erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index a6113cd2bb..60e41f9553 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -587,7 +587,9 @@ frappe.ui.form.on('Sales Invoice', { frm.set_query("account_for_change_amount", function() { return { filters: { - account_type: ['in', ["Cash", "Bank"]] + account_type: ['in', ["Cash", "Bank"]], + company: frm.doc.company, + is_group: 0 } }; }); @@ -668,7 +670,8 @@ frappe.ui.form.on('Sales Invoice', { frm.fields_dict["loyalty_redemption_account"].get_query = function() { return { filters:{ - "company": frm.doc.company + "company": frm.doc.company, + "is_group": 0 } } }; @@ -677,7 +680,8 @@ frappe.ui.form.on('Sales Invoice', { frm.fields_dict["loyalty_redemption_cost_center"].get_query = function() { return { filters:{ - "company": frm.doc.company + "company": frm.doc.company, + "is_group": 0 } } }; diff --git a/erpnext/hr/doctype/employee_other_income/__init__.py b/erpnext/hr/doctype/employee_other_income/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.js b/erpnext/hr/doctype/employee_other_income/employee_other_income.js new file mode 100644 index 0000000000..c1a74e863b --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Employee Other Income', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/hr/doctype/employee_other_income/employee_other_income.json b/erpnext/hr/doctype/employee_other_income/employee_other_income.json new file mode 100644 index 0000000000..2dd6c10988 --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "autoname": "HR-INCOME-.######", + "creation": "2020-03-18 15:04:40.767434", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "payroll_period", + "column_break_3", + "company", + "source", + "amount", + "amended_from" + ], + "fields": [ + { + "fieldname": "employee", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Employee", + "options": "Employee", + "reqd": 1 + }, + { + "fieldname": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "source", + "fieldtype": "Data", + "label": "Source" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "options": "Company:company:default_currency", + "reqd": 1 + }, + { + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Employee Other Income", + "print_hide": 1, + "read_only": 1 + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-03-19 18:06:45.361830", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Other Income", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Employee", + "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/hr/doctype/employee_other_income/employee_other_income.py b/erpnext/hr/doctype/employee_other_income/employee_other_income.py new file mode 100644 index 0000000000..ab63c0de62 --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/employee_other_income.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 EmployeeOtherIncome(Document): + pass diff --git a/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py new file mode 100644 index 0000000000..2eeca7a23d --- /dev/null +++ b/erpnext/hr/doctype/employee_other_income/test_employee_other_income.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestEmployeeOtherIncome(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json index e102ff8d70..18fad85c4b 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json @@ -1,620 +1,180 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "HR-TAX-DEC-.YYYY.-.#####", - "beta": 0, - "creation": "2018-04-13 16:53:36.175504", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "HR-TAX-DEC-.YYYY.-.#####", + "creation": "2018-04-13 16:53:36.175504", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "employee", + "employee_name", + "department", + "column_break_2", + "payroll_period", + "company", + "amended_from", + "section_break_8", + "declarations", + "section_break_10", + "total_declared_amount", + "column_break_12", + "total_exemption_amount" + ], "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": "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", - "fetch_if_empty": 0, - "fieldname": "employee_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": "Employee Name", - "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 - }, + "fetch_from": "employee.employee_name", + "fieldname": "employee_name", + "fieldtype": "Data", + "label": "Employee Name", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.department", - "fetch_if_empty": 0, - "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, - "fetch_if_empty": 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, - "fetch_if_empty": 0, - "fieldname": "payroll_period", - "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": "Payroll Period", - "length": 0, - "no_copy": 0, - "options": "Payroll Period", - "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": "payroll_period", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Payroll Period", + "options": "Payroll Period", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "employee.company", - "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": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fetch_from": "employee.company", + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, { - "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": "Employee Tax Exemption Declaration", - "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 Tax Exemption Declaration", + "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": "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, - "fetch_if_empty": 0, - "fieldname": "declarations", - "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": "Declarations", - "length": 0, - "no_copy": 0, - "options": "Employee Tax Exemption Declaration Category", - "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": "declarations", + "fieldtype": "Table", + "label": "Declarations", + "options": "Employee Tax Exemption Declaration Category" + }, { - "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_10", - "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_10", + "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_declared_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": "Total Declared Amount", - "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_declared_amount", + "fieldtype": "Currency", + "label": "Total Declared Amount", + "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_12", - "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_12", + "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_exemption_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": "Total Exemption Amount", - "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, - "fetch_if_empty": 0, - "fieldname": "other_incomes_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": "Other Incomes", - "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": "income_from_other_sources", - "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": "Income From Other Sources", - "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": "total_exemption_amount", + "fieldtype": "Currency", + "label": "Total Exemption Amount", + "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-05-11 16:13:50.472670", - "modified_by": "Administrator", - "module": "HR", - "name": "Employee Tax Exemption Declaration", - "name_case": "", - "owner": "Administrator", + ], + "is_submittable": 1, + "links": [], + "modified": "2020-03-18 14:56:25.625717", + "modified_by": "Administrator", + "module": "HR", + "name": "Employee Tax Exemption Declaration", + "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": "HR 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": "HR 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": "HR 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": "HR 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": "Employee", - "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": "Employee", + "share": 1, + "submit": 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, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index f2bba7afed..fb71a2877a 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -8,31 +8,17 @@ from frappe.model.document import Document from frappe import _ from frappe.utils import flt from frappe.model.mapper import get_mapped_doc -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_annual_eligible_hra_exemption - -class DuplicateDeclarationError(frappe.ValidationError): pass +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ + calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionDeclaration(Document): def validate(self): validate_tax_declaration(self.declarations) - self.validate_duplicate() + validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) self.set_total_declared_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() - def validate_duplicate(self): - duplicate = frappe.db.get_value("Employee Tax Exemption Declaration", - filters = { - "employee": self.employee, - "payroll_period": self.payroll_period, - "name": ["!=", self.name], - "docstatus": ["!=", 2] - } - ) - if duplicate: - frappe.throw(_("Duplicate Tax Declaration of {0} for period {1}") - .format(self.employee, self.payroll_period), DuplicateDeclarationError) - def set_total_declared_amount(self): self.total_declared_amount = 0.0 for d in self.declarations: diff --git a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index 9c87bbd1f3..9549fd1b75 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/hr/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext import unittest from erpnext.hr.doctype.employee.test_employee import make_employee -from erpnext.hr.doctype.employee_tax_exemption_declaration.employee_tax_exemption_declaration import DuplicateDeclarationError +from erpnext.hr.utils import DuplicateDeclarationError class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): def setUp(self): diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json index c170c1693d..8b117a25b5 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json +++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json @@ -21,8 +21,6 @@ "total_actual_amount", "column_break_12", "exemption_amount", - "other_incomes_section", - "income_from_other_sources", "attachment_section", "attachments", "amended_from" @@ -111,16 +109,6 @@ "label": "Total Exemption Amount", "read_only": 1 }, - { - "fieldname": "other_incomes_section", - "fieldtype": "Section Break", - "label": "Other Incomes" - }, - { - "fieldname": "income_from_other_sources", - "fieldtype": "Currency", - "label": "Income From Other Sources" - }, { "fieldname": "attachment_section", "fieldtype": "Section Break" @@ -142,7 +130,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-02 19:02:15.398486", + "modified": "2020-03-18 14:55:51.420016", "modified_by": "Administrator", "module": "HR", "name": "Employee Tax Exemption Proof Submission", diff --git a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 97ceb63476..5bc33a65f2 100644 --- a/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/hr/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -7,7 +7,8 @@ import frappe from frappe.model.document import Document from frappe import _ from frappe.utils import flt -from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, calculate_hra_exemption_for_period +from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ + calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period class EmployeeTaxExemptionProofSubmission(Document): def validate(self): @@ -15,6 +16,7 @@ class EmployeeTaxExemptionProofSubmission(Document): self.set_total_actual_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() + validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) def set_total_actual_amount(self): self.total_actual_amount = flt(self.get("house_rent_payment_amount")) @@ -32,4 +34,4 @@ class EmployeeTaxExemptionProofSubmission(Document): self.exemption_amount += hra_exemption["total_eligible_hra_exemption"] self.monthly_hra_exemption = hra_exemption["monthly_exemption"] self.monthly_house_rent = hra_exemption["monthly_house_rent"] - self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] \ No newline at end of file + self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] diff --git a/erpnext/hr/doctype/income_tax_slab/__init__.py b/erpnext/hr/doctype/income_tax_slab/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js new file mode 100644 index 0000000000..73a54eb8dd --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.js @@ -0,0 +1,6 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Income Tax Slab', { + +}); diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json new file mode 100644 index 0000000000..6d89b197d2 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.json @@ -0,0 +1,160 @@ +{ + "actions": [], + "autoname": "Prompt", + "creation": "2020-03-17 16:50:35.564915", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "effective_from", + "company", + "column_break_3", + "allow_tax_exemption", + "standard_tax_exemption_amount", + "disabled", + "amended_from", + "taxable_salary_slabs_section", + "slabs", + "taxes_and_charges_on_income_tax_section", + "other_taxes_and_charges" + ], + "fields": [ + { + "fieldname": "effective_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Effective from", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "default": "0", + "description": "If enabled, Tax Exemption Declaration will be considered for income tax calculation.", + "fieldname": "allow_tax_exemption", + "fieldtype": "Check", + "label": "Allow Tax Exemption" + }, + { + "fieldname": "taxable_salary_slabs_section", + "fieldtype": "Section Break", + "label": "Taxable Salary Slabs" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Income Tax Slab", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "slabs", + "fieldtype": "Table", + "label": "Taxable Salary Slabs", + "options": "Taxable Salary Slab", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "depends_on": "allow_tax_exemption", + "fieldname": "standard_tax_exemption_amount", + "fieldtype": "Currency", + "label": "Standard Tax Exemption Amount", + "options": "Company:company:default_currency" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "collapsible": 1, + "fieldname": "taxes_and_charges_on_income_tax_section", + "fieldtype": "Section Break", + "label": "Taxes and Charges on Income Tax" + }, + { + "fieldname": "other_taxes_and_charges", + "fieldtype": "Table", + "label": "Other Taxes and Charges", + "options": "Income Tax Slab Other Charges" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-04-24 12:28:36.805904", + "modified_by": "Administrator", + "module": "HR", + "name": "Income Tax Slab", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 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 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Administrator", + "share": 1, + "submit": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py new file mode 100644 index 0000000000..253f023f68 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/income_tax_slab.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 IncomeTaxSlab(Document): + pass diff --git a/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py new file mode 100644 index 0000000000..deaaf650a9 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab/test_income_tax_slab.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestIncomeTaxSlab(unittest.TestCase): + pass diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py b/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json new file mode 100644 index 0000000000..b23fb3dc31 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.json @@ -0,0 +1,75 @@ +{ + "actions": [], + "creation": "2020-04-24 11:46:59.041180", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "description", + "column_break_2", + "percent", + "conditions_section", + "min_taxable_income", + "column_break_7", + "max_taxable_income" + ], + "fields": [ + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "min_taxable_income", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Min Taxable Income", + "options": "Company:company:default_currency" + }, + { + "columns": 4, + "fieldname": "description", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Description", + "reqd": 1 + }, + { + "columns": 2, + "fieldname": "percent", + "fieldtype": "Percent", + "in_list_view": 1, + "label": "Percent", + "reqd": 1 + }, + { + "fieldname": "conditions_section", + "fieldtype": "Section Break", + "label": "Conditions" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "columns": 2, + "fieldname": "max_taxable_income", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Max Taxable Income", + "options": "Company:company:default_currency" + } + ], + "istable": 1, + "links": [], + "modified": "2020-04-24 13:27:43.598967", + "modified_by": "Administrator", + "module": "HR", + "name": "Income Tax Slab Other Charges", + "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/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py new file mode 100644 index 0000000000..b4098ecbf3 --- /dev/null +++ b/erpnext/hr/doctype/income_tax_slab_other_charges/income_tax_slab_other_charges.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, 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 IncomeTaxSlabOtherCharges(Document): + pass diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.json b/erpnext/hr/doctype/payroll_period/payroll_period.json index c9bac095f9..c0fa506e7f 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.json +++ b/erpnext/hr/doctype/payroll_period/payroll_period.json @@ -1,401 +1,102 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "Prompt", - "beta": 0, - "creation": "2018-04-13 15:18:53.698553", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "autoname": "Prompt", + "creation": "2018-04-13 15:18:53.698553", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "company", + "column_break_2", + "start_date", + "end_date", + "section_break_5", + "periods" + ], "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": "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, - "fetch_if_empty": 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, - "fetch_if_empty": 0, - "fieldname": "start_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": "Start 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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "start_date", + "fieldtype": "Date", + "label": "Start Date", + "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": "end_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": "End 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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "end_date", + "fieldtype": "Date", + "label": "End Date", + "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": "section_break_5", - "fieldtype": "Section Break", - "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": "Payroll Periods", - "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_5", + "fieldtype": "Section Break", + "hidden": 1, + "label": "Payroll Periods" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "periods", - "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": "Payroll Periods", - "length": 0, - "no_copy": 0, - "options": "Payroll Period Date", - "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": "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, - "label": "Taxable Salary Slabs", - "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": "taxable_salary_slabs", - "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": "Taxable Salary Slabs", - "length": 0, - "no_copy": 0, - "options": "Taxable Salary Slab", - "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": "standard_tax_exemption_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": "Standard Tax Exemption 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 + "fieldname": "periods", + "fieldtype": "Table", + "label": "Payroll Periods", + "options": "Payroll Period Date" } - ], - "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": "2019-04-26 01:45:03.160929", - "modified_by": "Administrator", - "module": "HR", - "name": "Payroll Period", - "name_case": "", - "owner": "Administrator", + ], + "links": [], + "modified": "2020-03-18 18:13:23.859980", + "modified_by": "Administrator", + "module": "HR", + "name": "Payroll Period", + "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": "HR Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR 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": "HR User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "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, - "track_views": 0 + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/payroll_period/payroll_period.py b/erpnext/hr/doctype/payroll_period/payroll_period.py index c1769591ea..6956c38285 100644 --- a/erpnext/hr/doctype/payroll_period/payroll_period.py +++ b/erpnext/hr/doctype/payroll_period/payroll_period.py @@ -45,8 +45,9 @@ class PayrollPeriod(Document): + _(") for {0}").format(self.company) frappe.throw(msg) -def get_payroll_period_days(start_date, end_date, employee): - company = frappe.db.get_value("Employee", employee, "company") +def get_payroll_period_days(start_date, end_date, employee, company=None): + if not company: + company = frappe.db.get_value("Employee", employee, "company") payroll_period = frappe.db.sql(""" select name, start_date, end_date from `tabPayroll Period` diff --git a/erpnext/hr/doctype/salary_component/salary_component.json b/erpnext/hr/doctype/salary_component/salary_component.json index 986030d8c5..5487e1dee8 100644 --- a/erpnext/hr/doctype/salary_component/salary_component.json +++ b/erpnext/hr/doctype/salary_component/salary_component.json @@ -1,264 +1,263 @@ { - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:salary_component", - "creation": "2016-06-30 15:42:43.631931", - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "salary_component", - "salary_component_abbr", - "type", - "description", - "column_break_4", - "is_payable", - "depends_on_payment_days", - "is_tax_applicable", - "deduct_full_tax_on_selected_payroll_date", - "round_to_the_nearest_integer", - "statistical_component", - "do_not_include_in_total", - "disabled", - "flexible_benefits", - "is_flexible_benefit", - "max_benefit_amount", - "column_break_9", - "pay_against_benefit_claim", - "only_tax_impact", - "create_separate_payment_entry_against_benefit_claim", - "section_break_11", - "variable_based_on_taxable_salary", - "section_break_5", - "accounts", - "condition_and_formula", - "condition", - "amount", - "amount_based_on_formula", - "formula", - "column_break_28", - "help" - ], - "fields": [ - { - "fieldname": "salary_component", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Name", - "reqd": 1, - "unique": 1 - }, - { - "fieldname": "salary_component_abbr", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Abbr", - "print_width": "120px", - "reqd": 1, - "width": "120px" - }, - { - "fieldname": "type", - "fieldtype": "Select", - "in_standard_filter": 1, - "label": "Type", - "options": "Earning\nDeduction", - "reqd": 1 - }, - { - "default": "1", - "depends_on": "eval:doc.type == \"Earning\"", - "fieldname": "is_tax_applicable", - "fieldtype": "Check", - "label": "Is Tax Applicable" - }, - { - "default": "1", - "fieldname": "is_payable", - "fieldtype": "Check", - "label": "Is Payable" - }, - { - "default": "1", - "fieldname": "depends_on_payment_days", - "fieldtype": "Check", - "label": "Depends on Payment Days", - "print_hide": 1 - }, - { - "default": "0", - "fieldname": "do_not_include_in_total", - "fieldtype": "Check", - "label": "Do Not Include in Total" - }, - { - "default": "0", - "depends_on": "is_tax_applicable", - "fieldname": "deduct_full_tax_on_selected_payroll_date", - "fieldtype": "Check", - "label": "Deduct Full Tax on Selected Payroll Date" - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "disabled", - "fieldtype": "Check", - "label": "Disabled" - }, - { - "fieldname": "description", - "fieldtype": "Small Text", - "in_list_view": 1, - "label": "Description" - }, - { - "default": "0", - "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", - "fieldname": "statistical_component", - "fieldtype": "Check", - "label": "Statistical Component" - }, - { - "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1", - "fieldname": "flexible_benefits", - "fieldtype": "Section Break", - "label": "Flexible Benefits" - }, - { - "default": "0", - "fieldname": "is_flexible_benefit", - "fieldtype": "Check", - "label": "Is Flexible Benefit" - }, - { - "depends_on": "is_flexible_benefit", - "fieldname": "max_benefit_amount", - "fieldtype": "Currency", - "label": "Max Benefit Amount (Yearly)" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "default": "0", - "depends_on": "is_flexible_benefit", - "fieldname": "pay_against_benefit_claim", - "fieldtype": "Check", - "label": "Pay Against Benefit Claim" - }, - { - "default": "0", - "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1", - "fieldname": "only_tax_impact", - "fieldtype": "Check", - "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)" - }, - { - "default": "0", - "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1", - "fieldname": "create_separate_payment_entry_against_benefit_claim", - "fieldtype": "Check", - "label": "Create Separate Payment Entry Against Benefit Claim" - }, - { - "depends_on": "eval:doc.type=='Deduction'", - "fieldname": "section_break_11", - "fieldtype": "Section Break" - }, - { - "default": "0", - "fieldname": "variable_based_on_taxable_salary", - "fieldtype": "Check", - "label": "Variable Based On Taxable Salary" - }, - { - "depends_on": "eval:doc.statistical_component != 1", - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "label": "Accounts" - }, - { - "fieldname": "accounts", - "fieldtype": "Table", - "label": "Accounts", - "options": "Salary Component Account" - }, - { - "collapsible": 1, - "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1", - "fieldname": "condition_and_formula", - "fieldtype": "Section Break", - "label": "Condition and Formula" - }, - { - "fieldname": "condition", - "fieldtype": "Code", - "label": "Condition" - }, - { - "default": "0", - "fieldname": "amount_based_on_formula", - "fieldtype": "Check", - "label": "Amount based on formula" - }, - { - "depends_on": "amount_based_on_formula", - "fieldname": "formula", - "fieldtype": "Code", - "label": "Formula" - }, - { - "depends_on": "eval:doc.amount_based_on_formula!==1", - "fieldname": "amount", - "fieldtype": "Currency", - "label": "Amount" - }, - { - "fieldname": "column_break_28", - "fieldtype": "Column Break" - }, - { - "fieldname": "help", - "fieldtype": "HTML", - "label": "Help", - "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" - }, - { - "default": "0", - "fieldname": "round_to_the_nearest_integer", - "fieldtype": "Check", - "label": "Round to the Nearest Integer" - } - ], - "icon": "fa fa-flag", - "modified": "2019-06-05 11:34:14.231228", - "modified_by": "Administrator", - "module": "HR", - "name": "Salary Component", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "HR User", - "share": 1, - "write": 1 - }, - { - "read": 1, - "role": "Employee" - } - ], - "sort_field": "modified", - "sort_order": "DESC" - } \ No newline at end of file + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:salary_component", + "creation": "2016-06-30 15:42:43.631931", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "salary_component", + "salary_component_abbr", + "type", + "description", + "column_break_4", + "depends_on_payment_days", + "is_tax_applicable", + "deduct_full_tax_on_selected_payroll_date", + "variable_based_on_taxable_salary", + "exempted_from_income_tax", + "round_to_the_nearest_integer", + "statistical_component", + "do_not_include_in_total", + "disabled", + "flexible_benefits", + "is_flexible_benefit", + "max_benefit_amount", + "column_break_9", + "pay_against_benefit_claim", + "only_tax_impact", + "create_separate_payment_entry_against_benefit_claim", + "section_break_5", + "accounts", + "condition_and_formula", + "condition", + "amount", + "amount_based_on_formula", + "formula", + "column_break_28", + "help" + ], + "fields": [ + { + "fieldname": "salary_component", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Name", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "salary_component_abbr", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Abbr", + "print_width": "120px", + "reqd": 1, + "width": "120px" + }, + { + "fieldname": "type", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Type", + "options": "Earning\nDeduction", + "reqd": 1 + }, + { + "default": "1", + "depends_on": "eval:doc.type == \"Earning\"", + "fieldname": "is_tax_applicable", + "fieldtype": "Check", + "label": "Is Tax Applicable" + }, + { + "default": "1", + "fieldname": "depends_on_payment_days", + "fieldtype": "Check", + "label": "Depends on Payment Days", + "print_hide": 1 + }, + { + "default": "0", + "fieldname": "do_not_include_in_total", + "fieldtype": "Check", + "label": "Do Not Include in Total" + }, + { + "default": "0", + "depends_on": "eval:doc.is_tax_applicable && doc.type=='Earning'", + "fieldname": "deduct_full_tax_on_selected_payroll_date", + "fieldtype": "Check", + "label": "Deduct Full Tax on Selected Payroll Date" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, + { + "fieldname": "description", + "fieldtype": "Small Text", + "in_list_view": 1, + "label": "Description" + }, + { + "default": "0", + "description": "If selected, the value specified or calculated in this component will not contribute to the earnings or deductions. However, it's value can be referenced by other components that can be added or deducted. ", + "fieldname": "statistical_component", + "fieldtype": "Check", + "label": "Statistical Component" + }, + { + "depends_on": "eval:doc.type==\"Earning\" && doc.statistical_component!=1", + "fieldname": "flexible_benefits", + "fieldtype": "Section Break", + "label": "Flexible Benefits" + }, + { + "default": "0", + "fieldname": "is_flexible_benefit", + "fieldtype": "Check", + "label": "Is Flexible Benefit" + }, + { + "depends_on": "is_flexible_benefit", + "fieldname": "max_benefit_amount", + "fieldtype": "Currency", + "label": "Max Benefit Amount (Yearly)" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "default": "0", + "depends_on": "is_flexible_benefit", + "fieldname": "pay_against_benefit_claim", + "fieldtype": "Check", + "label": "Pay Against Benefit Claim" + }, + { + "default": "0", + "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.create_separate_payment_entry_against_benefit_claim !=1", + "fieldname": "only_tax_impact", + "fieldtype": "Check", + "label": "Only Tax Impact (Cannot Claim But Part of Taxable Income)" + }, + { + "default": "0", + "depends_on": "eval:doc.is_flexible_benefit == 1 & doc.only_tax_impact !=1", + "fieldname": "create_separate_payment_entry_against_benefit_claim", + "fieldtype": "Check", + "label": "Create Separate Payment Entry Against Benefit Claim" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Deduction\"", + "fieldname": "variable_based_on_taxable_salary", + "fieldtype": "Check", + "label": "Variable Based On Taxable Salary" + }, + { + "depends_on": "eval:doc.statistical_component != 1", + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Accounts" + }, + { + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Salary Component Account" + }, + { + "collapsible": 1, + "depends_on": "eval:doc.is_flexible_benefit != 1 && doc.variable_based_on_taxable_salary != 1", + "fieldname": "condition_and_formula", + "fieldtype": "Section Break", + "label": "Condition and Formula" + }, + { + "fieldname": "condition", + "fieldtype": "Code", + "label": "Condition" + }, + { + "default": "0", + "fieldname": "amount_based_on_formula", + "fieldtype": "Check", + "label": "Amount based on formula" + }, + { + "depends_on": "amount_based_on_formula", + "fieldname": "formula", + "fieldtype": "Code", + "label": "Formula" + }, + { + "depends_on": "eval:doc.amount_based_on_formula!==1", + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "help", + "fieldtype": "HTML", + "label": "Help", + "options": "

Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + }, + { + "default": "0", + "fieldname": "round_to_the_nearest_integer", + "fieldtype": "Check", + "label": "Round to the Nearest Integer" + }, + { + "default": "0", + "depends_on": "eval:doc.type == \"Deduction\" && !doc.variable_based_on_taxable_salary", + "description": "If checked, the full amount will be deducted from taxable income before calculating income tax. Otherwise, it can be exempted via Employee Tax Exemption Declaration.", + "fieldname": "exempted_from_income_tax", + "fieldtype": "Check", + "label": "Exempted from Income Tax" + } + ], + "icon": "fa fa-flag", + "links": [], + "modified": "2020-04-24 14:50:28.994054", + "modified_by": "Administrator", + "module": "HR", + "name": "Salary Component", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "share": 1, + "write": 1 + }, + { + "read": 1, + "role": "Employee" + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_component/test_records.json b/erpnext/hr/doctype/salary_component/test_records.json index 7b22b481f3..104b44ffa1 100644 --- a/erpnext/hr/doctype/salary_component/test_records.json +++ b/erpnext/hr/doctype/salary_component/test_records.json @@ -3,14 +3,12 @@ "doctype": "Salary Component", "salary_component": "_Test Basic Salary", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { "doctype": "Salary Component", "salary_component": "_Test Allowance", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { @@ -27,14 +25,12 @@ "doctype": "Salary Component", "salary_component": "Basic", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 }, { "doctype": "Salary Component", "salary_component": "Leave Encashment", "type": "Earning", - "is_payable": 1, "is_tax_applicable": 1 } ] \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_component/test_salary_component.py b/erpnext/hr/doctype/salary_component/test_salary_component.py index 965cc9e9ff..4f7db0c71c 100644 --- a/erpnext/hr/doctype/salary_component/test_salary_component.py +++ b/erpnext/hr/doctype/salary_component/test_salary_component.py @@ -18,6 +18,5 @@ def create_salary_component(component_name, **args): "doctype": "Salary Component", "salary_component": component_name, "type": args.get("type") or "Earning", - "is_payable": args.get("is_payable") or 1, "is_tax_applicable": args.get("is_tax_applicable") or 1 }).insert() diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json index bde735d3bc..545f56a0b6 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/hr/doctype/salary_detail/salary_detail.json @@ -12,6 +12,7 @@ "deduct_full_tax_on_selected_payroll_date", "depends_on_payment_days", "is_tax_applicable", + "exempted_from_income_tax", "is_flexible_benefit", "variable_based_on_taxable_salary", "section_break_2", @@ -62,6 +63,7 @@ }, { "default": "0", + "depends_on": "eval:doc.parentfield=='earnings'", "fetch_from": "salary_component.is_tax_applicable", "fieldname": "is_tax_applicable", "fieldtype": "Check", @@ -71,6 +73,7 @@ }, { "default": "0", + "depends_on": "eval:doc.parentfield=='earnings'", "fetch_from": "salary_component.is_flexible_benefit", "fieldname": "is_flexible_benefit", "fieldtype": "Check", @@ -80,6 +83,7 @@ }, { "default": "0", + "depends_on": "eval:doc.parentfield=='deductions'", "fetch_from": "salary_component.variable_based_on_taxable_salary", "fieldname": "variable_based_on_taxable_salary", "fieldtype": "Check", @@ -187,11 +191,20 @@ "fieldtype": "HTML", "label": "Condition and Formula Help", "options": "

Condition and Formula Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + }, + { + "default": "0", + "depends_on": "eval:doc.parentfield=='deductions'", + "fetch_from": "salary_component.exempted_from_income_tax", + "fieldname": "exempted_from_income_tax", + "fieldtype": "Check", + "label": "Exempted from Income Tax", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2019-12-31 17:15:25.646689", + "modified": "2020-04-24 20:00:16.475295", "modified_by": "Administrator", "module": "HR", "name": "Salary Detail", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 223c4e3e3b..40fe572d75 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -451,7 +451,8 @@ class SalarySlip(TransactionBase): 'is_flexible_benefit': struct_row.is_flexible_benefit, 'variable_based_on_taxable_salary': struct_row.variable_based_on_taxable_salary, 'deduct_full_tax_on_selected_payroll_date': struct_row.deduct_full_tax_on_selected_payroll_date, - 'additional_amount': amount if struct_row.get("is_additional_component") else 0 + 'additional_amount': amount if struct_row.get("is_additional_component") else 0, + 'exempted_from_income_tax': struct_row.exempted_from_income_tax }) else: if struct_row.get("is_additional_component"): @@ -482,10 +483,12 @@ class SalarySlip(TransactionBase): return self.calculate_variable_tax(payroll_period, tax_component) def calculate_variable_tax(self, payroll_period, tax_component): + # get Tax slab from salary structure assignment for the employee and payroll period + tax_slab = self.get_income_tax_slabs(payroll_period) + # get remaining numbers of sub-period (period for which one salary is processed) remaining_sub_periods = get_period_factor(self.employee, self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1] - # get taxable_earnings, paid_taxes for previous period previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date, self.start_date) previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component) @@ -507,23 +510,27 @@ class SalarySlip(TransactionBase): unclaimed_taxable_benefits += current_taxable_earnings_for_payment_days.flexi_benefits # Total exemption amount based on tax exemption declaration - total_exemption_amount, other_incomes = self.get_total_exemption_amount_and_other_incomes(payroll_period) + total_exemption_amount = self.get_total_exemption_amount(payroll_period, tax_slab) + + #Employee Other Incomes + other_incomes = self.get_income_form_other_sources(payroll_period) or 0.0 # Total taxable earnings including additional and other incomes total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \ + current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount - + # Total taxable earnings without additional earnings with full tax total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax # Structured tax amount - total_structured_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings_without_full_tax_addl_components) + total_structured_tax_amount = self.calculate_tax_by_tax_slab( + total_taxable_earnings_without_full_tax_addl_components, tax_slab) current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods - + # Total taxable earnings with additional earnings with full tax full_tax_on_additional_earnings = 0.0 if current_additional_earnings_with_full_tax: - total_tax_amount = self.calculate_tax_by_tax_slab(payroll_period, total_taxable_earnings) + total_tax_amount = self.calculate_tax_by_tax_slab(total_taxable_earnings, tax_slab) full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings @@ -532,12 +539,30 @@ class SalarySlip(TransactionBase): return current_tax_amount + def get_income_tax_slabs(self, payroll_period): + income_tax_slab, ss_assignment_name = frappe.db.get_value("Salary Structure Assignment", + {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, ["income_tax_slab", 'name']) + + if not income_tax_slab: + frappe.throw(_("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name)) + + income_tax_slab_doc = frappe.get_doc("Income Tax Slab", income_tax_slab) + if income_tax_slab_doc.disabled: + frappe.throw(_("Income Tax Slab: {0} is disabled").format(income_tax_slab)) + + if getdate(income_tax_slab_doc.effective_from) > getdate(payroll_period.start_date): + frappe.throw(_("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}") + .format(payroll_period.start_date)) + + return income_tax_slab_doc + + def get_taxable_earnings_for_prev_period(self, start_date, end_date): taxable_earnings = frappe.db.sql(""" select sum(sd.amount) from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name - where + where sd.parentfield='earnings' and sd.is_tax_applicable=1 and is_flexible_benefit=0 @@ -550,7 +575,28 @@ class SalarySlip(TransactionBase): "from_date": start_date, "to_date": end_date }) - return flt(taxable_earnings[0][0]) if taxable_earnings else 0 + taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0 + + exempted_amount = frappe.db.sql(""" + select sum(sd.amount) + from + `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name + where + sd.parentfield='deductions' + and sd.exempted_from_income_tax=1 + and is_flexible_benefit=0 + and ss.docstatus=1 + and ss.employee=%(employee)s + and ss.start_date between %(from_date)s and %(to_date)s + and ss.end_date between %(from_date)s and %(to_date)s + """, { + "employee": self.employee, + "from_date": start_date, + "to_date": end_date + }) + exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0 + + return taxable_earnings - exempted_amount def get_tax_paid_in_period(self, start_date, end_date, tax_component): # find total_tax_paid, tax paid for benefit, additional_salary @@ -610,6 +656,13 @@ class SalarySlip(TransactionBase): else: taxable_earnings += amount + for ded in self.deductions: + if ded.exempted_from_income_tax: + amount = ded.amount + if based_on_payment_days: + amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0] + taxable_earnings -= flt(amount) + return frappe._dict({ "taxable_earnings": taxable_earnings, "additional_income": additional_income, @@ -672,40 +725,63 @@ class SalarySlip(TransactionBase): return total_benefits_paid - total_benefits_claimed - def get_total_exemption_amount_and_other_incomes(self, payroll_period): - total_exemption_amount, other_incomes = 0, 0 - if self.deduct_tax_for_unsubmitted_tax_exemption_proof: - exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", - {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["exemption_amount", "income_from_other_sources"]) - if exemption_proof: - total_exemption_amount, other_incomes = exemption_proof - else: - declaration = frappe.db.get_value("Employee Tax Exemption Declaration", - {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, - ["total_exemption_amount", "income_from_other_sources"]) - if declaration: - total_exemption_amount, other_incomes = declaration + def get_total_exemption_amount(self, payroll_period, tax_slab): + total_exemption_amount = 0 + if tax_slab.allow_tax_exemption: + if self.deduct_tax_for_unsubmitted_tax_exemption_proof: + exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission", + {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, + ["exemption_amount"]) + if exemption_proof: + total_exemption_amount = exemption_proof + else: + declaration = frappe.db.get_value("Employee Tax Exemption Declaration", + {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1}, + ["total_exemption_amount"]) + if declaration: + total_exemption_amount = declaration - return total_exemption_amount, other_incomes + total_exemption_amount += flt(tax_slab.standard_tax_exemption_amount) - def calculate_tax_by_tax_slab(self, payroll_period, annual_taxable_earning): - payroll_period_obj = frappe.get_doc("Payroll Period", payroll_period) - annual_taxable_earning -= flt(payroll_period_obj.standard_tax_exemption_amount) + return total_exemption_amount + + def get_income_form_other_sources(self, payroll_period): + return frappe.get_all("Employee Other Income", + filters={ + "employee": self.employee, + "payroll_period": payroll_period.name, + "company": self.company, + "docstatus": 1 + }, + fields="SUM(amount) as total_amount" + )[0].total_amount + + def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab): data = self.get_data_for_eval() data.update({"annual_taxable_earning": annual_taxable_earning}) - taxable_amount = 0 - for slab in payroll_period_obj.taxable_salary_slabs: + tax_amount = 0 + for slab in tax_slab.slabs: if slab.condition and not self.eval_tax_slab_condition(slab.condition, data): continue if not slab.to_amount and annual_taxable_earning > slab.from_amount: - taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 continue if annual_taxable_earning > slab.from_amount and annual_taxable_earning < slab.to_amount: - taxable_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 + tax_amount += (annual_taxable_earning - slab.from_amount) * slab.percent_deduction *.01 elif annual_taxable_earning > slab.from_amount and annual_taxable_earning > slab.to_amount: - taxable_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 - return taxable_amount + tax_amount += (slab.to_amount - slab.from_amount) * slab.percent_deduction * .01 + + # other taxes and charges on income tax + for d in tax_slab.other_taxes_and_charges: + if flt(d.min_taxable_income) and flt(d.min_taxable_income) > tax_amount: + continue + + if flt(d.max_taxable_income) and flt(d.max_taxable_income) < tax_amount: + continue + + tax_amount += tax_amount * flt(d.percent) / 100 + + return tax_amount def eval_tax_slab_condition(self, condition, data): try: diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index ecccac7d41..73bb19e9ee 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -47,10 +47,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.payment_days, no_of_days[0]) self.assertEqual(ss.earnings[0].amount, 50000) self.assertEqual(ss.earnings[1].amount, 3000) - self.assertEqual(ss.deductions[0].amount, 5000) - self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 68000.0) def test_salary_slip_with_holidays_excluded(self): no_of_days = self.get_no_of_days() @@ -67,10 +64,7 @@ class TestSalarySlip(unittest.TestCase): self.assertEqual(ss.earnings[0].amount, 50000) self.assertEqual(ss.earnings[0].default_amount, 50000) self.assertEqual(ss.earnings[1].amount, 3000) - self.assertEqual(ss.deductions[0].amount, 5000) - self.assertEqual(ss.deductions[1].amount, 5000) self.assertEqual(ss.gross_pay, 78000) - self.assertEqual(ss.net_pay, 68000.0) def test_payment_days(self): no_of_days = self.get_no_of_days() @@ -80,8 +74,8 @@ class TestSalarySlip(unittest.TestCase): # set joinng date in the same month make_employee("test_employee@salary.com") if getdate(nowdate()).day >= 15: - date_of_joining = getdate(add_days(nowdate(),-10)) relieving_date = getdate(add_days(nowdate(),-10)) + date_of_joining = getdate(add_days(nowdate(),-10)) elif getdate(nowdate()).day < 15 and getdate(nowdate()).day >= 5: date_of_joining = getdate(add_days(nowdate(),-3)) relieving_date = getdate(add_days(nowdate(),-3)) @@ -131,9 +125,7 @@ class TestSalarySlip(unittest.TestCase): def test_email_salary_slip(self): frappe.db.sql("delete from `tabEmail Queue`") - hr_settings = frappe.get_doc("HR Settings", "HR Settings") - hr_settings.email_salary_slip_to_employee = 1 - hr_settings.save() + frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 1) make_employee("test_employee@salary.com") ss = make_employee_salary_slip("test_employee@salary.com", "Monthly") @@ -203,8 +195,11 @@ class TestSalarySlip(unittest.TestCase): # as per assigned salary structure 40500 in monthly salary so 236000*5/100/12 frappe.db.sql("""delete from `tabPayroll Period`""") frappe.db.sql("""delete from `tabSalary Component`""") + payroll_period = create_payroll_period() - create_tax_slab(payroll_period) + + create_tax_slab(payroll_period, allow_tax_exemption=True) + employee = make_employee("test_tax@salary.slip") delete_docs = [ "Salary Slip", @@ -230,8 +225,7 @@ class TestSalarySlip(unittest.TestCase): payroll_period, deduct_random=False) tax_paid = get_tax_paid_in_period(employee) - # total taxable income 586000, 250000 @ 5%, 86000 @ 20% ie. 12500 + 17200 - annual_tax = 113568 + annual_tax = 113589.0 try: self.assertEqual(tax_paid, annual_tax) except AssertionError: @@ -255,8 +249,7 @@ class TestSalarySlip(unittest.TestCase): raise # Submit proof for total 120000 - data["proof-1"] = create_proof_submission(employee, payroll_period, 50000) - data["proof-2"] = create_proof_submission(employee, payroll_period, 70000) + data["proof"] = create_proof_submission(employee, payroll_period, 120000) # Submit benefit claim for total 50000 data["benefit-1"] = create_benefit_claim(employee, payroll_period, 15000, "Medical Allowance") @@ -270,7 +263,7 @@ class TestSalarySlip(unittest.TestCase): # total taxable income 416000, 166000 @ 5% ie. 8300 try: - self.assertEqual(tax_paid, 88608) + self.assertEqual(tax_paid, 82389.0) except AssertionError: print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n") raise @@ -285,7 +278,7 @@ class TestSalarySlip(unittest.TestCase): # total taxable income 566000, 250000 @ 5%, 66000 @ 20%, 12500 + 13200 tax_paid = get_tax_paid_in_period(employee) try: - self.assertEqual(tax_paid, 121211) + self.assertEqual(tax_paid, annual_tax) except AssertionError: print("\nSalary Slip - Tax calculation failed on following case\n", data, "\n") raise @@ -327,6 +320,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" + employee = frappe.db.get_value("Employee", {"user_id": user}) salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee) salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) @@ -456,17 +450,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No { "salary_component": 'Professional Tax', "abbr":'PT', - "condition": 'base > 10000', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1 + "amount": 200, + "exempted_from_income_tax": 1 + }, { "salary_component": 'TDS', "abbr":'T', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1, "depends_on_payment_days": 0, "variable_based_on_taxable_salary": 1, "round_to_the_nearest_integer": 1 @@ -477,9 +469,7 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No "salary_component": 'TDS', "abbr":'T', "condition": 'employment_type=="Intern"', - "formula": 'base*.1', "type": "Deduction", - "amount_based_on_formula": 1, "round_to_the_nearest_integer": 1 }) if setup or test_tax: @@ -535,29 +525,47 @@ def create_benefit_claim(employee, payroll_period, amount, component): }).submit() return claim_date -def create_tax_slab(payroll_period): - data = [ +def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False): + if frappe.db.exists("Income Tax Slab", "Tax Slab: " + payroll_period.name): + return + + slabs = [ { "from_amount": 250000, "to_amount": 500000, - "percent_deduction": 5.2, + "percent_deduction": 5, "condition": "annual_taxable_earning > 500000" }, { "from_amount": 500001, "to_amount": 1000000, - "percent_deduction": 20.8 + "percent_deduction": 20 }, { "from_amount": 1000001, - "percent_deduction": 31.2 + "percent_deduction": 30 } ] - payroll_period.taxable_salary_slabs = [] - for item in data: - payroll_period.append("taxable_salary_slabs", item) - payroll_period.standard_tax_exemption_amount = 52500 - payroll_period.save() + + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab: " + payroll_period.name + income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) + + if allow_tax_exemption: + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = 50000 + + for item in slabs: + income_tax_slab.append("slabs", item) + + income_tax_slab.append("other_taxes_and_charges", { + "description": "cess", + "percent": 4 + }) + + income_tax_slab.save() + if not dont_submit: + income_tax_slab.submit() def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): deducted_dates = [] diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.js b/erpnext/hr/doctype/salary_structure/salary_structure.js index 7120448100..7748403513 100755 --- a/erpnext/hr/doctype/salary_structure/salary_structure.js +++ b/erpnext/hr/doctype/salary_structure/salary_structure.js @@ -82,6 +82,7 @@ frappe.ui.form.on('Salary Structure', { {fieldname:"employee", fieldtype: "Link", options: "Employee", label: __("Employee")}, {fieldname:'base_variable', fieldtype:'Section Break'}, {fieldname:'from_date', fieldtype:'Date', label: __('From Date'), "reqd": 1}, + {fieldname:'income_tax_slab', fieldtype:'Link', label: __('Income Tax Slab'), options: 'Income Tax Slab'}, {fieldname:'base_col_br', fieldtype:'Column Break'}, {fieldname:'base', fieldtype:'Currency', label: __('Base')}, {fieldname:'variable', fieldtype:'Currency', label: __('Variable')} diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py index 568277f8a7..df76458fe0 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/salary_structure.py @@ -16,6 +16,7 @@ class SalaryStructure(Document): self.validate_amount() self.strip_condition_and_formula_fields() self.validate_max_benefits_with_flexi() + self.validate_component_based_on_tax_slab() def set_missing_values(self): overwritten_fields = ["depends_on_payment_days", "variable_based_on_taxable_salary", "is_tax_applicable", "is_flexible_benefit"] @@ -34,6 +35,12 @@ class SalaryStructure(Document): for fieldname in overwritten_fields_if_missing: d.set(fieldname, component_default_value.get(fieldname)) + def validate_component_based_on_tax_slab(self): + for row in self.deductions: + if row.variable_based_on_taxable_salary and (row.amount or row.formula): + frappe.throw(_("Row #{0}: Cannot set amount or formula for Salary Component {1} with Variable Based On Taxable Salary") + .format(row.idx, row.salary_component)) + def validate_amount(self): if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet: frappe.throw(_("Net pay cannot be negative")) @@ -82,21 +89,23 @@ class SalaryStructure(Document): @frappe.whitelist() def assign_salary_structure(self, company=None, grade=None, department=None, designation=None,employee=None, - from_date=None, base=None,variable=None): + from_date=None, base=None, variable=None, income_tax_slab=None): employees = self.get_employees(company= company, grade= grade,department= department,designation= designation,name=employee) if employees: if len(employees) > 20: frappe.enqueue(assign_salary_structure_for_employees, timeout=600, - employees=employees, salary_structure=self,from_date=from_date, base=base,variable=variable) + employees=employees, salary_structure=self,from_date=from_date, + base=base, variable=variable, income_tax_slab=income_tax_slab) else: - assign_salary_structure_for_employees(employees, self, from_date=from_date, base=base,variable=variable) + assign_salary_structure_for_employees(employees, self, from_date=from_date, + base=base, variable=variable, income_tax_slab=income_tax_slab) else: frappe.msgprint(_("No Employee Found")) -def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None,variable=None): +def assign_salary_structure_for_employees(employees, salary_structure, from_date=None, base=None, variable=None, income_tax_slab=None): salary_structures_assignments = [] existing_assignments_for = get_existing_assignments(employees, salary_structure, from_date) count=0 @@ -105,7 +114,8 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date continue count +=1 - salary_structures_assignment = create_salary_structures_assignment(employee, salary_structure, from_date, base, variable) + salary_structures_assignment = create_salary_structures_assignment(employee, + salary_structure, from_date, base, variable, income_tax_slab) salary_structures_assignments.append(salary_structures_assignment) frappe.publish_progress(count*100/len(set(employees) - set(existing_assignments_for)), title = _("Assigning Structures...")) @@ -113,7 +123,7 @@ def assign_salary_structure_for_employees(employees, salary_structure, from_date frappe.msgprint(_("Structures have been assigned successfully")) -def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable): +def create_salary_structures_assignment(employee, salary_structure, from_date, base, variable, income_tax_slab=None): assignment = frappe.new_doc("Salary Structure Assignment") assignment.employee = employee assignment.salary_structure = salary_structure.name @@ -121,6 +131,7 @@ def create_salary_structures_assignment(employee, salary_structure, from_date, b assignment.from_date = from_date assignment.base = base assignment.variable = variable + assignment.income_tax_slab = income_tax_slab assignment.save(ignore_permissions = True) assignment.submit() return assignment.name diff --git a/erpnext/hr/doctype/salary_structure/test_salary_structure.py b/erpnext/hr/doctype/salary_structure/test_salary_structure.py index 6ca6dfd2c0..c1869f05d7 100644 --- a/erpnext/hr/doctype/salary_structure/test_salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/test_salary_structure.py @@ -9,8 +9,9 @@ from frappe.utils.make_random import get_random from frappe.utils import nowdate, add_days, add_years, getdate, add_months from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.hr.doctype.salary_slip.test_salary_slip import make_earning_salary_component,\ - make_deduction_salary_component, make_employee_salary_slip + make_deduction_salary_component, make_employee_salary_slip, create_tax_slab from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import create_payroll_period test_dependencies = ["Fiscal Year"] @@ -70,10 +71,8 @@ class TestSalaryStructure(unittest.TestCase): self.assertEqual(sal_slip.get("earnings")[1].amount, 3000) self.assertEqual(sal_slip.get("earnings")[2].amount, 25000) self.assertEqual(sal_slip.get("gross_pay"), 78000) - self.assertEqual(sal_slip.get("deductions")[0].amount, 5000) - self.assertEqual(sal_slip.get("deductions")[1].amount, 5000) - self.assertEqual(sal_slip.get("total_deduction"), 10000) - self.assertEqual(sal_slip.get("net_pay"), 68000) + self.assertEqual(sal_slip.get("deductions")[0].amount, 200) + self.assertEqual(sal_slip.get("net_pay"), 78000 - sal_slip.get("total_deduction")) def test_whitespaces_in_formula_conditions_fields(self): salary_structure = make_salary_structure("Salary Structure Sample", "Monthly", dont_submit=True) @@ -112,6 +111,7 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do test_tax=False, company=None): if test_tax: frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) + if not frappe.db.exists('Salary Structure', salary_structure): details = { "doctype": "Salary Structure", @@ -124,7 +124,8 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do } if other_details and isinstance(other_details, dict): details.update(other_details) - salary_structure_doc = frappe.get_doc(details).insert() + salary_structure_doc = frappe.get_doc(details) + salary_structure_doc.insert() if not dont_submit: salary_structure_doc.submit() else: @@ -139,13 +140,18 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) + + payroll_period = create_payroll_period() + create_tax_slab(payroll_period, allow_tax_exemption=True) + salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment.employee = employee salary_structure_assignment.base = 50000 salary_structure_assignment.variable = 5000 - salary_structure_assignment.from_date = from_date or add_months(nowdate(), -1) + salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1) salary_structure_assignment.salary_structure = salary_structure salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.save(ignore_permissions=True) + salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.submit() return salary_structure_assignment diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js index 56a05e0495..818e853154 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -20,6 +20,16 @@ frappe.ui.form.on('Salary Structure Assignment', { } } }); + + frm.set_query("income_tax_slab", function() { + return { + filters: { + company: frm.doc.company, + docstatus: 1, + disabled: 0 + } + } + }); }, employee: function(frm) { if(frm.doc.employee){ diff --git a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json index 380c889477..0098aa8ec8 100644 --- a/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/hr/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -10,11 +10,12 @@ "employee", "employee_name", "department", - "designation", + "company", "column_break_6", + "designation", "salary_structure", "from_date", - "company", + "income_tax_slab", "section_break_7", "base", "column_break_9", @@ -113,11 +114,17 @@ "options": "Salary Structure Assignment", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "income_tax_slab", + "fieldtype": "Link", + "label": "Income Tax Slab", + "options": "Income Tax Slab" } ], "is_submittable": 1, "links": [], - "modified": "2019-12-31 16:35:34.415099", + "modified": "2020-04-25 18:24:23.617088", "modified_by": "Administrator", "module": "HR", "name": "Salary Structure Assignment", diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index ef276001c5..cd125108c6 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -9,6 +9,8 @@ from frappe.model.document import Document from frappe.desk.form import assign_to from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +class DuplicateDeclarationError(frappe.ValidationError): pass + class EmployeeBoardingController(Document): ''' Create the project and the task for the boarding process @@ -226,6 +228,17 @@ def get_employee_leave_policy(employee): else: frappe.throw(_("Please set leave policy for employee {0} in Employee / Grade record").format(employee)) +def validate_duplicate_exemption_for_payroll_period(doctype, docname, payroll_period, employee): + existing_record = frappe.db.exists(doctype, { + "payroll_period": payroll_period, + "employee": employee, + 'docstatus': ['<', 2], + 'name': ['!=', docname] + }) + if existing_record: + frappe.throw(_("{0} already exists for employee {1} and period {2}") + .format(doctype, employee, payroll_period), DuplicateDeclarationError) + def validate_tax_declaration(declarations): subcategories = [] for d in declarations: diff --git a/erpnext/patches.txt b/erpnext/patches.txt index eb2b35cc8e..a73e8c1926 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -669,4 +669,5 @@ erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse -erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign \ No newline at end of file +erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign +erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 diff --git a/erpnext/patches/v11_0/set_salary_component_properties.py b/erpnext/patches/v11_0/set_salary_component_properties.py index fa3605ba5f..83fb53d2a7 100644 --- a/erpnext/patches/v11_0/set_salary_component_properties.py +++ b/erpnext/patches/v11_0/set_salary_component_properties.py @@ -5,8 +5,7 @@ def execute(): frappe.reload_doc('hr', 'doctype', 'salary_detail') frappe.reload_doc('hr', 'doctype', 'salary_component') - frappe.db.sql("update `tabSalary Component` set is_payable=1, is_tax_applicable=1 where type='Earning'") - frappe.db.sql("update `tabSalary Component` set is_payable=0 where type='Deduction'") + frappe.db.sql("update `tabSalary Component` set is_tax_applicable=1 where type='Earning'") frappe.db.sql("""update `tabSalary Component` set variable_based_on_taxable_salary=1 where type='Deduction' and name in ('TDS', 'Tax Deducted at Source')""") diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py new file mode 100644 index 0000000000..a6aefac12a --- /dev/null +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -0,0 +1,99 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if not frappe.db.table_exists("Payroll Period"): + return + + for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income"): + frappe.reload_doc("hr", "doctype", doctype) + + + for company in frappe.get_all("Company"): + payroll_periods = frappe.db.sql(""" + SELECT + name, start_date, end_date, standard_tax_exemption_amount + FROM + `tabPayroll Period` + WHERE company=%s + ORDER BY start_date DESC + """, company.name, as_dict = 1) + + for i, period in enumerate(payroll_periods): + income_tax_slab = frappe.new_doc("Income Tax Slab") + income_tax_slab.name = "Tax Slab:" + period.name + + if i == 0: + income_tax_slab.disabled = 0 + else: + income_tax_slab.disabled = 1 + + income_tax_slab.effective_from = period.start_date + income_tax_slab.company = company.name + income_tax_slab.allow_tax_exemption = 1 + income_tax_slab.standard_tax_exemption_amount = period.standard_tax_exemption_amount + + income_tax_slab.flags.ignore_mandatory = True + income_tax_slab.submit() + + frappe.db.sql( + """ UPDATE `tabTaxable Salary Slab` + SET parent = %s , parentfield = 'slabs' , parenttype = "Income Tax Slab" + WHERE parent = %s + """, (income_tax_slab.name, period.name), as_dict = 1) + + if i == 0: + frappe.db.sql(""" + UPDATE + `tabSalary Structure Assignment` + set + income_tax_slab = %s + where + company = %s + and from_date >= %s + and docstatus < 2 + """, (income_tax_slab.name, company.name, period.start_date)) + + # move other incomes to separate document + migrated = [] + proofs = frappe.get_all("Employee Tax Exemption Proof Submission", + filters = {'docstatus': 1}, + fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + ) + for proof in proofs: + if proof.income_from_other_sources: + employee_other_income = frappe.new_doc("Employee Other Income") + employee_other_income.employee = proof.employee + employee_other_income.payroll_period = proof.payroll_period + employee_other_income.company = proof.company + employee_other_income.amount = proof.income_from_other_sources + + try: + employee_other_income.submit() + migrated.append([proof.employee, proof.payroll_period]) + except: + pass + + declerations = frappe.get_all("Employee Tax Exemption Declaration", + filters = {'docstatus': 1}, + fields =['payroll_period', 'employee', 'company', 'income_from_other_sources'] + ) + + for declaration in declerations: + if declaration.income_from_other_sources \ + and [declaration.employee, declaration.payroll_period] not in migrated: + employee_other_income = frappe.new_doc("Employee Other Income") + employee_other_income.employee = declaration.employee + employee_other_income.payroll_period = declaration.payroll_period + employee_other_income.company = declaration.company + employee_other_income.amount = declaration.income_from_other_sources + + try: + employee_other_income.submit() + except: + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 4be6804db5..b4e3558af6 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -531,12 +531,18 @@ def make_fixtures(company=None): def set_salary_components(docs): docs.extend([ - {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', 'description': 'Professional Tax', 'type': 'Deduction'}, - {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', 'description': 'Provident fund', 'type': 'Deduction'}, - {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', 'description': 'House Rent Allowance', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Basic', 'description': 'Basic', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Arrear', 'description': 'Arrear', 'type': 'Earning'}, - {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', 'description': 'Leave Encashment', 'type': 'Earning'} + {'doctype': 'Salary Component', 'salary_component': 'Professional Tax', + 'description': 'Professional Tax', 'type': 'Deduction', 'exempted_from_income_tax': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Provident Fund', + 'description': 'Provident fund', 'type': 'Deduction', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'House Rent Allowance', + 'description': 'House Rent Allowance', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Basic', + 'description': 'Basic', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Arrear', + 'description': 'Arrear', 'type': 'Earning', 'is_tax_applicable': 1}, + {'doctype': 'Salary Component', 'salary_component': 'Leave Encashment', + 'description': 'Leave Encashment', 'type': 'Earning', 'is_tax_applicable': 1} ]) def set_tax_withholding_category(company): From 5d5454ef1f0a074397a4163d85eef484d4944451 Mon Sep 17 00:00:00 2001 From: Marica Date: Sun, 26 Apr 2020 20:07:22 +0530 Subject: [PATCH 42/73] fix [ux]: Purchase Order Form Cleanup (#20932) * chore: Purchase Order Form Cleanup * fix: Get Items from popups cleanup * fix: Shift Set Target Warehouse next to Supply Raw Materials. Co-authored-by: Nabin Hait --- .../purchase_taxes_and_charges.json | 23 ++++----- .../doctype/purchase_order/purchase_order.js | 6 +-- .../purchase_order/purchase_order.json | 44 ++++++++++------- .../purchase_order_item.json | 42 ++++++++++------- .../purchase_order_item_supplied.json | 47 ++++++++++++++----- 5 files changed, 100 insertions(+), 62 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 1741869361..0e748f84bb 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-21 16:16:04", "doctype": "DocType", @@ -14,11 +15,11 @@ "col_break1", "account_head", "description", + "section_break_10", + "rate", "accounting_dimensions_section", "cost_center", "dimension_col_break", - "section_break_10", - "rate", "section_break_9", "tax_amount", "tax_amount_after_discount_amount", @@ -27,8 +28,7 @@ "base_tax_amount", "base_total", "base_tax_amount_after_discount_amount", - "item_wise_tax_detail", - "parenttype" + "item_wise_tax_detail" ], "fields": [ { @@ -53,6 +53,7 @@ }, { "columns": 2, + "default": "On Net Total", "fieldname": "charge_type", "fieldtype": "Select", "in_list_view": 1, @@ -196,15 +197,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "parenttype", - "fieldtype": "Data", - "hidden": 1, - "label": "Parenttype", - "oldfieldname": "parenttype", - "oldfieldtype": "Data", - "print_hide": 1 - }, { "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", @@ -217,11 +209,14 @@ ], "idx": 1, "istable": 1, - "modified": "2019-05-25 23:08:38.281025", + "links": [], + "modified": "2020-03-12 14:53:47.679439", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", "owner": "Administrator", "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index 3111a3a7d5..ed054aedb5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -365,9 +365,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( method: "erpnext.stock.doctype.material_request.material_request.make_purchase_order", source_doctype: "Material Request", target: me.frm, - setters: { - company: me.frm.doc.company - }, + setters: {}, get_query_filters: { material_request_type: "Purchase", docstatus: 1, @@ -384,7 +382,7 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( source_doctype: "Supplier Quotation", target: me.frm, setters: { - company: me.frm.doc.company + supplier: me.frm.doc.supplier }, get_query_filters: { docstatus: 1, diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json index 578858ca52..a4f60fbba5 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.json +++ b/erpnext/buying/doctype/purchase_order/purchase_order.json @@ -54,11 +54,6 @@ "items_section", "scan_barcode", "items", - "section_break_48", - "pricing_rules", - "raw_material_details", - "set_reserve_warehouse", - "supplied_items", "sb_last_purchase", "total_qty", "base_total", @@ -67,6 +62,11 @@ "total_net_weight", "total", "net_total", + "section_break_48", + "pricing_rules", + "raw_material_details", + "set_reserve_warehouse", + "supplied_items", "taxes_section", "tax_category", "column_break_50", @@ -105,23 +105,25 @@ "payment_schedule_section", "payment_terms_template", "payment_schedule", + "tracking_section", + "per_billed", + "column_break_75", + "per_received", "terms_section_break", "tc_name", "terms", "more_info", "status", "ref_sq", + "column_break_74", "party_account_currency", "inter_company_order_reference", - "column_break_74", - "per_received", - "per_billed", "column_break5", "letter_head", "select_print_heading", "column_break_86", - "group_same_items", "language", + "group_same_items", "subscription_section", "from_date", "to_date", @@ -220,7 +222,7 @@ "allow_on_submit": 1, "fieldname": "schedule_date", "fieldtype": "Date", - "label": "Reqd By Date" + "label": "Required By" }, { "allow_on_submit": 1, @@ -432,6 +434,7 @@ "fieldtype": "Section Break" }, { + "description": "Sets 'Warehouse' in each row of the Items table.", "fieldname": "set_warehouse", "fieldtype": "Link", "label": "Set Target Warehouse", @@ -827,6 +830,7 @@ "read_only": 1 }, { + "collapsible": 1, "fieldname": "payment_schedule_section", "fieldtype": "Section Break", "label": "Payment Terms" @@ -917,7 +921,8 @@ "fieldname": "inter_company_order_reference", "fieldtype": "Link", "label": "Inter Company Order Reference", - "options": "Sales Order" + "options": "Sales Order", + "read_only": 1 }, { "fieldname": "column_break_74", @@ -930,8 +935,6 @@ "in_list_view": 1, "label": "% Received", "no_copy": 1, - "oldfieldname": "per_received", - "oldfieldtype": "Currency", "print_hide": 1, "read_only": 1 }, @@ -942,8 +945,6 @@ "in_list_view": 1, "label": "% Billed", "no_copy": 1, - "oldfieldname": "per_billed", - "oldfieldtype": "Currency", "print_hide": 1, "read_only": 1 }, @@ -998,6 +999,7 @@ "print_hide": 1 }, { + "collapsible": 1, "fieldname": "subscription_section", "fieldtype": "Section Break", "label": "Subscription Section" @@ -1050,13 +1052,23 @@ "fieldtype": "Link", "label": "Set Reserve Warehouse", "options": "Warehouse" + }, + { + "collapsible": 1, + "fieldname": "tracking_section", + "fieldtype": "Section Break", + "label": "Tracking" + }, + { + "fieldname": "column_break_75", + "fieldtype": "Column Break" } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2020-04-17 13:04:28.185197", + "modified": "2020-04-24 12:13:14.186280", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order", 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 e37e1dd99d..7a52c28a0e 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -18,10 +18,6 @@ "col_break1", "image", "image_view", - "manufacture_details", - "manufacturer", - "column_break_14", - "manufacturer_part_no", "quantity_and_rate", "qty", "stock_uom", @@ -44,7 +40,6 @@ "base_amount", "pricing_rules", "is_free_item", - "is_fixed_asset", "section_break_29", "net_rate", "net_amount", @@ -52,11 +47,6 @@ "base_net_rate", "base_net_amount", "billed_amt", - "item_weight_details", - "weight_per_unit", - "total_weight", - "column_break_40", - "weight_uom", "warehouse_and_reference", "warehouse", "delivered_by_supplier", @@ -80,20 +70,31 @@ "column_break_60", "received_qty", "returned_qty", + "manufacture_details", + "manufacturer", + "column_break_14", + "manufacturer_part_no", + "more_info_section_break", + "is_fixed_asset", + "item_tax_rate", "accounting_details", "expense_account", "column_break_68", + "item_weight_details", + "weight_per_unit", + "total_weight", + "column_break_40", + "weight_uom", "accounting_dimensions_section", "cost_center", "dimension_col_break", "section_break_72", - "page_break", - "item_tax_rate" + "page_break" ], "fields": [ { "bold": 1, - "columns": 3, + "columns": 2, "fieldname": "item_code", "fieldtype": "Link", "in_list_view": 1, @@ -133,7 +134,7 @@ "fieldname": "schedule_date", "fieldtype": "Date", "in_list_view": 1, - "label": "Reqd By Date", + "label": "Required By", "oldfieldname": "schedule_date", "oldfieldtype": "Date", "print_hide": 1, @@ -216,15 +217,16 @@ "print_hide": 1 }, { + "columns": 1, "fieldname": "uom", "fieldtype": "Link", + "in_list_view": 1, "label": "UOM", "oldfieldname": "uom", "oldfieldtype": "Link", "options": "UOM", "print_width": "100px", - "reqd": 1, - "width": "100px" + "reqd": 1 }, { "fieldname": "conversion_factor", @@ -685,6 +687,7 @@ "fieldtype": "Column Break" }, { + "collapsible": 1, "fieldname": "manufacture_details", "fieldtype": "Section Break", "label": "Manufacture" @@ -717,12 +720,17 @@ "fieldtype": "Check", "label": "Is Fixed Asset", "read_only": 1 + }, + { + "fieldname": "more_info_section_break", + "fieldtype": "Section Break", + "label": "More Information" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-07 18:35:17.558928", + "modified": "2020-04-21 11:55:58.643393", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index 8435bbb06e..c3e1bf5303 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -1,20 +1,26 @@ { + "actions": [], "creation": "2013-02-22 01:27:42", "doctype": "DocType", "editable_grid": 1, + "engine": "InnoDB", "field_order": [ "main_item_code", - "rm_item_code", - "required_qty", - "supplied_qty", - "rate", - "amount", - "column_break_6", "bom_detail_no", - "reference_name", - "conversion_factor", "stock_uom", - "reserve_warehouse" + "conversion_factor", + "column_break_6", + "rm_item_code", + "reference_name", + "reserve_warehouse", + "section_break2", + "rate", + "col_break2", + "amount", + "section_break1", + "required_qty", + "col_break1", + "supplied_qty" ], "fields": [ { @@ -120,15 +126,34 @@ "in_list_view": 1, "label": "Supplied Qty", "read_only": 1 + }, + { + "fieldname": "section_break1", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break2", + "fieldtype": "Section Break" + }, + { + "fieldname": "col_break2", + "fieldtype": "Column Break" } ], "hide_toolbar": 1, "idx": 1, "istable": 1, - "modified": "2019-08-20 13:37:32.702068", + "links": [], + "modified": "2020-03-12 15:43:53.862897", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", "owner": "dhanalekshmi@webnotestech.com", - "permissions": [] + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC" } \ No newline at end of file From d78cf9725098af1ebe0f08df177d2a54bb1062e9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 26 Apr 2020 20:08:52 +0530 Subject: [PATCH 43/73] feat: Allow tax withholding category selection at invoice level (#20870) * feat: Allow tax withholding category selection at invoice level * fix: Linitng fixes * feat: TDS calculation using common PAN * fix: Add provision to deduct Lower TDS in purchase invoice * fix: Consider only ref docs company while computing TDS * fix: Default permission fixes * fix: Add validation for dates in fiscal year * fix: Undefined variable --- .../purchase_invoice/purchase_invoice.js | 15 +- .../purchase_invoice/purchase_invoice.json | 11 +- .../purchase_invoice/purchase_invoice.py | 2 +- .../tax_withholding_category.py | 128 +++++++++++++--- .../tds_computation_summary.py | 11 +- erpnext/patches.txt | 1 + .../add_permission_in_lower_deduction.py | 13 ++ .../lower_deduction_certificate/__init__.py | 0 .../lower_deduction_certificate.js | 8 + .../lower_deduction_certificate.json | 138 ++++++++++++++++++ .../lower_deduction_certificate.py | 26 ++++ .../test_lower_deduction_certificate.py | 10 ++ erpnext/regional/india/setup.py | 2 +- 13 files changed, 334 insertions(+), 31 deletions(-) create mode 100644 erpnext/patches/v12_0/add_permission_in_lower_deduction.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/__init__.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py create mode 100644 erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 9292b633fc..3cf4d5994a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -261,12 +261,25 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ price_list: this.frm.doc.buying_price_list }, function() { me.apply_pricing_rule(); - me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0; + me.frm.doc.tax_withholding_category = me.frm.supplier_tds; me.frm.set_df_property("apply_tds", "read_only", me.frm.supplier_tds ? 0 : 1); + me.frm.set_df_property("tax_withholding_category", "hidden", me.frm.supplier_tds ? 0 : 1); }) }, + apply_tds: function(frm) { + var me = this; + + if (!me.frm.doc.apply_tds) { + me.frm.set_value("tax_withholding_category", ''); + me.frm.set_df_property("tax_withholding_category", "hidden", 1); + } else { + me.frm.set_value("tax_withholding_category", me.frm.supplier_tds); + me.frm.set_df_property("tax_withholding_category", "hidden", 0); + } + }, + credit_to: function() { var me = this; if(this.frm.doc.credit_to) { diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 0e0945454c..98ba5c72ae 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -13,6 +13,7 @@ "supplier_name", "tax_id", "due_date", + "tax_withholding_category", "column_break1", "company", "posting_date", @@ -1294,13 +1295,21 @@ "fieldtype": "Check", "label": "Is Internal Supplier", "read_only": 1 + }, + { + "fieldname": "tax_withholding_category", + "fieldtype": "Link", + "hidden": 1, + "label": "Tax Withholding Category", + "options": "Tax Withholding Category", + "print_hide": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-04-17 13:05:25.199832", + "modified": "2020-04-18 13:05:25.199832", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 0283d304d7..b1ae194301 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1002,7 +1002,7 @@ class PurchaseInvoice(BuyingController): if not self.apply_tds: return - tax_withholding_details = get_party_tax_withholding_details(self) + tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category) if not tax_withholding_details: return diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 6c31e9efed..dd6b4fdc60 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -6,23 +6,42 @@ from __future__ import unicode_literals import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import flt +from frappe.utils import flt, getdate from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): pass -def get_party_tax_withholding_details(ref_doc): - tax_withholding_category = frappe.db.get_value('Supplier', ref_doc.supplier, 'tax_withholding_category') +def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): + + pan_no = '' + suppliers = [] + + if not tax_withholding_category: + tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan']) + if not tax_withholding_category: return + if not pan_no: + pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan') + + # Get others suppliers with the same PAN No + if pan_no: + suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})] + + if not suppliers: + suppliers.append(ref_doc.supplier) + fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company) tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company) if not tax_details: frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') .format(tax_withholding_category, ref_doc.company)) - tds_amount = get_tds_amount(ref_doc, tax_details, fy) + + tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company, + tax_details, fy, ref_doc.posting_date, pan_no) + tax_row = get_tax_row(tax_details, tds_amount) return tax_row @@ -51,6 +70,7 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year): frappe.throw(_("No Tax Withholding data found for the current Fiscal Year.")) def get_tax_row(tax_details, tds_amount): + return { "category": "Total", "add_deduct_tax": "Deduct", @@ -60,25 +80,36 @@ def get_tax_row(tax_details, tds_amount): "tax_amount": tds_amount } -def get_tds_amount(ref_doc, tax_details, fiscal_year_details): +def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None): fiscal_year, year_start_date, year_end_date = fiscal_year_details tds_amount = 0 tds_deducted = 0 - def _get_tds(amount): + def _get_tds(amount, rate): if amount <= 0: return 0 - return amount * tax_details.rate / 100 + return amount * rate / 100 + + ldc_name = frappe.db.get_value('Lower Deduction Certificate', + { + 'pan_no': pan_no, + 'fiscal_year': fiscal_year + }, 'name') + ldc = '' + + if ldc_name: + ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name) entries = frappe.db.sql(""" select voucher_no, credit from `tabGL Entry` - where party=%s and fiscal_year=%s and credit > 0 - """, (ref_doc.supplier, fiscal_year), as_dict=1) + where company = %s and + party in %s and fiscal_year=%s and credit > 0 + """, (company, tuple(suppliers), fiscal_year), as_dict=1) vouchers = [d.voucher_no for d in entries] - advance_vouchers = get_advance_vouchers(ref_doc.supplier, fiscal_year) + advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company) tds_vouchers = vouchers + advance_vouchers @@ -93,7 +124,20 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details): tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0 if tds_deducted: - tds_amount = _get_tds(ref_doc.net_total) + if ldc: + limit_consumed = frappe.db.get_value('Purchase Invoice', + { + 'supplier': ('in', suppliers), + 'apply_tds': 1, + 'docstatus': 1 + }, 'sum(net_total)') + + if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, + ldc.certificate_limit): + + tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + else: + tds_amount = _get_tds(net_total, tax_details.rate) else: supplier_credit_amount = frappe.get_all('Purchase Invoice Item', fields = ['sum(net_amount)'], @@ -106,43 +150,79 @@ def get_tds_amount(ref_doc, tax_details, fiscal_year_details): fields = ['sum(credit_in_account_currency)'], filters = { 'parent': ('in', vouchers), 'docstatus': 1, - 'party': ref_doc.supplier, + 'party': ('in', suppliers), 'reference_type': ('not in', ['Purchase Invoice']) }, as_list=1) supplier_credit_amount += (jv_supplier_credit_amt[0][0] if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0) - supplier_credit_amount += ref_doc.net_total + supplier_credit_amount += net_total - debit_note_amount = get_debit_note_amount(ref_doc.supplier, year_start_date, year_end_date) + debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date) supplier_credit_amount -= debit_note_amount if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold) or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): - tds_amount = _get_tds(supplier_credit_amount) + + if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total, + ldc.certificate_limit): + tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate, + tax_details) + else: + tds_amount = _get_tds(supplier_credit_amount, tax_details.rate) return tds_amount -def get_advance_vouchers(supplier, fiscal_year=None, company=None, from_date=None, to_date=None): +def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None): condition = "fiscal_year=%s" % fiscal_year + + if company: + condition += "and company =%s" % (company) if from_date and to_date: - condition = "company=%s and posting_date between %s and %s" % (company, from_date, to_date) + condition += "and posting_date between %s and %s" % (company, from_date, to_date) + + ## Appending the same supplier again if length of suppliers list is 1 + ## since tuple of single element list contains None, For example ('Test Supplier 1', ) + ## and the below query fails + if len(suppliers) == 1: + suppliers.append(suppliers[0]) return frappe.db.sql_list(""" select distinct voucher_no from `tabGL Entry` - where party=%s and %s and debit > 0 - """, (supplier, condition)) or [] + where party in %s and %s and debit > 0 + """, (tuple(suppliers), condition)) or [] -def get_debit_note_amount(supplier, year_start_date, year_end_date, company=None): - condition = "" +def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): + condition = "and 1=1" if company: condition = " and company=%s " % company + if len(suppliers) == 1: + suppliers.append(suppliers[0]) + return flt(frappe.db.sql(""" select abs(sum(net_total)) from `tabPurchase Invoice` - where supplier=%s %s and is_return=1 and docstatus=1 - and posting_date between %s and %s - """, (supplier, condition, year_start_date, year_end_date))) \ No newline at end of file + where supplier in %s and is_return=1 and docstatus=1 + and posting_date between %s and %s %s + """, (tuple(suppliers), year_start_date, year_end_date, condition))) + +def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): + if current_amount < (certificate_limit - deducted_amount): + return current_amount * rate/100 + else: + ltds_amount = (certificate_limit - deducted_amount) + tds_amount = current_amount - ltds_amount + + return ltds_amount * rate/100 + tds_amount * tax_details.rate/100 + +def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit): + valid = False + + if ((getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and + certificate_limit > deducted_amount): + valid = True + + return valid \ No newline at end of file diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index 2e805f8d3f..c7cfee74cb 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -44,9 +44,14 @@ def get_result(filters): out = [] for supplier in filters.supplier: tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category) - rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year][0] + rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year] + + if rate: + rate = rate[0] + try: account = [d.account for d in tds.accounts if d.company == filters.company][0] + except IndexError: account = [] total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account, @@ -76,7 +81,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): supplier_credit_amount = flt(sum([d.credit for d in entries])) vouchers = [d.voucher_no for d in entries] - vouchers += get_advance_vouchers(supplier, company=company, + vouchers += get_advance_vouchers([supplier], company=company, from_date=from_date, to_date=to_date) tds_deducted = 0 @@ -89,7 +94,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): """.format(', '.join(["'%s'" % d for d in vouchers])), (account, from_date, to_date, company))[0][0]) - debit_note_amount = get_debit_note_amount(supplier, from_date, to_date, company=company) + debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, company=company) total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount diff --git a/erpnext/patches.txt b/erpnext/patches.txt index a73e8c1926..801d583ab1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -661,6 +661,7 @@ erpnext.patches.v12_0.set_job_offer_applicant_email erpnext.patches.v12_0.create_irs_1099_field_united_states erpnext.patches.v12_0.move_bank_account_swift_number_to_bank erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 +erpnext.patches.v12_0.add_permission_in_lower_deduction erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom erpnext.patches.v12_0.rename_account_type_doctype erpnext.patches.v12_0.recalculate_requested_qty_in_bin diff --git a/erpnext/patches/v12_0/add_permission_in_lower_deduction.py b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py new file mode 100644 index 0000000000..af9bf74f30 --- /dev/null +++ b/erpnext/patches/v12_0/add_permission_in_lower_deduction.py @@ -0,0 +1,13 @@ +import frappe +from frappe.permissions import add_permission, update_permission_property + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + frappe.reload_doc('regional', 'doctype', 'Lower Deduction Certificate') + + add_permission('Lower Deduction Certificate', 'Accounts Manager', 0) + update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'write', 1) + update_permission_property('Lower Deduction Certificate', 'Accounts Manager', 0, 'create', 1) \ No newline at end of file diff --git a/erpnext/regional/doctype/lower_deduction_certificate/__init__.py b/erpnext/regional/doctype/lower_deduction_certificate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js new file mode 100644 index 0000000000..8257bf8a96 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Lower Deduction Certificate', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json new file mode 100644 index 0000000000..f48fe6f476 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.json @@ -0,0 +1,138 @@ +{ + "actions": [], + "autoname": "field:certificate_no", + "creation": "2020-03-10 23:12:10.072631", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "certificate_details_section", + "section_code", + "fiscal_year", + "column_break_3", + "certificate_no", + "section_break_3", + "supplier", + "column_break_7", + "pan_no", + "validity_details_section", + "valid_from", + "column_break_10", + "valid_upto", + "section_break_9", + "rate", + "column_break_14", + "certificate_limit" + ], + "fields": [ + { + "fieldname": "certificate_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Certificate No", + "reqd": 1, + "unique": 1 + }, + { + "fieldname": "section_code", + "fieldtype": "Select", + "label": "Section Code", + "options": "192\n193\n194\n194A\n194C\n194D\n194H\n194I\n194J\n194LA\n194LBB\n194LBC\n195", + "reqd": 1 + }, + { + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Deductee Details" + }, + { + "fieldname": "supplier", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier", + "options": "Supplier", + "reqd": 1 + }, + { + "fetch_from": "supplier.pan", + "fetch_if_empty": 1, + "fieldname": "pan_no", + "fieldtype": "Data", + "in_list_view": 1, + "label": "PAN No", + "reqd": 1 + }, + { + "fieldname": "validity_details_section", + "fieldtype": "Section Break", + "label": "Validity Details" + }, + { + "fieldname": "valid_upto", + "fieldtype": "Date", + "label": "Valid Upto", + "reqd": 1 + }, + { + "fieldname": "section_break_9", + "fieldtype": "Section Break" + }, + { + "fieldname": "rate", + "fieldtype": "Percent", + "label": "Rate Of TDS As Per Certificate", + "reqd": 1 + }, + { + "fieldname": "certificate_limit", + "fieldtype": "Currency", + "label": "Certificate Limit", + "reqd": 1 + }, + { + "fieldname": "certificate_details_section", + "fieldtype": "Section Break", + "label": "Certificate Details" + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_10", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_7", + "fieldtype": "Column Break" + }, + { + "fieldname": "valid_from", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Valid From", + "reqd": 1 + }, + { + "fieldname": "fiscal_year", + "fieldtype": "Link", + "label": "Fiscal Year", + "options": "Fiscal Year", + "reqd": 1 + } + ], + "links": [], + "modified": "2020-04-23 23:04:41.203721", + "modified_by": "Administrator", + "module": "Regional", + "name": "Lower Deduction Certificate", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py new file mode 100644 index 0000000000..e8a8ed8750 --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import getdate +from frappe.model.document import Document +from erpnext.accounts.utils import get_fiscal_year + +class LowerDeductionCertificate(Document): + def validate(self): + if getdate(self.valid_upto) < getdate(self.valid_from): + frappe.throw(_("Valid Upto date cannot be before Valid From date")) + + fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) + + if not (fiscal_year.year_start_date <= getdate(self.valid_from) \ + <= fiscal_year.year_end_date): + frappe.throw(_("Valid From date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + + if not (fiscal_year.year_start_date <= getdate(self.valid_upto) \ + <= fiscal_year.year_end_date): + frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year))) + diff --git a/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py new file mode 100644 index 0000000000..7e950206fc --- /dev/null +++ b/erpnext/regional/doctype/lower_deduction_certificate/test_lower_deduction_certificate.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestLowerDeductionCertificate(unittest.TestCase): + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index b4e3558af6..8593966cc3 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -61,7 +61,7 @@ def add_custom_roles_for_reports(): )).insert() def add_permissions(): - for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report'): + for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'): add_permission(doctype, 'All', 0) for role in ('Accounts Manager', 'Accounts User', 'System Manager'): add_permission(doctype, role, 0) From ba70e7e8bce2ccfb0c31699a8d0dcb2505f1453b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 20:17:48 +0530 Subject: [PATCH 44/73] Payroll based on attendance (#21258) * feat: Payroll based on attendance and leave * test: salary slip based 0n attendance * feat: Payroll based on attendance * fix: Codacy issues Co-authored-by: Anurag Mishra --- erpnext/hr/doctype/attendance/attendance.json | 7 +- erpnext/hr/doctype/attendance/attendance.py | 95 +++++++++------ .../hr/doctype/hr_settings/hr_settings.json | 20 +++- erpnext/hr/doctype/hr_settings/hr_settings.py | 3 + erpnext/hr/doctype/salary_slip/salary_slip.js | 15 ++- .../hr/doctype/salary_slip/salary_slip.json | 12 +- erpnext/hr/doctype/salary_slip/salary_slip.py | 111 ++++++++++++++---- .../doctype/salary_slip/test_salary_slip.py | 106 ++++++++++++++++- erpnext/patches.txt | 1 + .../v12_0/set_default_payroll_based_on.py | 6 + 10 files changed, 291 insertions(+), 85 deletions(-) create mode 100644 erpnext/patches/v12_0/set_default_payroll_based_on.py diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index eaca9f6ebe..906f6f77f2 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -87,11 +87,12 @@ "search_index": 1 }, { - "depends_on": "eval:doc.status==\"On Leave\"", + "depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)", "fieldname": "leave_type", "fieldtype": "Link", "in_standard_filter": 1, "label": "Leave Type", + "mandatory_depends_on": "eval:in_list([\"On Leave\", \"Half Day\"], doc.status)", "oldfieldname": "leave_type", "oldfieldtype": "Link", "options": "Leave Type" @@ -100,6 +101,7 @@ "fieldname": "leave_application", "fieldtype": "Link", "label": "Leave Application", + "no_copy": 1, "options": "Leave Application", "read_only": 1 }, @@ -175,7 +177,8 @@ "icon": "fa fa-ok", "idx": 1, "is_submittable": 1, - "modified": "2020-02-19 14:25:32.945842", + "links": [], + "modified": "2020-04-11 11:40:14.319496", "modified_by": "Administrator", "module": "HR", "name": "Attendance", diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 9e965dbc39..7355a56128 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -7,33 +7,15 @@ import frappe from frappe.utils import getdate, nowdate from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, get_datetime, get_datetime_str -from frappe.utils import update_progress_bar +from frappe.utils import cstr, get_datetime, format_date class Attendance(Document): - def validate_duplicate_record(self): - res = frappe.db.sql("""select name from `tabAttendance` where employee = %s and attendance_date = %s - and name != %s and docstatus != 2""", - (self.employee, getdate(self.attendance_date), self.name)) - if res: - frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) - - def check_leave_record(self): - leave_record = frappe.db.sql("""select leave_type, half_day, half_day_date from `tabLeave Application` - where employee = %s and %s between from_date and to_date and status = 'Approved' - and docstatus = 1""", (self.employee, self.attendance_date), as_dict=True) - if leave_record: - for d in leave_record: - if d.half_day_date == getdate(self.attendance_date): - self.status = 'Half Day' - frappe.msgprint(_("Employee {0} on Half day on {1}").format(self.employee, self.attendance_date)) - else: - self.status = 'On Leave' - self.leave_type = d.leave_type - frappe.msgprint(_("Employee {0} is on Leave on {1}").format(self.employee, self.attendance_date)) - - if self.status == "On Leave" and not leave_record: - frappe.throw(_("No leave record found for employee {0} for {1}").format(self.employee, self.attendance_date)) + def validate(self): + from erpnext.controllers.status_updater import validate_status + validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) + self.validate_attendance_date() + self.validate_duplicate_record() + self.check_leave_record() def validate_attendance_date(self): date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining") @@ -44,19 +26,52 @@ class Attendance(Document): elif date_of_joining and getdate(self.attendance_date) < getdate(date_of_joining): frappe.throw(_("Attendance date can not be less than employee's joining date")) + def validate_duplicate_record(self): + res = frappe.db.sql(""" + select name from `tabAttendance` + where employee = %s + and attendance_date = %s + and name != %s + and docstatus != 2 + """, (self.employee, getdate(self.attendance_date), self.name)) + if res: + frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) + + def check_leave_record(self): + leave_record = frappe.db.sql(""" + select leave_type, half_day, half_day_date + from `tabLeave Application` + where employee = %s + and %s between from_date and to_date + and status = 'Approved' + and docstatus = 1 + """, (self.employee, self.attendance_date), as_dict=True) + if leave_record: + for d in leave_record: + self.leave_type = d.leave_type + if d.half_day_date == getdate(self.attendance_date): + self.status = 'Half Day' + frappe.msgprint(_("Employee {0} on Half day on {1}") + .format(self.employee, format_date(self.attendance_date))) + else: + self.status = 'On Leave' + frappe.msgprint(_("Employee {0} is on Leave on {1}") + .format(self.employee, format_date(self.attendance_date))) + + if self.status in ("On Leave", "Half Day"): + if not leave_record: + frappe.msgprint(_("No leave record found for employee {0} on {1}") + .format(self.employee, format_date(self.attendance_date)), alert=1) + elif self.leave_type: + self.leave_type = None + self.leave_application = None + def validate_employee(self): emp = frappe.db.sql("select name from `tabEmployee` where name = %s and status = 'Active'", self.employee) if not emp: frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee)) - def validate(self): - from erpnext.controllers.status_updater import validate_status - validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) - self.validate_attendance_date() - self.validate_duplicate_record() - self.check_leave_record() - @frappe.whitelist() def get_events(start, end, filters=None): events = [] @@ -90,18 +105,20 @@ def add_attendance(events, start, end, conditions=None): if e not in events: events.append(e) -def mark_attendance(employee, attendance_date, status, shift=None): - employee_doc = frappe.get_doc('Employee', employee) +def mark_attendance(employee, attendance_date, status, shift=None, leave_type=None, ignore_validate=False): if not frappe.db.exists('Attendance', {'employee':employee, 'attendance_date':attendance_date, 'docstatus':('!=', '2')}): - doc_dict = { + company = frappe.db.get_value('Employee', employee, 'company') + attendance = frappe.get_doc({ 'doctype': 'Attendance', 'employee': employee, 'attendance_date': attendance_date, 'status': status, - 'company': employee_doc.company, - 'shift': shift - } - attendance = frappe.get_doc(doc_dict).insert() + 'company': company, + 'shift': shift, + 'leave_type': leave_type + }) + attendance.flags.ignore_validate = ignore_validate + attendance.insert() attendance.submit() return attendance.name diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index 90f49886f8..9161ed822a 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -13,10 +13,12 @@ "stop_birthday_reminders", "expense_approver_mandatory_in_expense_claim", "payroll_settings", + "payroll_based_on", + "max_working_hours_against_timesheet", "include_holidays_in_total_working_days", "disable_rounded_total", - "max_working_hours_against_timesheet", "column_break_11", + "daily_wages_fraction_for_half_day", "email_salary_slip_to_employee", "encrypt_salary_slips_in_emails", "password_policy", @@ -184,13 +186,27 @@ "fieldtype": "Link", "label": "Role Allowed to Create Backdated Leave Application", "options": "Role" + }, + { + "default": "Leave", + "fieldname": "payroll_based_on", + "fieldtype": "Select", + "label": "Calculate Working Days in Payroll based on", + "options": "Leave\nAttendance" + }, + { + "default": "0.5", + "description": "The fraction of daily wages to be paid for half-day attendance", + "fieldname": "daily_wages_fraction_for_half_day", + "fieldtype": "Float", + "label": "Daily Wages Fraction for Half Day" } ], "icon": "fa fa-cog", "idx": 1, "issingle": 1, "links": [], - "modified": "2020-01-06 18:46:30.189815", + "modified": "2020-04-13 21:20:59.382394", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index bf919067ca..5ed4c87c62 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -15,6 +15,9 @@ class HRSettings(Document): self.set_naming_series() self.validate_password_policy() + if not self.daily_wages_fraction_for_half_day: + self.daily_wages_fraction_for_half_day = 0.5 + def set_naming_series(self): from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series set_by_naming_series("Employee", "employee_number", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.js b/erpnext/hr/doctype/salary_slip/salary_slip.js index f430eeed4e..1c4d4e34c5 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.js +++ b/erpnext/hr/doctype/salary_slip/salary_slip.js @@ -51,7 +51,7 @@ frappe.ui.form.on("Salary Slip", { }, end_date: function(frm) { - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, set_end_date: function(frm){ @@ -86,7 +86,7 @@ frappe.ui.form.on("Salary Slip", { salary_slip_based_on_timesheet: function(frm) { frm.trigger("toggle_fields"); - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, payroll_frequency: function(frm) { @@ -95,15 +95,14 @@ frappe.ui.form.on("Salary Slip", { }, employee: function(frm) { - frm.events.get_emp_and_leave_details(frm); + frm.events.get_emp_and_working_day_details(frm); }, leave_without_pay: function(frm){ if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) { return frappe.call({ - method: 'process_salary_based_on_leave', + method: 'process_salary_based_on_working_days', doc: frm.doc, - args: {"lwp": frm.doc.leave_without_pay}, callback: function(r, rt) { frm.refresh(); } @@ -115,12 +114,12 @@ frappe.ui.form.on("Salary Slip", { frm.toggle_display(['hourly_wages', 'timesheets'], cint(frm.doc.salary_slip_based_on_timesheet)===1); frm.toggle_display(['payment_days', 'total_working_days', 'leave_without_pay'], - frm.doc.payroll_frequency!=""); + frm.doc.payroll_frequency != ""); }, - get_emp_and_leave_details: function(frm) { + get_emp_and_working_day_details: function(frm) { return frappe.call({ - method: 'get_emp_and_leave_details', + method: 'get_emp_and_working_day_details', doc: frm.doc, callback: function(r, rt) { frm.refresh(); diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.json b/erpnext/hr/doctype/salary_slip/salary_slip.json index 097d3a096b..54a8164587 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.json +++ b/erpnext/hr/doctype/salary_slip/salary_slip.json @@ -11,20 +11,20 @@ "employee_name", "department", "designation", + "branch", "column_break1", - "company", + "status", "journal_entry", "payroll_entry", + "company", "letter_head", - "branch", - "status", "section_break_10", "salary_slip_based_on_timesheet", - "payroll_frequency", "start_date", "end_date", "column_break_15", "salary_structure", + "payroll_frequency", "total_working_days", "leave_without_pay", "payment_days", @@ -309,6 +309,7 @@ { "fieldname": "earning", "fieldtype": "Column Break", + "label": "Earning", "oldfieldtype": "Column Break", "width": "50%" }, @@ -323,6 +324,7 @@ { "fieldname": "deduction", "fieldtype": "Column Break", + "label": "Deduction", "oldfieldtype": "Column Break", "width": "50%" }, @@ -463,7 +465,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2020-04-09 20:02:53.159827", + "modified": "2020-04-14 20:02:53.159827", "modified_by": "Administrator", "module": "HR", "name": "Salary Slip", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 40fe572d75..916b64a83d 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, format_date from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -44,9 +44,9 @@ class SalarySlip(TransactionBase): if not (len(self.get("earnings")) or len(self.get("deductions"))): # get details from salary structure - self.get_emp_and_leave_details() + self.get_emp_and_working_day_details() else: - self.get_leave_details(lwp = self.leave_without_pay) + self.get_working_days_details(lwp = self.leave_without_pay) self.calculate_net_pay() @@ -117,7 +117,7 @@ class SalarySlip(TransactionBase): self.start_date = date_details.start_date self.end_date = date_details.end_date - def get_emp_and_leave_details(self): + def get_emp_and_working_day_details(self): '''First time, load all the components from salary structure''' if self.employee: self.set("earnings", []) @@ -129,7 +129,8 @@ class SalarySlip(TransactionBase): joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) - self.get_leave_details(joining_date, relieving_date) + #getin leave details + self.get_working_days_details(joining_date, relieving_date) struct = self.check_sal_struct(joining_date, relieving_date) if struct: @@ -188,10 +189,9 @@ class SalarySlip(TransactionBase): make_salary_slip(self._salary_structure_doc.name, self) - def get_leave_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): - if not joining_date: - joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0): + payroll_based_on = frappe.db.get_value("HR Settings", None, "payroll_based_on") + include_holidays_in_total_working_days = frappe.db.get_single_value("HR Settings", "include_holidays_in_total_working_days") working_days = date_diff(self.end_date, self.start_date) + 1 if for_preview: @@ -200,24 +200,42 @@ class SalarySlip(TransactionBase): return holidays = self.get_holidays_for_employee(self.start_date, self.end_date) - actual_lwp = self.calculate_lwp(holidays, working_days) - if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): + + if not cint(include_holidays_in_total_working_days): working_days -= len(holidays) if working_days < 0: frappe.throw(_("There are more holidays than working days this month.")) + if not payroll_based_on: + frappe.throw(_("Please set Payroll based on in HR settings")) + + if payroll_based_on == "Attendance": + actual_lwp = self.calculate_lwp_based_on_attendance(holidays) + else: + actual_lwp = self.calculate_lwp_based_on_leave_application(holidays, working_days) + if not lwp: lwp = actual_lwp elif lwp != actual_lwp: - frappe.msgprint(_("Leave Without Pay does not match with approved Leave Application records")) + frappe.msgprint(_("Leave Without Pay does not match with approved {} records") + .format(payroll_based_on)) - self.total_working_days = working_days self.leave_without_pay = lwp + self.total_working_days = working_days - payment_days = flt(self.get_payment_days(joining_date, relieving_date)) - flt(lwp) - self.payment_days = payment_days > 0 and payment_days or 0 + payment_days = self.get_payment_days(joining_date, + relieving_date, include_holidays_in_total_working_days) + + if flt(payment_days) > flt(lwp): + self.payment_days = flt(payment_days) - flt(lwp) + else: + self.payment_days = 0 + + def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days): + if not joining_date: + joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee, + ["date_of_joining", "relieving_date"]) - def get_payment_days(self, joining_date, relieving_date): start_date = getdate(self.start_date) if joining_date: if getdate(self.start_date) <= joining_date <= getdate(self.end_date): @@ -235,9 +253,10 @@ class SalarySlip(TransactionBase): payment_days = date_diff(end_date, start_date) + 1 - if not cint(frappe.db.get_value("HR Settings", None, "include_holidays_in_total_working_days")): + if not cint(include_holidays_in_total_working_days): holidays = self.get_holidays_for_employee(start_date, end_date) payment_days -= len(holidays) + return payment_days def get_holidays_for_employee(self, start_date, end_date): @@ -256,27 +275,67 @@ class SalarySlip(TransactionBase): return holidays - def calculate_lwp(self, holidays, working_days): + def calculate_lwp_based_on_leave_application(self, holidays, working_days): lwp = 0 holidays = "','".join(holidays) + daily_wages_fraction_for_half_day = \ + flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + for d in range(working_days): dt = add_days(cstr(getdate(self.start_date)), d) leave = frappe.db.sql(""" SELECT t1.name, - CASE WHEN t1.half_day_date = %(dt)s or t1.to_date = t1.from_date + CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date) THEN t1.half_day else 0 END FROM `tabLeave Application` t1, `tabLeave Type` t2 WHERE t2.name = t1.leave_type AND t2.is_lwp = 1 AND t1.docstatus = 1 AND t1.employee = %(employee)s - AND CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = '' - WHEN t2.include_holiday THEN %(dt)s between from_date and to_date and ifnull(t1.salary_slip, '') = '' - END + AND ifnull(t1.salary_slip, '') = '' + AND CASE + WHEN t2.include_holiday != 1 + THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date + WHEN t2.include_holiday + THEN %(dt)s between from_date and to_date + END """.format(holidays), {"employee": self.employee, "dt": dt}) if leave: - lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) + is_half_day_leave = cint(leave[0][1]) + lwp += (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1 + + return lwp + + def calculate_lwp_based_on_attendance(self, holidays): + lwp = 0 + + daily_wages_fraction_for_half_day = \ + flt(frappe.db.get_value("HR Settings", None, "daily_wages_fraction_for_half_day")) or 0.5 + + lwp_leave_types = dict(frappe.get_all("Leave Type", {"is_lwp": 1}, ["name", "include_holiday"], as_list=1)) + + attendances = frappe.db.sql(''' + SELECT attendance_date, status, leave_type + FROM `tabAttendance` + WHERE + status in ("Absent", "Half Day", "On leave") + AND employee = %s + AND docstatus = 1 + AND attendance_date between %s and %s + ''', values=(self.employee, self.start_date, self.end_date), as_dict=1) + + for d in attendances: + if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: + continue + + if format_date(d.attendance_date, "yyyy-mm-dd") in holidays: + if d.status == "Absent" or \ + (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): + continue + + lwp += (1 - daily_wages_fraction_for_half_day) if d.status == "Half Day" else 1 + return lwp def add_earning_for_hourly_wages(self, doc, salary_component, amount): @@ -945,7 +1004,7 @@ class SalarySlip(TransactionBase): if not self.salary_slip_based_on_timesheet: self.get_date_details() self.pull_emp_details() - self.get_leave_details(for_preview=for_preview) + self.get_working_days_details(for_preview=for_preview) self.calculate_net_pay() def pull_emp_details(self): @@ -954,8 +1013,8 @@ class SalarySlip(TransactionBase): self.bank_name = emp.bank_name self.bank_account_no = emp.bank_ac_no - def process_salary_based_on_leave(self, lwp=0): - self.get_leave_details(lwp=lwp) + def process_salary_based_on_working_days(self): + self.get_working_days_details(lwp=self.leave_without_pay) self.calculate_net_pay() def unlink_ref_doc_from_salary_slip(ref_no): diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index 73bb19e9ee..fc687a355c 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -21,18 +21,105 @@ class TestSalarySlip(unittest.TestCase): make_earning_salary_component(setup=True, company_list=["_Test Company"]) make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]: + for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance"]: frappe.db.sql("delete from `tab%s`" % dt) self.make_holiday_list() frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) - + frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) + frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) + def tearDown(self): frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") + def test_payment_days_based_on_attendance(self): + from erpnext.hr.doctype.attendance.attendance import mark_attendance + no_of_days = self.get_no_of_days() + + # Payroll based on attendance + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Attendance") + frappe.db.set_value("HR Settings", None, "daily_wages_fraction_for_half_day", 0.75) + + emp_id = make_employee("test_for_attendance@salary.com") + frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + mark_attendance(emp_id, first_sunday, 'Absent', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # valid lwp + mark_attendance(emp_id, add_days(first_sunday, 2), 'Half Day', leave_type='Leave Without Pay', ignore_validate=True) # valid 0.75 lwp + mark_attendance(emp_id, add_days(first_sunday, 3), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # valid lwp + mark_attendance(emp_id, add_days(first_sunday, 4), 'On Leave', leave_type='Casual Leave', ignore_validate=True) # invalid lwp + mark_attendance(emp_id, add_days(first_sunday, 7), 'On Leave', leave_type='Leave Without Pay', ignore_validate=True) # invalid lwp + + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + + self.assertEqual(ss.leave_without_pay, 2.25) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 2.25) + + #Gross pay calculation based on attendances + gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) + + self.assertEqual(ss.gross_pay, gross_pay) + + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + + def test_payment_days_based_on_leave_application(self): + no_of_days = self.get_no_of_days() + + # Payroll based on attendance + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + + emp_id = make_employee("test_for_attendance@salary.com") + frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"}) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + + month_start_date = get_first_day(nowdate()) + month_end_date = get_last_day(nowdate()) + + first_sunday = frappe.db.sql(""" + select holiday_date from `tabHoliday` + where parent = 'Salary Slip Test Holiday List' + and holiday_date between %s and %s + order by holiday_date + """, (month_start_date, month_end_date))[0][0] + + make_leave_application(emp_id, first_sunday, add_days(first_sunday, 3), "Leave Without Pay") + + ss = make_employee_salary_slip("test_for_attendance@salary.com", "Monthly") + + self.assertEqual(ss.leave_without_pay, 3) + + days_in_month = no_of_days[0] + no_of_holidays = no_of_days[1] + + self.assertEqual(ss.payment_days, days_in_month - no_of_holidays - 3) + + #Gross pay calculation based on attendances + gross_pay = 78000 - ((78000 / (days_in_month - no_of_holidays)) * flt(ss.leave_without_pay)) + + self.assertEqual(ss.gross_pay, gross_pay) + + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") + def test_salary_slip_with_holidays_included(self): no_of_days = self.get_no_of_days() frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1) @@ -315,7 +402,6 @@ class TestSalarySlip(unittest.TestCase): return [no_of_days_in_month[1], no_of_holidays_in_month] - def make_employee_salary_slip(user, payroll_frequency, salary_structure=None): from erpnext.hr.doctype.salary_structure.test_salary_structure import make_salary_structure if not salary_structure: @@ -603,3 +689,17 @@ def create_additional_salary(employee, payroll_period, amount): "type": "Earning" }).submit() return salary_date + +def make_leave_application(employee, from_date, to_date, leave_type, company=None): + leave_application = frappe.get_doc(dict( + doctype = 'Leave Application', + employee = employee, + leave_type = leave_type, + from_date = from_date, + to_date = to_date, + company = company or erpnext.get_default_company() or "_Test Company", + docstatus = 1, + status = "Approved", + leave_approver = 'test@example.com' + )) + leave_application.submit() \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 801d583ab1..0ea83fd7bc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -669,6 +669,7 @@ erpnext.patches.v12_0.update_healthcare_refactored_changes erpnext.patches.v12_0.set_total_batch_quantity erpnext.patches.v12_0.rename_mws_settings_fields erpnext.patches.v12_0.set_updated_purpose_in_pick_list +erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 diff --git a/erpnext/patches/v12_0/set_default_payroll_based_on.py b/erpnext/patches/v12_0/set_default_payroll_based_on.py new file mode 100644 index 0000000000..04b54a6cf6 --- /dev/null +++ b/erpnext/patches/v12_0/set_default_payroll_based_on.py @@ -0,0 +1,6 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("hr", "doctype", "hr_settings") + frappe.db.set_value("HR Settings", None, "payroll_based_on", "Leave") \ No newline at end of file From ded418e31d859c4aa940b8d3dec5c1ded9f3a4b6 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Sun, 26 Apr 2020 21:04:58 +0530 Subject: [PATCH 45/73] feat: employee leave balance reports (#20754) * feat: Employee leave balance summary report new design * feat: Employee leave balance report new design * fix: leave based on multiple holiday list Co-authored-by: Nabin Hait --- .../leave_application/leave_application.py | 20 +- .../leave_ledger_entry.json | 11 +- .../employee_leave_balance.py | 213 +++++++++++++----- .../employee_leave_balance_summary.js | 13 +- .../employee_leave_balance_summary.py | 159 +++++-------- 5 files changed, 234 insertions(+), 182 deletions(-) diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index c441751525..47b1bb7684 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -374,7 +374,8 @@ class LeaveApplication(Document): leaves=self.total_leave_days * -1, from_date=self.from_date, to_date=self.to_date, - is_lwp=lwp + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee) ) create_leave_ledger_entry(self, args, submit) @@ -384,7 +385,9 @@ class LeaveApplication(Document): from_date=self.from_date, to_date=expiry_date, leaves=(date_diff(expiry_date, self.from_date) + 1) * -1, - is_lwp=lwp + is_lwp=lwp, + holiday_list=get_holiday_list_for_employee(self.employee), + ) create_leave_ledger_entry(self, args, submit) @@ -410,7 +413,7 @@ def get_allocation_expiry(employee, leave_type, to_date, from_date): return expiry[0]['to_date'] if expiry else None @frappe.whitelist() -def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None): +def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day = None, half_day_date = None, holiday_list = None): number_of_days = 0 if cint(half_day) == 1: if from_date == to_date: @@ -424,7 +427,7 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day number_of_days = date_diff(to_date, from_date) + 1 if not frappe.db.get_value("Leave Type", leave_type, "include_holiday"): - number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date)) + number_of_days = flt(number_of_days) - flt(get_holidays(employee, from_date, to_date, holiday_list=holiday_list)) return number_of_days @frappe.whitelist() @@ -575,7 +578,7 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date): {'name': leave_entry.transaction_name}, ['half_day_date']) leave_days += get_number_of_leave_days(employee, leave_type, - leave_entry.from_date, leave_entry.to_date, half_day, half_day_date) * -1 + leave_entry.from_date, leave_entry.to_date, half_day, half_day_date, holiday_list=leave_entry.holiday_list) * -1 return leave_days @@ -589,7 +592,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date): ''' Returns leave entries between from_date and to_date. ''' return frappe.db.sql(""" SELECT - employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, + employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, holiday_list, is_carry_forward, is_expired FROM `tabLeave Ledger Entry` WHERE employee=%(employee)s AND leave_type=%(leave_type)s @@ -607,9 +610,10 @@ def get_leave_entries(employee, leave_type, from_date, to_date): }, as_dict=1) @frappe.whitelist() -def get_holidays(employee, from_date, to_date): +def get_holidays(employee, from_date, to_date, holiday_list = None): '''get holidays between two dates for the given employee''' - holiday_list = get_holiday_list_for_employee(employee) + if not holiday_list: + holiday_list = get_holiday_list_for_employee(employee) holidays = frappe.db.sql("""select count(distinct holiday_date) from `tabHoliday` h1, `tabHoliday List` h2 where h1.parent = h2.name and h1.holiday_date between %s and %s diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json index 771e706bbb..a5ac3f3d47 100644 --- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json +++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "creation": "2019-05-09 15:47:39.760406", "doctype": "DocType", "engine": "InnoDB", @@ -12,6 +13,7 @@ "column_break_7", "from_date", "to_date", + "holiday_list", "is_carry_forward", "is_expired", "is_lwp", @@ -98,11 +100,18 @@ "fieldname": "is_lwp", "fieldtype": "Check", "label": "Is Leave Without Pay" + }, + { + "fieldname": "holiday_list", + "fieldtype": "Link", + "label": "Holiday List", + "options": "Holiday List" } ], "in_create": 1, "is_submittable": 1, - "modified": "2019-08-20 14:40:04.130799", + "links": [], + "modified": "2020-02-27 14:40:10.502605", "modified_by": "Administrator", "module": "HR", "name": "Leave Ledger Entry", diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py index 35c8630e8e..97be5cd813 100644 --- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py +++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py @@ -1,85 +1,186 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt from __future__ import unicode_literals import frappe -from frappe import _ from frappe.utils import flt -from erpnext.hr.doctype.leave_application.leave_application \ - import get_leave_balance_on, get_leaves_for_period - -from erpnext.hr.report.employee_leave_balance_summary.employee_leave_balance_summary \ - import get_department_leave_approver_map +from frappe import _ +from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on, get_leave_allocation_records def execute(filters=None): - leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") + if filters.to_date <= filters.from_date: + frappe.throw(_('From date can not be greater than than To date')) - columns = get_columns(leave_types) - data = get_data(filters, leave_types) + columns = get_columns() + data = get_data(filters) return columns, data -def get_columns(leave_types): - columns = [ - _("Employee") + ":Link.Employee:150", - _("Employee Name") + "::200", - _("Department") +"::150" - ] - - for leave_type in leave_types: - columns.append(_(leave_type) + " " + _("Opening") + ":Float:160") - columns.append(_(leave_type) + " " + _("Taken") + ":Float:160") - columns.append(_(leave_type) + " " + _("Balance") + ":Float:160") +def get_columns(): + columns = [{ + 'label': _('Leave Type'), + 'fieldtype': 'Link', + 'fieldname': 'leave_type', + 'width': 200, + 'options': 'Leave Type' + }, { + 'label': _('Employee'), + 'fieldtype': 'Link', + 'fieldname': 'employee', + 'width': 100, + 'options': 'Employee' + }, { + 'label': _('Employee Name'), + 'fieldtype': 'Data', + 'fieldname': 'employee_name', + 'width': 100, + }, { + 'label': _('Opening Balance'), + 'fieldtype': 'float', + 'fieldname': 'opening_balance', + 'width': 130, + }, { + 'label': _('Leaves Allocated'), + 'fieldtype': 'float', + 'fieldname': 'leaves_allocated', + 'width': 130, + }, { + 'label': _('Leaves Taken'), + 'fieldtype': 'float', + 'fieldname': 'leaves_taken', + 'width': 130, + }, { + 'label': _('Leaves Expired'), + 'fieldtype': 'float', + 'fieldname': 'leaves_expired', + 'width': 130, + }, { + 'label': _('Closing Balance'), + 'fieldtype': 'float', + 'fieldname': 'closing_balance', + 'width': 130, + }] return columns -def get_conditions(filters): - conditions = { - "status": "Active", - "company": filters.company, - } - if filters.get("department"): - conditions.update({"department": filters.get("department")}) - if filters.get("employee"): - conditions.update({"employee": filters.get("employee")}) +def get_data(filters): + leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC") - return conditions - -def get_data(filters, leave_types): - user = frappe.session.user conditions = get_conditions(filters) - if filters.to_date <= filters.from_date: - frappe.throw(_("From date can not be greater than than To date")) - - active_employees = frappe.get_all("Employee", - filters=conditions, - fields=["name", "employee_name", "department", "user_id", "leave_approver"]) - + user = frappe.session.user department_approver_map = get_department_leave_approver_map(filters.get('department')) + active_employees = frappe.get_list('Employee', + filters=conditions, + fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver']) + data = [] - for employee in active_employees: - leave_approvers = department_approver_map.get(employee.department_name, []) - if employee.leave_approver: - leave_approvers.append(employee.leave_approver) - if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)): - row = [employee.name, employee.employee_name, employee.department] + for leave_type in leave_types: + if len(active_employees) > 1: + data.append({ + 'leave_type': leave_type + }) + else: + row = frappe._dict({ + 'leave_type': leave_type + }) + + for employee in active_employees: + + leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) + + if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ + or ("HR Manager" in frappe.get_roles(user)): + if len(active_employees) > 1: + row = frappe._dict() + row.employee = employee.name, + row.employee_name = employee.employee_name - for leave_type in leave_types: - # leaves taken leaves_taken = get_leaves_for_period(employee.name, leave_type, filters.from_date, filters.to_date) * -1 - # opening balance + new_allocation, expired_leaves = get_allocated_and_expired_leaves(filters.from_date, filters.to_date, employee.name, leave_type) + + opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) + closing = get_leave_balance_on(employee.name, leave_type, filters.to_date) - # closing balance - closing = max(opening - leaves_taken, 0) + row.leaves_allocated = new_allocation + row.leaves_expired = expired_leaves - leaves_taken if expired_leaves - leaves_taken > 0 else 0 + row.opening_balance = opening + row.leaves_taken = leaves_taken + row.closing_balance = closing + row.indent = 1 + data.append(row) + new_leaves_allocated = 0 - row += [opening, leaves_taken, closing] - data.append(row) + return data - return data \ No newline at end of file +def get_conditions(filters): + conditions={ + 'status': 'Active', + } + if filters.get('employee'): + conditions['name'] = filters.get('employee') + + if filters.get('employee'): + conditions['name'] = filters.get('employee') + + return conditions + +def get_department_leave_approver_map(department=None): + conditions='' + if department: + conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department} + + # get current department and all its child + department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec + + # retrieve approvers list from current department and from its subsequent child departments + approver_list = frappe.get_all('Department Approver', filters={ + 'parentfield': 'leave_approvers', + 'parent': ('in', department_list) + }, fields=['parent', 'approver'], as_list=1) + + approvers = {} + + for k, v in approver_list: + approvers.setdefault(k, []).append(v) + + return approvers + +def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): + + from frappe.utils import getdate + + new_allocation = 0 + expired_leaves = 0 + + records= frappe.db.sql(""" + SELECT + employee, leave_type, from_date, to_date, leaves, transaction_name, + is_carry_forward, is_expired + FROM `tabLeave Ledger Entry` + WHERE employee=%(employee)s AND leave_type=%(leave_type)s + AND docstatus=1 AND leaves>0 + AND (from_date between %(from_date)s AND %(to_date)s + OR to_date between %(from_date)s AND %(to_date)s + OR (from_date < %(from_date)s AND to_date > %(to_date)s)) + """, { + "from_date": from_date, + "to_date": to_date, + "employee": employee, + "leave_type": leave_type + }, as_dict=1) + + for record in records: + if record.to_date <= getdate(to_date): + expired_leaves += record.leaves + + if record.from_date >= getdate(from_date): + new_allocation += record.leaves + + return new_allocation, expired_leaves diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js index 3fb8f6e9c1..cb05d1138f 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.js @@ -5,18 +5,11 @@ frappe.query_reports['Employee Leave Balance Summary'] = { filters: [ { - fieldname:'from_date', - label: __('From Date'), + fieldname:'date', + label: __('Date'), fieldtype: 'Date', reqd: 1, - default: frappe.defaults.get_default('year_start_date') - }, - { - fieldname:'to_date', - label: __('To Date'), - fieldtype: 'Date', - reqd: 1, - default: frappe.defaults.get_default('year_end_date') + default: frappe.datetime.now_date() }, { fieldname:'company', diff --git a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py index 777de02238..a5cdecf36a 100644 --- a/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py +++ b/erpnext/hr/report/employee_leave_balance_summary/employee_leave_balance_summary.py @@ -1,130 +1,75 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt +# 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 -from frappe.utils import flt from frappe import _ -from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on +from frappe.utils import flt +from erpnext.hr.doctype.leave_application.leave_application \ + import get_leave_balance_on, get_leaves_for_period + +from erpnext.hr.report.employee_leave_balance.employee_leave_balance \ + import get_department_leave_approver_map def execute(filters=None): - if filters.to_date <= filters.from_date: - frappe.throw(_('From date can not be greater than than To date')) + leave_types = frappe.db.sql_list("select name from `tabLeave Type` order by name asc") - columns = get_columns() - data = get_data(filters) + columns = get_columns(leave_types) + data = get_data(filters, leave_types) return columns, data -def get_columns(): - columns = [{ - 'label': _('Leave Type'), - 'fieldtype': 'Link', - 'fieldname': 'leave_type', - 'width': 300, - 'options': 'Leave Type' - }, { - 'label': _('Employee'), - 'fieldtype': 'Link', - 'fieldname': 'employee', - 'width': 100, - 'options': 'Employee' - }, { - 'label': _('Employee Name'), - 'fieldtype': 'Data', - 'fieldname': 'employee_name', - 'width': 100, - }, { - 'label': _('Opening Balance'), - 'fieldtype': 'float', - 'fieldname': 'opening_balance', - 'width': 160, - }, { - 'label': _('Leaves Taken'), - 'fieldtype': 'float', - 'fieldname': 'leaves_taken', - 'width': 160, - }, { - 'label': _('Closing Balance'), - 'fieldtype': 'float', - 'fieldname': 'closing_balance', - 'width': 160, - }] +def get_columns(leave_types): + columns = [ + _("Employee") + ":Link.Employee:150", + _("Employee Name") + "::200", + _("Department") +"::150" + ] + + for leave_type in leave_types: + columns.append(_(leave_type) + ":Float:160") return columns -def get_data(filters): - leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC") - - conditions = get_conditions(filters) - - user = frappe.session.user - department_approver_map = get_department_leave_approver_map(filters.get('department')) - - active_employees = frappe.get_list('Employee', - filters=conditions, - fields=['name', 'employee_name', 'department', 'user_id', 'leave_approver']) - - data = [] - - for leave_type in leave_types: - data.append({ - 'leave_type': leave_type - }) - for employee in active_employees: - - leave_approvers = department_approver_map.get(employee.department_name, []).append(employee.leave_approver) - - if (leave_approvers and len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) \ - or ("HR Manager" in frappe.get_roles(user)): - row = frappe._dict({ - 'employee': employee.name, - 'employee_name': employee.employee_name - }) - - leaves_taken = get_leaves_for_period(employee.name, leave_type, - filters.from_date, filters.to_date) * -1 - - opening = get_leave_balance_on(employee.name, leave_type, filters.from_date) - closing = get_leave_balance_on(employee.name, leave_type, filters.to_date) - - row.opening_balance = opening - row.leaves_taken = leaves_taken - row.closing_balance = closing - row.indent = 1 - data.append(row) - - return data - def get_conditions(filters): - conditions={ - 'status': 'Active', + conditions = { + "status": "Active", + "company": filters.company, } - if filters.get('employee'): - conditions['name'] = filters.get('employee') - - if filters.get('employee'): - conditions['name'] = filters.get('employee') + if filters.get("department"): + conditions.update({"department": filters.get("department")}) + if filters.get("employee"): + conditions.update({"employee": filters.get("employee")}) return conditions -def get_department_leave_approver_map(department=None): - conditions='' - if department: - conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department} +def get_data(filters, leave_types): + user = frappe.session.user + conditions = get_conditions(filters) - # get current department and all its child - department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec + active_employees = frappe.get_all("Employee", + filters=conditions, + fields=["name", "employee_name", "department", "user_id", "leave_approver"]) - # retrieve approvers list from current department and from its subsequent child departments - approver_list = frappe.get_all('Department Approver', filters={ - 'parentfield': 'leave_approvers', - 'parent': ('in', department_list) - }, fields=['parent', 'approver'], as_list=1) + department_approver_map = get_department_leave_approver_map(filters.get('department')) - approvers = {} + data = [] + for employee in active_employees: + leave_approvers = department_approver_map.get(employee.department_name, []) + if employee.leave_approver: + leave_approvers.append(employee.leave_approver) - for k, v in approver_list: - approvers.setdefault(k, []).append(v) + if (len(leave_approvers) and user in leave_approvers) or (user in ["Administrator", employee.user_id]) or ("HR Manager" in frappe.get_roles(user)): + row = [employee.name, employee.employee_name, employee.department] - return approvers + for leave_type in leave_types: + + # opening balance + opening = get_leave_balance_on(employee.name, leave_type, filters.date) + + + row += [opening] + + data.append(row) + + return data \ No newline at end of file From 3e3a793567813047aece67753019f09cff56c349 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Sun, 26 Apr 2020 23:25:40 +0530 Subject: [PATCH 46/73] fix: Set barcode field empty only if it has value (#21425) - To avoid unnecessary form dirty trigger --- erpnext/public/js/controllers/transaction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 42964474b0..5843034543 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -288,7 +288,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ this.setup_sms(); this.setup_quality_inspection(); let scan_barcode_field = this.frm.get_field('scan_barcode'); - if (scan_barcode_field) { + if (scan_barcode_field && scan_barcode_field.get_value()) { scan_barcode_field.set_value(""); scan_barcode_field.set_new_description(""); From c9a1aa8eaa91880c9e6f77354eeb3c9761c16b2d Mon Sep 17 00:00:00 2001 From: Himanshu Date: Sun, 26 Apr 2020 23:28:33 +0530 Subject: [PATCH 47/73] fix: add dashboard to quality inspection template (#21423) --- .../quality_inspection_template.json | 250 ++++++------------ 1 file changed, 80 insertions(+), 170 deletions(-) diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json index eab08e2216..9646f2d8e8 100644 --- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json +++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.json @@ -1,186 +1,96 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:quality_inspection_template_name", - "beta": 0, - "creation": "2018-01-24 16:23:41.691127", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_import": 1, + "allow_rename": 1, + "autoname": "field:quality_inspection_template_name", + "creation": "2018-01-24 16:23:41.691127", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "quality_inspection_template_name", + "item_quality_inspection_parameter" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "quality_inspection_template_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": "Quality Inspection Template 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, - "translatable": 0, - "unique": 0 - }, + "fieldname": "quality_inspection_template_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Quality Inspection Template Name", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "item_quality_inspection_parameter", - "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": "Item Quality Inspection Parameter", - "length": 0, - "no_copy": 0, - "options": "Item Quality Inspection Parameter", - "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_quality_inspection_parameter", + "fieldtype": "Table", + "label": "Item Quality Inspection Parameter", + "options": "Item Quality Inspection Parameter", + "reqd": 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-02-21 12:05:29.304432", - "modified_by": "Administrator", - "module": "Stock", - "name": "Quality Inspection Template", - "name_case": "", - "owner": "Administrator", + ], + "links": [ + { + "group": "Quality Inspection", + "link_doctype": "Quality Inspection", + "link_fieldname": "quality_inspection_template" + } + ], + "modified": "2020-04-26 20:13:02.810132", + "modified_by": "Administrator", + "module": "Stock", + "name": "Quality Inspection Template", + "owner": "Administrator", "permissions": [ { - "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": "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, - "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": "Stock User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock 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": 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 - }, + }, { - "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": "Manufacturing User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Manufacturing User", + "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 From 2305d00e0ba78195d7f8d5f29a6e96c6ce6d97dd Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Sun, 26 Apr 2020 23:29:13 +0530 Subject: [PATCH 48/73] fix: Procurement tracker report (#21421) * fix: procurement report data was not coming * fix: leave allocation minor issue --- .../procurement_tracker.py | 74 +++++++++---------- .../leave_allocation/leave_allocation.py | 6 +- 2 files changed, 39 insertions(+), 41 deletions(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index 866bf0c733..39668795cb 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -141,19 +141,18 @@ def get_conditions(filters): conditions = "" if filters.get("company"): - conditions += " AND company=%s"% frappe.db.escape(filters.get('company')) + conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company')) if filters.get("cost_center") or filters.get("project"): conditions += """ - AND (cost_center=%s - OR project=%s) - """% (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) + AND (child.`cost_center`=%s OR child.`project`=%s) + """ % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project'))) if filters.get("from_date"): - conditions += " AND transaction_date>=%s"% filters.get('from_date') + conditions += " AND par.transaction_date>='%s'" % filters.get('from_date') if filters.get("to_date"): - conditions += " AND transaction_date<=%s"% filters.get('to_date') + conditions += " AND par.transaction_date<='%s'" % filters.get('to_date') return conditions def get_data(filters): @@ -162,7 +161,6 @@ def get_data(filters): mr_records, procurement_record_against_mr = get_mapped_mr_details(conditions) pr_records = get_mapped_pr_records() pi_records = get_mapped_pi_records() - print(pi_records) procurement_record=[] if procurement_record_against_mr: @@ -198,16 +196,16 @@ def get_mapped_mr_details(conditions): mr_records = {} mr_details = frappe.db.sql(""" SELECT - mr.transaction_date, - mr.per_ordered, - mr_item.name, - mr_item.parent, - mr_item.amount - FROM `tabMaterial Request` mr, `tabMaterial Request Item` mr_item + par.transaction_date, + par.per_ordered, + child.name, + child.parent, + child.amount + FROM `tabMaterial Request` par, `tabMaterial Request Item` child WHERE - mr.per_ordered>=0 - AND mr.name=mr_item.parent - AND mr.docstatus=1 + par.per_ordered>=0 + AND par.name=child.parent + AND par.docstatus=1 {conditions} """.format(conditions=conditions), as_dict=1) #nosec @@ -254,29 +252,29 @@ def get_mapped_pr_records(): def get_po_entries(conditions): return frappe.db.sql(""" SELECT - po_item.name, - po_item.parent, - po_item.cost_center, - po_item.project, - po_item.warehouse, - po_item.material_request, - po_item.material_request_item, - po_item.description, - po_item.stock_uom, - po_item.qty, - po_item.amount, - po_item.base_amount, - po_item.schedule_date, - po.transaction_date, - po.supplier, - po.status, - po.owner - FROM `tabPurchase Order` po, `tabPurchase Order Item` po_item + child.name, + child.parent, + child.cost_center, + child.project, + child.warehouse, + child.material_request, + child.material_request_item, + child.description, + child.stock_uom, + child.qty, + child.amount, + child.base_amount, + child.schedule_date, + par.transaction_date, + par.supplier, + par.status, + par.owner + FROM `tabPurchase Order` par, `tabPurchase Order Item` child WHERE - po.docstatus = 1 - AND po.name = po_item.parent - AND po.status not in ("Closed","Completed","Cancelled") + par.docstatus = 1 + AND par.name = child.parent + AND par.status not in ("Closed","Completed","Cancelled") {conditions} GROUP BY - po.name,po_item.item_code + par.name, child.item_code """.format(conditions=conditions), as_dict=1) #nosec \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index d13bb4577c..03fe3fa035 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -30,16 +30,16 @@ class LeaveAllocation(Document): def validate_leave_allocation_days(self): company = frappe.db.get_value("Employee", self.employee, "company") leave_period = get_leave_period(self.from_date, self.to_date, company) - max_leaves_allowed = frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed") + max_leaves_allowed = flt(frappe.db.get_value("Leave Type", self.leave_type, "max_leaves_allowed")) if max_leaves_allowed > 0: leave_allocated = 0 if leave_period: leave_allocated = get_leave_allocation_for_period(self.employee, self.leave_type, leave_period[0].from_date, leave_period[0].to_date) - leave_allocated += self.new_leaves_allocated + leave_allocated += flt(self.new_leaves_allocated) if leave_allocated > max_leaves_allowed: frappe.throw(_("Total allocated leaves are more days than maximum allocation of {0} leave type for employee {1} in the period") - .format(self.leave_type, self.employee)) + .format(self.leave_type, self.employee)) def on_submit(self): self.create_leave_ledger_entry() From e34ec70d441d13aede056669990d7906cbcd542a Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Sun, 26 Apr 2020 23:40:37 +0530 Subject: [PATCH 49/73] fix(Desk Page): Number of Open Leads not visible on Shortcut Card --- erpnext/crm/desk_page/crm/crm.json | 4 ++-- erpnext/selling/desk_page/retail/retail.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index 4a599fe478..747c8e3a41 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -33,7 +33,7 @@ "idx": 0, "is_standard": 1, "label": "CRM", - "modified": "2020-04-01 11:28:51.219999", + "modified": "2020-04-26 22:31:15.865799", "modified_by": "Administrator", "module": "CRM", "name": "CRM", @@ -42,7 +42,7 @@ "pin_to_top": 0, "shortcuts": [ { - "format": "Open", + "format": "{} Open", "label": "Lead", "link_to": "Lead", "stats_filter": "{\"status\":\"Open\"}", diff --git a/erpnext/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json index 9f3912db4c..7b30af20cc 100644 --- a/erpnext/selling/desk_page/retail/retail.json +++ b/erpnext/selling/desk_page/retail/retail.json @@ -17,7 +17,7 @@ "idx": 0, "is_standard": 1, "label": "Retail", - "modified": "2020-04-01 11:28:50.966145", + "modified": "2020-04-26 22:42:39.346750", "modified_by": "Administrator", "module": "Selling", "name": "Retail", From 9979ceb96b2a074f9d4c496197c2a9d860aa9231 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:03 +0530 Subject: [PATCH 50/73] fix: E-way bill fix in sales invoice --- .../doctype/sales_invoice/regional/india.js | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index ba6c03b95f..6336db16eb 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -26,16 +26,24 @@ frappe.ui.form.on("Sales Invoice", { && !frm.doc.is_return && !frm.doc.ewaybill) { frm.add_custom_button('E-Way Bill JSON', () => { - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(frm.doc.doctype) - + "&dn=" + encodeURIComponent(frm.doc.name) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': frm.doc.doctype, + 'dn': [frm.doc.name] + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: frm.doc.name + }; + open_url_post(frappe.request.url, args); + } + } + }); + }, __("Create")); } } From 10aff8e11818c43ef2a74ad3ed78afd13ca1ebf7 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:17 +0530 Subject: [PATCH 51/73] fix: E-way bill fix in List view --- .../sales_invoice/regional/india_list.js | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js index d17582769c..3e1c5228ea 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india_list.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india_list.js @@ -16,17 +16,23 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) { } } - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(doclist.doctype) - + "&dn=" + encodeURIComponent(docnames) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } - + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': doclist.doctype, + 'dn': docnames + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: docnames + }; + open_url_post(frappe.request.url, args); + } + } + }); }; doclist.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); From 00ea59b447c52ef879c6709233d1b7bcaac5a772 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:40 +0530 Subject: [PATCH 52/73] fix: Utils messsage cleanup --- erpnext/regional/india/utils.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 02823821c4..badb4b4dab 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -372,7 +372,6 @@ def calculate_hra_exemption_for_period(doc): return exemptions def get_ewb_data(dt, dn): - dn = dn.split(',') ewaybills = [] for doc_name in dn: @@ -453,18 +452,24 @@ def get_ewb_data(dt, dn): @frappe.whitelist() def generate_ewb_json(dt, dn): + dn = json.loads(dn) + return get_ewb_data(dt, dn) - data = get_ewb_data(dt, dn) +@frappe.whitelist() +def download_ewb_json(): + data = frappe._dict(frappe.local.form_dict) - frappe.local.response.filecontent = json.dumps(data, indent=4, sort_keys=True) + frappe.local.response.filecontent = json.dumps(data['data'], indent=4, sort_keys=True) frappe.local.response.type = 'download' - if len(data['billLists']) > 1: + billList = json.loads(data['data'])['billLists'] + + if len(billList) > 1: doc_name = 'Bulk' else: - doc_name = dn + doc_name = data['docname'] - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, frappe.utils.random_string(5)) + frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(data['docname'], frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): From 4eecc65cff6742e2e1a8465cf12b81e5f36096c3 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 10:50:59 +0530 Subject: [PATCH 53/73] fix: E-way bill fix in Delivery Note --- .../doctype/delivery_note/regional/india.js | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/delivery_note/regional/india.js b/erpnext/stock/doctype/delivery_note/regional/india.js index 0c1ca5caaa..5e1ff98000 100644 --- a/erpnext/stock/doctype/delivery_note/regional/india.js +++ b/erpnext/stock/doctype/delivery_note/regional/india.js @@ -3,21 +3,28 @@ erpnext.setup_auto_gst_taxation('Delivery Note'); frappe.ui.form.on('Delivery Note', { - refresh: function(frm) { - if(frm.doc.docstatus == 1 && !frm.is_dirty() && !frm.doc.ewaybill) { + refresh: function(frm) { + if(frm.doc.docstatus == 1 && !frm.is_dirty() && !frm.doc.ewaybill) { frm.add_custom_button('E-Way Bill JSON', () => { - var w = window.open( - frappe.urllib.get_full_url( - "/api/method/erpnext.regional.india.utils.generate_ewb_json?" - + "dt=" + encodeURIComponent(frm.doc.doctype) - + "&dn=" + encodeURIComponent(frm.doc.name) - ) - ); - if (!w) { - frappe.msgprint(__("Please enable pop-ups")); return; - } + frappe.call({ + method: 'erpnext.regional.india.utils.generate_ewb_json', + args: { + 'dt': frm.doc.doctype, + 'dn': [frm.doc.name] + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.india.utils.download_ewb_json', + data: r.message, + docname: frm.doc.name + }; + open_url_post(frappe.request.url, args); + } + } + }); }, __("Create")); } - } + } }) From 7bbe3dd8a0789ee2a8e420a81cb12ecd75403f5f Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Apr 2020 10:51:46 +0530 Subject: [PATCH 54/73] fix: Patch for updating Appointment Reminder method in Scheduled Job Type (#21431) --- erpnext/patches.txt | 1 + .../v12_0/update_appointment_reminder_scheduler_entry.py | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index eaeebcf0e9..a216f53a8b 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -674,3 +674,4 @@ erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status +erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py new file mode 100644 index 0000000000..91931eeb3b --- /dev/null +++ b/erpnext/patches/v12_0/update_appointment_reminder_scheduler_entry.py @@ -0,0 +1,7 @@ +import frappe + +def execute(): + job = frappe.db.exists('Scheduled Job Type', 'patient_appointment.send_appointment_reminder') + if job: + method = 'erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder' + frappe.db.set_value('Scheduled Job Type', job, 'method', method) \ No newline at end of file From 131452ca9425795fadd78023c0bd3dfb1f8a5d45 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Apr 2020 10:52:38 +0530 Subject: [PATCH 55/73] fix: Lab Test Invoicing (#21435) --- erpnext/healthcare/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py index 9a32c737cf..a756532358 100644 --- a/erpnext/healthcare/utils.py +++ b/erpnext/healthcare/utils.py @@ -43,7 +43,7 @@ def validate_customer_created(patient): def get_fee_validity(patient_appointments): if not frappe.db.get_single_value('Healthcare Settings', 'enable_free_follow_ups'): - return + return [] items_to_invoice = [] for appointment in patient_appointments: @@ -110,7 +110,7 @@ def get_lab_tests_to_invoice(patient): filters={'patient': patient.name, 'invoiced': False, 'docstatus': 1} ) for lab_test in lab_tests: - item, is_billable = frappe.get_cached_value('Lab Test Template', lab_test.lab_test_code, ['item', 'is_billable']) + item, is_billable = frappe.get_cached_value('Lab Test Template', lab_test.template, ['item', 'is_billable']) if is_billable: lab_tests_to_invoice.append({ 'reference_type': 'Lab Test', From f34faa91818334581df7c9c12cc104c0a9032675 Mon Sep 17 00:00:00 2001 From: Ahmad Date: Mon, 27 Apr 2020 10:27:02 +0500 Subject: [PATCH 56/73] Module Import Fix (#21433) --- erpnext/hr/doctype/attendance/attendance.py | 8 ++++---- erpnext/hr/doctype/salary_slip/salary_slip.py | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 7355a56128..b6c80655c2 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -7,7 +7,7 @@ import frappe from frappe.utils import getdate, nowdate from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, get_datetime, format_date +from frappe.utils import cstr, get_datetime, formatdate class Attendance(Document): def validate(self): @@ -52,16 +52,16 @@ class Attendance(Document): if d.half_day_date == getdate(self.attendance_date): self.status = 'Half Day' frappe.msgprint(_("Employee {0} on Half day on {1}") - .format(self.employee, format_date(self.attendance_date))) + .format(self.employee, formatdate(self.attendance_date))) else: self.status = 'On Leave' frappe.msgprint(_("Employee {0} is on Leave on {1}") - .format(self.employee, format_date(self.attendance_date))) + .format(self.employee, formatdate(self.attendance_date))) if self.status in ("On Leave", "Half Day"): if not leave_record: frappe.msgprint(_("No leave record found for employee {0} on {1}") - .format(self.employee, format_date(self.attendance_date)), alert=1) + .format(self.employee, formatdate(self.attendance_date)), alert=1) elif self.leave_type: self.leave_type = None self.leave_application = None diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index 916b64a83d..8a4da7e7d3 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -5,7 +5,7 @@ from __future__ import unicode_literals import frappe, erpnext import datetime, math -from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, format_date +from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate from frappe.model.naming import make_autoname from frappe import msgprint, _ @@ -329,7 +329,7 @@ class SalarySlip(TransactionBase): if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in lwp_leave_types: continue - if format_date(d.attendance_date, "yyyy-mm-dd") in holidays: + if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays: if d.status == "Absent" or \ (d.leave_type and d.leave_type in lwp_leave_types and not lwp_leave_types[d.leave_type]): continue From f40a431d8c4d47e5002db91f5fe84754db72f607 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 27 Apr 2020 11:29:56 +0530 Subject: [PATCH 57/73] fix: delivery trip form --- erpnext/stock/doctype/delivery_trip/delivery_trip.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.js b/erpnext/stock/doctype/delivery_trip/delivery_trip.js index a025f06711..a6fbb66aa2 100755 --- a/erpnext/stock/doctype/delivery_trip/delivery_trip.js +++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.js @@ -95,8 +95,6 @@ frappe.ui.form.on('Delivery Trip', { }; }, - }, - optimize_route: function (frm) { if (!frm.doc.driver_address) { frappe.throw(__("Cannot Optimize Route as Driver Address is Missing.")); From fe7e6f5f530823550e783018352dc5cb6e44ceab Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 27 Apr 2020 14:05:45 +0530 Subject: [PATCH 58/73] fix: Test --- erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py | 2 +- erpnext/regional/india/utils.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index a2819af508..88b54fec8f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -1892,7 +1892,7 @@ class TestSalesInvoice(unittest.TestCase): si.submit() - data = get_ewb_data("Sales Invoice", si.name) + data = get_ewb_data("Sales Invoice", [si.name]) self.assertEqual(data['version'], '1.0.1118') self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index badb4b4dab..094f01017b 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -469,7 +469,7 @@ def download_ewb_json(): else: doc_name = data['docname'] - frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(data['docname'], frappe.utils.random_string(5)) + frappe.local.response.filename = '{0}_e-WayBill_Data_{1}.json'.format(doc_name, frappe.utils.random_string(5)) @frappe.whitelist() def get_gstins_for_company(company): From cc9dbb912e1c223494c6064e3f19c4bd8486edd5 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Mon, 27 Apr 2020 22:37:02 +0530 Subject: [PATCH 59/73] Adding campaign card in CRM Desk --- erpnext/crm/desk_page/crm/crm.json | 17 +++++++++----- .../twitter_settings/twitter_settings.js | 22 +++++++++++-------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index 747c8e3a41..19c89eb3e2 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -12,13 +12,18 @@ }, { "hidden": 0, - "label": "Settings", - "links": "[\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sends Mails to lead or contact based on a Campaign schedule\",\n \"label\": \"Email Campaign\",\n \"name\": \"Email Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Send mass SMS to your contacts\",\n \"label\": \"SMS Center\",\n \"name\": \"SMS Center\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Logs for maintaining sms delivery status\",\n \"label\": \"SMS Log\",\n \"name\": \"SMS Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup SMS gateway settings\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n }\n]" + "label": "Maintenance", + "links": "[\n {\n \"description\": \"Plan for maintenance visits.\",\n \"label\": \"Maintenance Schedule\",\n \"name\": \"Maintenance Schedule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Visit report for maintenance call.\",\n \"label\": \"Maintenance Visit\",\n \"name\": \"Maintenance Visit\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Warranty Claim against Serial No.\",\n \"label\": \"Warranty Claim\",\n \"name\": \"Warranty Claim\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, - "label": "Maintenance", - "links": "[\n {\n \"description\": \"Plan for maintenance visits.\",\n \"label\": \"Maintenance Schedule\",\n \"name\": \"Maintenance Schedule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Visit report for maintenance call.\",\n \"label\": \"Maintenance Visit\",\n \"name\": \"Maintenance Visit\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Warranty Claim against Serial No.\",\n \"label\": \"Warranty Claim\",\n \"name\": \"Warranty Claim\",\n \"type\": \"doctype\"\n }\n]" + "label": "Campaign", + "links": "[\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sends Mails to lead or contact based on a Campaign schedule\",\n \"label\": \"Email Campaign\",\n \"name\": \"Email Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and Schedule social media posts\",\n \"label\": \"Social Media Post\",\n \"name\": \"Social Media Post\",\n \"type\": \"doctype\"\n }\n]" + }, + { + "hidden": 0, + "label": "Settings", + "links": "[\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Send mass SMS to your contacts\",\n \"label\": \"SMS Center\",\n \"name\": \"SMS Center\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Logs for maintaining sms delivery status\",\n \"label\": \"SMS Log\",\n \"name\": \"SMS Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup SMS gateway settings\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Twitter Settings\",\n \"name\": \"Twitter Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"LinkedIn Settings\",\n \"name\": \"LinkedIn Settings\",\n \"type\": \"doctype\"\n }\n]" } ], "category": "Modules", @@ -33,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "CRM", - "modified": "2020-04-26 22:31:15.865799", + "modified": "2020-04-27 22:32:26.682911", "modified_by": "Administrator", "module": "CRM", "name": "CRM", @@ -42,7 +47,7 @@ "pin_to_top": 0, "shortcuts": [ { - "format": "{} Open", + "format": "Open", "label": "Lead", "link_to": "Lead", "stats_filter": "{\"status\":\"Open\"}", diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.js b/erpnext/crm/doctype/twitter_settings/twitter_settings.js index 8f9c419062..b55946a8bd 100644 --- a/erpnext/crm/doctype/twitter_settings/twitter_settings.js +++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.js @@ -16,23 +16,27 @@ frappe.ui.form.on('Twitter Settings', { } }, refresh: function(frm){ - let msg,color; + let msg, color, flag=false; if (frm.doc.session_status == "Active"){ msg = __("Session Active"); color = 'green'; + flag = true; } - else { + else if(frm.doc.consumer_key && frm.doc.consumer_secret) { msg = __("Session Not Active. Save doc to login."); color = 'red'; + flag = true; } - frm.dashboard.set_headline_alert( - `
-
- -
-
` - ); + if (flag){ + frm.dashboard.set_headline_alert( + `
+
+ +
+
` + ); + } }, login: function(frm){ if (frm.doc.consumer_key && frm.doc.consumer_secret){ From 01d0b373e05835346fbd64e385405c81b2e2a9f6 Mon Sep 17 00:00:00 2001 From: Anupam K Date: Tue, 28 Apr 2020 00:21:10 +0530 Subject: [PATCH 60/73] Review changes --- erpnext/crm/desk_page/crm/crm.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index 19c89eb3e2..ca13d6abb6 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -47,7 +47,7 @@ "pin_to_top": 0, "shortcuts": [ { - "format": "Open", + "format": "{} Open", "label": "Lead", "link_to": "Lead", "stats_filter": "{\"status\":\"Open\"}", From 504a5f3a3ae00d842c7724f435791e259ca8c583 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 28 Apr 2020 11:16:04 +0530 Subject: [PATCH 61/73] fix: reload income_tax_slab_other_charges in patch (#21447) * fix: reload income_tax_slab_other_charges in patch * fix: reload lower_deduction_certificate in patch --- erpnext/patches/v11_0/add_permissions_in_gst_settings.py | 1 + .../move_tax_slabs_from_payroll_period_to_income_tax_slab.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py index 121a20288c..d7936110ed 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -6,4 +6,5 @@ def execute(): if not company: return + frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") add_permissions() \ No newline at end of file diff --git a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py index a6aefac12a..179be2cfde 100644 --- a/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py +++ b/erpnext/patches/v13_0/move_tax_slabs_from_payroll_period_to_income_tax_slab.py @@ -10,7 +10,7 @@ def execute(): if not frappe.db.table_exists("Payroll Period"): return - for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income"): + for doctype in ("income_tax_slab", "salary_structure_assignment", "employee_other_income", "income_tax_slab_other_charges"): frappe.reload_doc("hr", "doctype", doctype) From 142af4b58aafc7ea7dc4814a8e01374afb2f7580 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 11:17:45 +0530 Subject: [PATCH 62/73] fix: Default column width in Gross profit report --- .../report/gross_profit/gross_profit.py | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 6ef6d6eea0..4e22b05a81 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -55,27 +55,27 @@ def get_columns(group_wise_columns, filters): columns = [] column_map = frappe._dict({ "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date", - "posting_time": _("Posting Time"), - "item_code": _("Item Code") + ":Link/Item", - "item_name": _("Item Name"), - "item_group": _("Item Group") + ":Link/Item Group", - "brand": _("Brand"), - "description": _("Description"), - "warehouse": _("Warehouse") + ":Link/Warehouse", - "qty": _("Qty") + ":Float", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency", - "buying_rate": _("Valuation Rate") + ":Currency/currency", - "base_amount": _("Selling Amount") + ":Currency/currency", - "buying_amount": _("Buying Amount") + ":Currency/currency", - "gross_profit": _("Gross Profit") + ":Currency/currency", - "gross_profit_percent": _("Gross Profit %") + ":Percent", - "project": _("Project") + ":Link/Project", + "posting_date": _("Posting Date") + ":Date:100", + "posting_time": _("Posting Time") + ":Data:100", + "item_code": _("Item Code") + ":Link/Item:100", + "item_name": _("Item Name") + ":Data:100", + "item_group": _("Item Group") + ":Link/Item Group:100", + "brand": _("Brand") + ":Link/Brand:100", + "description": _("Description") +":Data:100", + "warehouse": _("Warehouse") + ":Link/Warehouse:100", + "qty": _("Qty") + ":Float:80", + "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", + "buying_rate": _("Valuation Rate") + ":Currency/currency:100", + "base_amount": _("Selling Amount") + ":Currency/currency:100", + "buying_amount": _("Buying Amount") + ":Currency/currency:100", + "gross_profit": _("Gross Profit") + ":Currency/currency:100", + "gross_profit_percent": _("Gross Profit %") + ":Percent:100", + "project": _("Project") + ":Link/Project:100", "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency", - "customer": _("Customer") + ":Link/Customer", - "customer_group": _("Customer Group") + ":Link/Customer Group", - "territory": _("Territory") + ":Link/Territory" + "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", + "customer": _("Customer") + ":Link/Customer:100", + "customer_group": _("Customer Group") + ":Link/Customer Group:100", + "territory": _("Territory") + ":Link/Territory:100" }) for col in group_wise_columns.get(scrub(filters.group_by)): @@ -85,7 +85,8 @@ def get_columns(group_wise_columns, filters): "fieldname": "currency", "label" : _("Currency"), "fieldtype": "Link", - "options": "Currency" + "options": "Currency", + "hidden": 1 }) return columns @@ -277,7 +278,7 @@ class GrossProfitGenerator(object): from `tabPurchase Invoice Item` a where a.item_code = %s and a.docstatus=1 and modified <= %s - order by a.modified desc limit 1""", (item_code,self.filters.to_date)) + order by a.modified desc limit 1""", (item_code, self.filters.to_date)) else: last_purchase_rate = frappe.db.sql(""" select (a.base_rate / a.conversion_factor) From daf37e7570432c1f27e99c7a86b586813e2e708a Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 28 Apr 2020 12:48:22 +0530 Subject: [PATCH 63/73] fix: better validation message for the expense claim and set default cost center in the expenses table (#21454) --- .../hr/doctype/expense_claim/expense_claim.js | 3 ++- .../hr/doctype/expense_claim/expense_claim.py | 21 ++++++++++++++----- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 88f3865434..fb2310396b 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -17,7 +17,7 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ return; } return frappe.call({ - method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account", + method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center", args: { "expense_claim_type": d.expense_type, "company": doc.company @@ -25,6 +25,7 @@ erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({ callback: function(r) { if (r.message) { d.default_account = r.message.account; + d.cost_center = r.message.cost_center; } } }); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index fe8afdf873..ad9d86b66e 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -2,9 +2,9 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals -import frappe +import frappe, erpnext from frappe import _ -from frappe.utils import get_fullname, flt, cstr +from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.model.document import Document from erpnext.hr.utils import set_employee_name from erpnext.accounts.party import get_party_account @@ -192,7 +192,8 @@ class ExpenseClaim(AccountsController): def validate_account_details(self): for data in self.expenses: if not data.cost_center: - frappe.throw(_("Cost center is required to book an expense claim")) + frappe.throw(_("Row {0}: {1} is required in the expenses table to book an expense claim.") + .format(data.idx, frappe.bold("Cost Center"))) if self.is_paid: if not self.mode_of_payment: @@ -308,13 +309,23 @@ def make_bank_entry(dt, dn): return je.as_dict() +@frappe.whitelist() +def get_expense_claim_account_and_cost_center(expense_claim_type, company): + data = get_expense_claim_account(expense_claim_type, company) + cost_center = erpnext.get_default_cost_center(company) + + return { + "account": data.get("account"), + "cost_center": cost_center + } + @frappe.whitelist() def get_expense_claim_account(expense_claim_type, company): account = frappe.db.get_value("Expense Claim Account", {"parent": expense_claim_type, "company": company}, "default_account") if not account: - frappe.throw(_("Please set default account in Expense Claim Type {0}") - .format(expense_claim_type)) + frappe.throw(_("Set the default account for the {0} {1}") + .format(frappe.bold("Expense Claim Type"), get_link_to_form("Expense Claim Type", expense_claim_type))) return { "account": account From 299e21766805f6fcc705928fbfe3f7ca3a4bf8a6 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 28 Apr 2020 13:00:04 +0530 Subject: [PATCH 64/73] fix: Blanket Order in SO/PO child tables (#21442) --- .../doctype/purchase_order/purchase_order.js | 9 --------- erpnext/controllers/queries.py | 13 +++++++++++++ erpnext/public/js/controllers/transaction.js | 14 ++++++++++++++ erpnext/selling/doctype/sales_order/sales_order.js | 11 +---------- 4 files changed, 28 insertions(+), 19 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index ed054aedb5..4a8146a797 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -27,15 +27,6 @@ frappe.ui.form.on("Purchase Order", { frm.set_indicator_formatter('item_code', function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" }) - frm.set_query("blanket_order", "items", function() { - return { - filters: { - "company": frm.doc.company, - "docstatus": 1 - } - } - }); - frm.set_query("expense_account", "items", function() { return { query: "erpnext.controllers.queries.get_expense_account", diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index c14bb669a4..5febfd6bf2 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -371,6 +371,19 @@ def get_account_list(doctype, txt, searchfield, start, page_len, filters): fields = ["name", "parent_account"], limit_start=start, limit_page_length=page_len, as_list=True) +def get_blanket_orders(doctype, txt, searchfield, start, page_len, filters): + return frappe.db.sql("""select distinct bo.name, bo.blanket_order_type, bo.to_date + from `tabBlanket Order` bo, `tabBlanket Order Item` boi + where + boi.parent = bo.name + and boi.item_code = {item_code} + and bo.blanket_order_type = '{blanket_order_type}' + and bo.company = {company} + and bo.docstatus = 1""" + .format(item_code = frappe.db.escape(filters.get("item")), + blanket_order_type = filters.get("blanket_order_type"), + company = frappe.db.escape(filters.get("company")) + )) @frappe.whitelist() def get_income_account(doctype, txt, searchfield, start, page_len, filters): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5843034543..c9d7728521 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -175,6 +175,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }; } + if (this.frm.fields_dict["items"].grid.get_field('blanket_order')) { + this.frm.set_query("blanket_order", "items", function(doc, cdt, cdn) { + var item = locals[cdt][cdn]; + return { + query: "erpnext.controllers.queries.get_blanket_orders", + filters: { + "company": doc.company, + "blanket_order_type": doc.doctype === "Sales Order" ? "Selling" : "Purchasing", + "item": item.item_code + } + } + }); + } + }, onload: function() { var me = this; diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 3c1ffe9596..45a43c5e7e 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -65,15 +65,6 @@ frappe.ui.form.on("Sales Order", { } }); - frm.set_query("blanket_order", "items", function() { - return { - filters: { - "company": frm.doc.company, - "docstatus": 1 - } - } - }); - erpnext.queries.setup_warehouse_query(frm); }, @@ -148,7 +139,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); - + const order_is_a_sale = ["Sales", "Shopping Cart"].indexOf(doc.order_type) !== -1; const order_is_maintenance = ["Maintenance"].indexOf(doc.order_type) !== -1; // order type has been customised then show all the action buttons From 28a4880b48fdfccabfe044c89e6d61e13cd04a56 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 28 Apr 2020 13:01:43 +0530 Subject: [PATCH 65/73] fix: added validation to not allow to select expired batch (#21455) --- erpnext/controllers/stock_controller.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 55a2c435a1..9d453af2ac 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -3,7 +3,7 @@ from __future__ import unicode_literals import frappe, erpnext -from frappe.utils import cint, flt, cstr +from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate from frappe import _ import frappe.defaults from erpnext.accounts.utils import get_fiscal_year @@ -55,6 +55,13 @@ class StockController(AccountsController): frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") .format(d.idx, serial_no_data.name, d.batch_no)) + if d.qty > 0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: + expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") + + if expiry_date and getdate(expiry_date) < getdate(self.posting_date): + frappe.throw(_("Row #{0}: The batch {1} has already expired.") + .format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) + def get_gl_entries(self, warehouse_account=None, default_expense_account=None, default_cost_center=None): From fa2eecc5fc7520fe6f274dbc2ec9b42227f83fc9 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 13:12:22 +0530 Subject: [PATCH 66/73] fix: Report summary fix in consolidated financial statement for report type Profit and Loss --- .../consolidated_financial_statement.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index b62238b59b..c2c7207e37 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -84,6 +84,7 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): def get_profit_loss_data(fiscal_year, companies, columns, filters): income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters) + company_currency = get_company_currency(filters) data = [] data.extend(income or []) @@ -93,7 +94,7 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters): chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss) - report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, True) + report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, True) return data, None, chart, report_summary From 7b14721e2f61060d60284f8066a17c4fd6b42f4b Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 14:25:36 +0530 Subject: [PATCH 67/73] fix: Allow rename for Loan Security --- .../doctype/loan_security/loan_security.json | 21 +++++++++++++------ .../loan_security_type.json | 12 +++++++---- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/erpnext/loan_management/doctype/loan_security/loan_security.json b/erpnext/loan_management/doctype/loan_security/loan_security.json index e6984ee7f1..e879b17a43 100644 --- a/erpnext/loan_management/doctype/loan_security/loan_security.json +++ b/erpnext/loan_management/doctype/loan_security/loan_security.json @@ -1,15 +1,17 @@ { + "actions": [], + "allow_rename": 1, "autoname": "field:loan_security_name", "creation": "2019-09-02 15:07:08.885593", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "loan_security_type", - "loan_security_code", "loan_security_name", "unit_of_measure", + "loan_security_code", "column_break_3", + "loan_security_type", "haircut", "disabled" ], @@ -17,7 +19,9 @@ { "fieldname": "loan_security_name", "fieldtype": "Data", + "in_list_view": 1, "label": "Loan Security Name", + "reqd": 1, "unique": 1 }, { @@ -33,8 +37,10 @@ { "fieldname": "loan_security_type", "fieldtype": "Link", + "in_list_view": 1, "label": "Loan Security Type", - "options": "Loan Security Type" + "options": "Loan Security Type", + "reqd": 1 }, { "fieldname": "loan_security_code", @@ -52,11 +58,15 @@ "fetch_from": "loan_security_type.unit_of_measure", "fieldname": "unit_of_measure", "fieldtype": "Link", + "in_list_view": 1, "label": "Unit Of Measure", - "options": "UOM" + "options": "UOM", + "read_only": 1, + "reqd": 1 } ], - "modified": "2019-11-16 11:36:37.901656", + "links": [], + "modified": "2020-04-28 14:07:54.506896", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security", @@ -87,7 +97,6 @@ "write": 1 } ], - "quick_entry": 1, "search_fields": "loan_security_code", "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json index 5f296093a4..f46b88cbca 100644 --- a/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json +++ b/erpnext/loan_management/doctype/loan_security_type/loan_security_type.json @@ -9,9 +9,9 @@ "loan_security_type", "unit_of_measure", "haircut", - "disabled", "column_break_5", - "loan_to_value_ratio" + "loan_to_value_ratio", + "disabled" ], "fields": [ { @@ -23,7 +23,9 @@ { "fieldname": "loan_security_type", "fieldtype": "Data", + "in_list_view": 1, "label": "Loan Security Type", + "reqd": 1, "unique": 1 }, { @@ -34,8 +36,10 @@ { "fieldname": "unit_of_measure", "fieldtype": "Link", + "in_list_view": 1, "label": "Unit Of Measure", - "options": "UOM" + "options": "UOM", + "reqd": 1 }, { "fieldname": "column_break_5", @@ -48,7 +52,7 @@ } ], "links": [], - "modified": "2020-02-28 12:43:20.364447", + "modified": "2020-04-28 14:06:49.046177", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Type", From 8705df628394e7826fa97a96139179d450ab06d2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 28 Apr 2020 20:19:26 +0530 Subject: [PATCH 68/73] fix: Remove duplicate code from accounting dimension --- .../public/js/utils/dimension_tree_filter.js | 38 +++++++------------ 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js index 75c5a820b4..b223fc557b 100644 --- a/erpnext/public/js/utils/dimension_tree_filter.js +++ b/erpnext/public/js/utils/dimension_tree_filter.js @@ -24,7 +24,7 @@ doctypes_with_dimensions.forEach((doctype) => { onload: function(frm) { erpnext.dimension_filters.forEach((dimension) => { frappe.model.with_doctype(dimension['document_type'], () => { - if (frappe.meta.has_field(dimension['document_type'], 'is_group')) { + if(frappe.meta.has_field(dimension['document_type'], 'is_group')) { frm.set_query(dimension['fieldname'], { "is_group": 0 }); @@ -42,19 +42,21 @@ doctypes_with_dimensions.forEach((doctype) => { update_dimension: function(frm) { erpnext.dimension_filters.forEach((dimension) => { - if (frm.is_new()) { - if (frm.doc.company && Object.keys(default_dimensions || {}).length > 0 + if(frm.is_new()) { + if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0 && default_dimensions[frm.doc.company]) { - if (frappe.meta.has_field(doctype, dimension['fieldname'])) { - frm.set_value(dimension['fieldname'], - default_dimensions[frm.doc.company][dimension['document_type']]); - } + let default_dimension = default_dimensions[frm.doc.company][dimension['document_type']]; - $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) { - frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], - default_dimensions[frm.doc.company][dimension['document_type']]) - }); + if(default_dimension) { + if (frappe.meta.has_field(doctype, dimension['fieldname'])) { + frm.set_value(dimension['fieldname'], default_dimension); + } + + $.each(frm.doc.items || frm.doc.accounts || [], function(i, row) { + frappe.model.set_value(row.doctype, row.name, dimension['fieldname'], default_dimension); + }); + } } } }); @@ -71,20 +73,6 @@ child_docs.forEach((doctype) => { }); }, - accounts_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]); - }); - }, - - items_add: function(frm, cdt, cdn) { - erpnext.dimension_filters.forEach((dimension) => { - var row = frappe.get_doc(cdt, cdn); - frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]); - }); - }, - accounts_add: function(frm, cdt, cdn) { erpnext.dimension_filters.forEach((dimension) => { var row = frappe.get_doc(cdt, cdn); From 6c871d6bcf931884e0f0b9e699790bd63d9f7e89 Mon Sep 17 00:00:00 2001 From: marination Date: Tue, 28 Apr 2020 21:28:53 +0530 Subject: [PATCH 69/73] fix: Removed Finished Product and Finished Qty columns from Stock Ledger Report --- erpnext/stock/report/stock_ledger/stock_ledger.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 28d72084de..0190f09f3d 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -46,19 +46,6 @@ def execute(filters=None): "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) if include_uom: @@ -77,8 +64,6 @@ def get_columns(): {"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}, From 49bb8ccd2e65c730d674cb3d421f31e486216183 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <> Date: Tue, 28 Apr 2020 18:56:47 +0200 Subject: [PATCH 70/73] feat: add Bank Account to dashboards --- erpnext/buying/doctype/supplier/supplier_dashboard.py | 8 ++------ erpnext/education/doctype/student/student_dashboard.py | 7 +++++-- erpnext/hr/doctype/employee/employee_dashboard.py | 5 ++++- erpnext/non_profit/doctype/member/member_dashboard.py | 9 ++++++++- erpnext/selling/doctype/customer/customer_dashboard.py | 5 +++-- 5 files changed, 22 insertions(+), 12 deletions(-) diff --git a/erpnext/buying/doctype/supplier/supplier_dashboard.py b/erpnext/buying/doctype/supplier/supplier_dashboard.py index d0d5b73984..16251035bd 100644 --- a/erpnext/buying/doctype/supplier/supplier_dashboard.py +++ b/erpnext/buying/doctype/supplier/supplier_dashboard.py @@ -23,15 +23,11 @@ def get_data(): }, { 'label': _('Payments'), - 'items': ['Payment Entry'] - }, - { - 'label': _('Bank'), - 'items': ['Bank Account'] + 'items': ['Payment Entry', 'Bank Account'] }, { 'label': _('Pricing'), 'items': ['Pricing Rule'] } ] - } \ No newline at end of file + } diff --git a/erpnext/education/doctype/student/student_dashboard.py b/erpnext/education/doctype/student/student_dashboard.py index 0cbd17b8a4..d2614628b1 100644 --- a/erpnext/education/doctype/student/student_dashboard.py +++ b/erpnext/education/doctype/student/student_dashboard.py @@ -6,6 +6,9 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('This is based on the attendance of this Student'), 'fieldname': 'student', + 'non_standard_fieldnames': { + 'Bank Account': 'party' + }, 'transactions': [ { 'label': _('Admission'), @@ -29,7 +32,7 @@ def get_data(): }, { 'label': _('Fee'), - 'items': ['Fees'] + 'items': ['Fees', 'Bank Account'] } ] - } \ No newline at end of file + } diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py index 11ad83ba37..0203332164 100644 --- a/erpnext/hr/doctype/employee/employee_dashboard.py +++ b/erpnext/hr/doctype/employee/employee_dashboard.py @@ -6,6 +6,9 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('This is based on the attendance of this Employee'), 'fieldname': 'employee', + 'non_standard_fieldnames': { + 'Bank Account': 'party' + }, 'transactions': [ { 'label': _('Leave and Attendance'), @@ -33,7 +36,7 @@ def get_data(): }, { 'label': _('Payroll'), - 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus'] + 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account'] }, { 'label': _('Training'), diff --git a/erpnext/non_profit/doctype/member/member_dashboard.py b/erpnext/non_profit/doctype/member/member_dashboard.py index 945fb7b7d3..743db2513a 100644 --- a/erpnext/non_profit/doctype/member/member_dashboard.py +++ b/erpnext/non_profit/doctype/member/member_dashboard.py @@ -6,10 +6,17 @@ def get_data(): 'heatmap': True, 'heatmap_message': _('Member Activity'), 'fieldname': 'member', + 'non_standard_fieldnames': { + 'Bank Account': 'party' + }, 'transactions': [ { 'label': _('Membership Details'), 'items': ['Membership'] + }, + { + 'label': _('Fee'), + 'items': ['Bank Account'] } ] - } \ No newline at end of file + } diff --git a/erpnext/selling/doctype/customer/customer_dashboard.py b/erpnext/selling/doctype/customer/customer_dashboard.py index 654dd48c66..22e30e3113 100644 --- a/erpnext/selling/doctype/customer/customer_dashboard.py +++ b/erpnext/selling/doctype/customer/customer_dashboard.py @@ -11,7 +11,8 @@ def get_data(): 'non_standard_fieldnames': { 'Payment Entry': 'party', 'Quotation': 'party_name', - 'Opportunity': 'party_name' + 'Opportunity': 'party_name', + 'Bank Account': 'party' }, 'dynamic_links': { 'party_name': ['Customer', 'quotation_to'] @@ -27,7 +28,7 @@ def get_data(): }, { 'label': _('Payments'), - 'items': ['Payment Entry'] + 'items': ['Payment Entry', 'Bank Account'] }, { 'label': _('Support'), From 50b4106d1dece03412c57fca596e73c1adf14857 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 29 Apr 2020 02:27:47 +0530 Subject: [PATCH 71/73] fix: payment request not able to make against fees --- erpnext/accounts/doctype/payment_request/payment_request.py | 6 +++--- erpnext/education/doctype/fees/fees.js | 2 ++ erpnext/education/doctype/fees/fees.py | 3 ++- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index 53ff2225d3..68aeb6d1d6 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -326,7 +326,7 @@ def make_payment_request(**args): "reference_doctype": args.dt, "reference_name": args.dn, "party_type": args.get("party_type") or "Customer", - "party": args.get("party") or ref_doc.customer, + "party": args.get("party") or ref_doc.get("customer"), "bank_account": bank_account }) @@ -420,7 +420,7 @@ def make_payment_entry(docname): def update_payment_req_status(doc, method): from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details - + for ref in doc.references: payment_request_name = frappe.db.get_value("Payment Request", {"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name, @@ -430,7 +430,7 @@ def update_payment_req_status(doc, method): ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency) pay_req_doc = frappe.get_doc('Payment Request', payment_request_name) status = pay_req_doc.status - + if status != "Paid" and not ref_details.outstanding_amount: status = 'Paid' elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount: diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js index e2c6f1d856..17ef44954b 100644 --- a/erpnext/education/doctype/fees/fees.js +++ b/erpnext/education/doctype/fees/fees.js @@ -112,6 +112,8 @@ frappe.ui.form.on("Fees", { args: { "dt": frm.doc.doctype, "dn": frm.doc.name, + "party_type": "Student", + "party": frm.doc.student, "recipient_id": frm.doc.student_email }, callback: function(r) { diff --git a/erpnext/education/doctype/fees/fees.py b/erpnext/education/doctype/fees/fees.py index aa616e6206..f31003bf32 100644 --- a/erpnext/education/doctype/fees/fees.py +++ b/erpnext/education/doctype/fees/fees.py @@ -75,7 +75,8 @@ class Fees(AccountsController): self.make_gl_entries() if self.send_payment_request and self.student_email: - pr = make_payment_request(dt="Fees", dn=self.name, recipient_id=self.student_email, + pr = make_payment_request(party_type="Student", party=self.student, dt="Fees", + dn=self.name, recipient_id=self.student_email, submit_doc=True, use_dummy_message=True) frappe.msgprint(_("Payment request {0} created").format(getlink("Payment Request", pr.name))) From 33793d4e0df0355e779b90bac7deed294911fe67 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Wed, 29 Apr 2020 11:48:41 +0530 Subject: [PATCH 72/73] fix: Permission issue Employee Tax exemption (#21490) --- erpnext/hr/doctype/salary_structure/salary_structure.py | 4 ++-- erpnext/regional/india/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/salary_structure/salary_structure.py b/erpnext/hr/doctype/salary_structure/salary_structure.py index df76458fe0..5ba7f1c432 100644 --- a/erpnext/hr/doctype/salary_structure/salary_structure.py +++ b/erpnext/hr/doctype/salary_structure/salary_structure.py @@ -149,7 +149,7 @@ def get_existing_assignments(employees, salary_structure, from_date): return salary_structures_assignments @frappe.whitelist() -def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0): +def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None, for_preview=0, ignore_permissions=False): def postprocess(source, target): if employee: employee_details = frappe.db.get_value("Employee", employee, @@ -169,7 +169,7 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = "name": "salary_structure" } } - }, target_doc, postprocess, ignore_child_tables=True) + }, target_doc, postprocess, ignore_child_tables=True, ignore_permissions=ignore_permissions) if cint(as_print): doc.name = 'Preview for {0}'.format(employee) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 094f01017b..33098587c2 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -288,7 +288,7 @@ def calculate_annual_eligible_hra_exemption(doc): }) def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component): - salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1) + salary_slip = make_salary_slip(salary_structure, employee=employee, for_preview=1, ignore_permissions=True) basic_amt, hra_amt = 0, 0 for earning in salary_slip.earnings: if earning.salary_component == basic_component: From 3d8dadaab6e075814284dba0742bc4a690c6d75a Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Wed, 29 Apr 2020 12:05:56 +0530 Subject: [PATCH 73/73] fix: Group by filter fix in item wise sales and purchase register --- .../item_wise_purchase_register/item_wise_purchase_register.py | 2 +- .../report/item_wise_sales_register/item_wise_sales_register.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index 127f3133f5..1f78c7a006 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -102,7 +102,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data.append(row) - if filters.get('group_by'): + if filters.get('group_by') and item_list: total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) data.append(total_row) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 0c8957ae44..92a22e62f1 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -111,7 +111,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data.append(row) - if filters.get('group_by'): + if filters.get('group_by') and item_list: total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) data.append(total_row)