Merge pull request #21389 from marination/stock-entry-qty
fix: Issues on qty trigger in Stock Entry Detail
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 05f0147..a378a51 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -302,7 +302,7 @@
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/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index e13fcb9..19f571f 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -237,6 +237,7 @@
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/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 6f78db2..60e41f9 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -32,6 +32,7 @@
me.frm.script_manager.trigger("is_pos");
me.frm.refresh_fields();
}
+ erpnext.queries.setup_warehouse_query(this.frm);
},
refresh: function(doc, dt, dn) {
@@ -586,7 +587,9 @@
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
}
};
});
@@ -667,7 +670,8 @@
frm.fields_dict["loyalty_redemption_account"].get_query = function() {
return {
filters:{
- "company": frm.doc.company
+ "company": frm.doc.company,
+ "is_group": 0
}
}
};
@@ -676,7 +680,8 @@
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/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 6550981..260f35f 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 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 @@
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 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 @@
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 @@
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/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py
index 00a4bd1..8f60ecf 100644
--- a/erpnext/crm/doctype/email_campaign/email_campaign.py
+++ b/erpnext/crm/doctype/email_campaign/email_campaign.py
@@ -27,7 +27,7 @@
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/crm/doctype/linkedin_settings/__init__.py b/erpnext/crm/doctype/linkedin_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/__init__.py
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 0000000..50b98e9
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.js
@@ -0,0 +1,71 @@
+// 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"){
+ let msg = __("Session Not Active. Save doc to login.");
+ frm.dashboard.set_headline_alert(
+ `<div class="row">
+ <div class="col-xs-12">
+ <span class="indicator whitespace-nowrap red"><span class="hidden-xs">${msg}</span></span>
+ </div>
+ </div>`
+ );
+ }
+
+ 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(
+ `<div class="row">
+ <div class="col-xs-12">
+ <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">${msg}</span></span>
+ </div>
+ </div>`
+ );
+ }
+ },
+ login: function(frm){
+ if (frm.doc.consumer_key && frm.doc.consumer_secret){
+ frappe.dom.freeze();
+ 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 0000000..9eacb00
--- /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 0000000..5df35df
--- /dev/null
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -0,0 +1,166 @@
+# -*- 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())
+
+ 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")
+
+ 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/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 0000000..9c3ef3f
--- /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 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/__init__.py
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 0000000..3a14f2d
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.js
@@ -0,0 +1,67 @@
+// 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.twitter === 0 && frm.doc.linkedin === 0){
+ frappe.throw(__("Select atleast one Social Media from Share on."))
+ }
+ 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()){
+ frappe.throw(__("Invalid Scheduled Time"));
+ }
+ }
+ 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 += `<div class="col-xs-6">
+ <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">Twitter : ${status} </span></span>
+ </div>` ;
+ }
+ if (frm.doc.linkedin){
+ let color = frm.doc.linkedin_post_id ? "green" : "red";
+ let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
+ html += `<div class="col-xs-6">
+ <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">LinkedIn : ${status} </span></span>
+ </div>` ;
+ }
+ html = `<div class="row">${html}</div>`;
+ 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.dom.freeze();
+ 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();
+ frappe.dom.unfreeze();
+ }
+ })
+
+}
\ 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 0000000..2601c14
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.json
@@ -0,0 +1,166 @@
+{
+ "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",
+ "scheduled_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",
+ "column_break_15",
+ "attachments_section",
+ "image",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "text",
+ "fieldtype": "Small Text",
+ "label": "Tweet",
+ "mandatory_depends_on": "eval:doc.twitter ==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
+ },
+ {
+ "depends_on": "eval:doc.twitter ==1",
+ "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,
+ "depends_on": "eval:doc.linkedin==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",
+ "mandatory_depends_on": "eval:doc.linkedin ==1"
+ },
+ {
+ "fieldname": "column_break_15",
+ "fieldtype": "Column Break"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "scheduled_time",
+ "fieldtype": "Datetime",
+ "label": "Scheduled Time",
+ "read_only_depends_on": "eval:doc.post_status == \"Posted\""
+ }
+ ],
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-04-21 15:10:04.953713",
+ "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 0000000..ed1b583
--- /dev/null
+++ b/erpnext/crm/doctype/social_media_post/social_media_post.py
@@ -0,0 +1,56 @@
+# -*- 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 _
+import datetime
+
+class SocialMediaPost(Document):
+ def validate(self):
+ if self.scheduled_time:
+ current_time = frappe.utils.now_datetime()
+ scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
+ if scheduled_time < current_time:
+ frappe.throw(_("Invalid Scheduled Time"))
+
+ def submit(self):
+ if self.scheduled_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.linkedin_post, self.image)
+ self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1])
+ self.db_set("post_status", "Posted")
+
+ except:
+ 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():
+ posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time","post_status"])
+ start = frappe.utils.now_datetime()
+ end = start + datetime.timedelta(minutes=10)
+ for post in posts:
+ if post.scheduled_time:
+ post_time = frappe.utils.get_datetime(post.scheduled_time)
+ if post_time > start and post_time <= end:
+ publish('Social Media Post', post.name)
+
+@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 0000000..c60b91a
--- /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 0000000..ec81ee5
--- /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 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/twitter_settings/__init__.py
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 0000000..3f999c1
--- /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 0000000..8f9c419
--- /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){
+ let msg,color;
+ if (frm.doc.session_status == "Active"){
+ msg = __("Session Active");
+ color = 'green';
+ }
+ else {
+ msg = __("Session Not Active. Save doc to login.");
+ color = 'red';
+ }
+
+ frm.dashboard.set_headline_alert(
+ `<div class="row">
+ <div class="col-xs-12">
+ <span class="indicator whitespace-nowrap ${color}"><span class="hidden-xs">${msg}</span></span>
+ </div>
+ </div>`
+ );
+ },
+ login: function(frm){
+ if (frm.doc.consumer_key && frm.doc.consumer_secret){
+ frappe.dom.freeze();
+ 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 0000000..f92e7f0
--- /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": "API Key",
+ "reqd": 1
+ },
+ {
+ "fieldname": "consumer_secret",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "API 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-21 22:06:43.726798",
+ "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 0000000..64f53b5
--- /dev/null
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -0,0 +1,98 @@
+# -*- 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)
+ api = self.get_api()
+ user = api.me()
+ profile_pic = (user._json["profile_image_url"]).replace("_normal","")
+
+ 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:
+ 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/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
index 4422d23..6188652 100644
--- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
+++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
@@ -144,6 +144,10 @@
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 @@
"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/hooks.py b/erpnext/hooks.py
index 6b6d1e2..e6f6c8e 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -270,7 +270,8 @@
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",
+ "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/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py
index f10e3b6..f0663ae 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_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
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/employee_other_income/__init__.py
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 0000000..c1a74e8
--- /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 0000000..2dd6c10
--- /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 0000000..ab63c0d
--- /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 0000000..2eeca7a
--- /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 e102ff8..18fad85 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 f2bba7a..fb71a28 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 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 9c87bbd..9549fd1 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 @@
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 c170c16..8b117a2 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"
@@ -112,16 +110,6 @@
"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 97ceb63..5bc33a6 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 @@
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 @@
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 @@
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 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab/__init__.py
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 0000000..73a54eb
--- /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 0000000..6d89b19
--- /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 0000000..253f023
--- /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 0000000..deaaf65
--- /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 0000000..e69de29
--- /dev/null
+++ b/erpnext/hr/doctype/income_tax_slab_other_charges/__init__.py
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 0000000..b23fb3d
--- /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 0000000..b4098ec
--- /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 c9bac09..c0fa506 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 c176959..6956c38 100644
--- a/erpnext/hr/doctype/payroll_period/payroll_period.py
+++ b/erpnext/hr/doctype/payroll_period/payroll_period.py
@@ -45,8 +45,9 @@
+ _(") 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 986030d..5487e1d 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": "<h3>Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base < 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS > 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>"
- },
- {
- "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": "<h3>Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base < 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS > 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>"
+ },
+ {
+ "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 7b22b48..104b44f 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 965cc9e..4f7db0c 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 @@
"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 bde735d..545f56a 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": "<h3>Condition and Formula Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Use field name from Salary Slip in conditions and formulas. <code>Payment Days = payment_days</code><code>Leave without pay = leave_without_pay</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base < 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS > 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>"
+ },
+ {
+ "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 223c4e3..40fe572 100644
--- a/erpnext/hr/doctype/salary_slip/salary_slip.py
+++ b/erpnext/hr/doctype/salary_slip/salary_slip.py
@@ -451,7 +451,8 @@
'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 @@
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 @@
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 @@
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 @@
"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 @@
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 @@
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 ecccac7..73bb19e 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 @@
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 @@
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 @@
# 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 @@
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 @@
# 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 @@
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 @@
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 @@
# 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 @@
# 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 @@
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 @@
{
"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 @@
"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 @@
}).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 7120448..7748403 100755
--- a/erpnext/hr/doctype/salary_structure/salary_structure.js
+++ b/erpnext/hr/doctype/salary_structure/salary_structure.js
@@ -82,6 +82,7 @@
{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 568277f..df76458 100644
--- a/erpnext/hr/doctype/salary_structure/salary_structure.py
+++ b/erpnext/hr/doctype/salary_structure/salary_structure.py
@@ -16,6 +16,7 @@
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 @@
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 @@
@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 @@
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 @@
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 @@
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 6ca6dfd..c1869f0 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 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 @@
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 @@
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 @@
}
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 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 56a05e0..818e853 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 @@
}
}
});
+
+ 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 380c889..0098aa8 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 ef27600..cd12510 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -9,6 +9,8 @@
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 @@
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/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 2918486..c9e36a8 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 @@
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 @@
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",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index a83d193..b1fc4de 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -193,7 +193,7 @@
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":
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 358a542..c3f27cd 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -346,6 +346,7 @@
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 d541866..4314517 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -313,7 +313,7 @@
"Work in Progress": "progress-bar-warning",
"Completed": "progress-bar-success"
};
-
+
let bars = [];
let message = '';
let title = '';
@@ -404,7 +404,6 @@
},
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 e6990fd..00a67a0 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",
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 765f911..a73e8c1 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -669,3 +669,5 @@
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
+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 fa3605b..83fb53d 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 @@
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/v12_0/update_end_date_and_status_in_email_campaign.py b/erpnext/patches/v12_0/update_end_date_and_status_in_email_campaign.py
new file mode 100644
index 0000000..db71a73
--- /dev/null
+++ b/erpnext/patches/v12_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
diff --git a/erpnext/patches/v13_0/__init__.py b/erpnext/patches/v13_0/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/patches/v13_0/__init__.py
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 0000000..a6aefac
--- /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/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 3443abc..4296447 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1412,7 +1412,7 @@
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]);
}
}
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 4be6804..b4e3558 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -531,12 +531,18 @@
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):
diff --git a/erpnext/selling/doctype/campaign/campaign_dashboard.py b/erpnext/selling/doctype/campaign/campaign_dashboard.py
index a9d8eca..3cef560 100644
--- a/erpnext/selling/doctype/campaign/campaign_dashboard.py
+++ b/erpnext/selling/doctype/campaign/campaign_dashboard.py
@@ -8,6 +8,10 @@
{
'label': _('Email Campaigns'),
'items': ['Email Campaign']
+ },
+ {
+ 'label': _('Social Media Campaigns'),
+ 'items': ['Social Media Post']
}
- ],
+ ]
}
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 61aa608..3c1ffe9 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -148,9 +148,14 @@
}
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 @@
}
// 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 @@
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'));
}
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index b97da69..eb298a6 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -299,6 +299,7 @@
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 2d98557..739d749 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -8,7 +8,7 @@
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 @@
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 = ["""<a href="#Form/Work Order/%s" target="_blank">%s</a>""" % \
- (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")), '<br>' + ', '.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: <br> {0}")
+ .format(new_line_sep(errors)))
return work_orders
diff --git a/requirements.txt b/requirements.txt
index c277545..9da537e 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -8,3 +8,4 @@
python-stdnum==1.12
Unidecode==1.1.1
WooCommerce==2.1.1
+tweepy==3.8.0
\ No newline at end of file