Merge branch 'develop' into fix-scrap-items-updation
This commit is contained in:
commit
f5e52549e2
@ -320,7 +320,8 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
pos2.get("items")[0].serial_no = serial_nos[0]
|
pos2.get("items")[0].serial_no = serial_nos[0]
|
||||||
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, pos2.insert)
|
pos2.insert()
|
||||||
|
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||||
|
|
||||||
def test_delivered_serialized_item_transaction(self):
|
def test_delivered_serialized_item_transaction(self):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
|
||||||
@ -348,7 +349,8 @@ class TestPOSInvoice(unittest.TestCase):
|
|||||||
pos2.get("items")[0].serial_no = serial_nos[0]
|
pos2.get("items")[0].serial_no = serial_nos[0]
|
||||||
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000})
|
||||||
|
|
||||||
self.assertRaises(frappe.ValidationError, pos2.insert)
|
pos2.insert()
|
||||||
|
self.assertRaises(frappe.ValidationError, pos2.submit)
|
||||||
|
|
||||||
def test_loyalty_points(self):
|
def test_loyalty_points(self):
|
||||||
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
|
from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records
|
||||||
|
|||||||
@ -36,6 +36,7 @@ class Lead(SellingController):
|
|||||||
})
|
})
|
||||||
|
|
||||||
def set_full_name(self):
|
def set_full_name(self):
|
||||||
|
if self.first_name:
|
||||||
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
|
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
|
||||||
|
|
||||||
def validate_email_id(self):
|
def validate_email_id(self):
|
||||||
|
|||||||
@ -2,8 +2,8 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('LinkedIn Settings', {
|
frappe.ui.form.on('LinkedIn Settings', {
|
||||||
onload: function(frm){
|
onload: function(frm) {
|
||||||
if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
|
if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret) {
|
||||||
frappe.confirm(
|
frappe.confirm(
|
||||||
__('Session not valid, Do you want to login?'),
|
__('Session not valid, Do you want to login?'),
|
||||||
function(){
|
function(){
|
||||||
@ -14,8 +14,9 @@ frappe.ui.form.on('LinkedIn Settings', {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings'>${__('Click here')}</a>`]));
|
||||||
},
|
},
|
||||||
refresh: function(frm){
|
refresh: function(frm) {
|
||||||
if (frm.doc.session_status=="Expired"){
|
if (frm.doc.session_status=="Expired"){
|
||||||
let msg = __("Session Not Active. Save doc to login.");
|
let msg = __("Session Not Active. Save doc to login.");
|
||||||
frm.dashboard.set_headline_alert(
|
frm.dashboard.set_headline_alert(
|
||||||
@ -53,7 +54,7 @@ frappe.ui.form.on('LinkedIn Settings', {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
login: function(frm){
|
login: function(frm) {
|
||||||
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@ -67,7 +68,7 @@ frappe.ui.form.on('LinkedIn Settings', {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
after_save: function(frm){
|
after_save: function(frm) {
|
||||||
frm.trigger("login");
|
frm.trigger("login");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2020-01-30 13:36:39.492931",
|
"creation": "2020-01-30 13:36:39.492931",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/linkedin-settings",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
@ -87,7 +88,7 @@
|
|||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-04-16 23:22:51.966397",
|
"modified": "2021-02-18 15:19:21.920725",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "LinkedIn Settings",
|
"name": "LinkedIn Settings",
|
||||||
|
|||||||
@ -3,11 +3,12 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe, requests, json
|
import frappe
|
||||||
|
import requests
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import get_site_url, get_url_to_form, get_link_to_form
|
from frappe.utils import get_url_to_form
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils.file_manager import get_file, get_file_path
|
from frappe.utils.file_manager import get_file_path
|
||||||
from six.moves.urllib.parse import urlencode
|
from six.moves.urllib.parse import urlencode
|
||||||
|
|
||||||
class LinkedInSettings(Document):
|
class LinkedInSettings(Document):
|
||||||
@ -42,11 +43,7 @@ class LinkedInSettings(Document):
|
|||||||
self.db_set("access_token", response["access_token"])
|
self.db_set("access_token", response["access_token"])
|
||||||
|
|
||||||
def get_member_profile(self):
|
def get_member_profile(self):
|
||||||
headers = {
|
response = requests.get(url="https://api.linkedin.com/v2/me", headers=self.get_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())
|
response = frappe.parse_json(response.content.decode())
|
||||||
|
|
||||||
frappe.db.set_value(self.doctype, self.name, {
|
frappe.db.set_value(self.doctype, self.name, {
|
||||||
@ -55,16 +52,16 @@ class LinkedInSettings(Document):
|
|||||||
"session_status": "Active"
|
"session_status": "Active"
|
||||||
})
|
})
|
||||||
frappe.local.response["type"] = "redirect"
|
frappe.local.response["type"] = "redirect"
|
||||||
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings","LinkedIn Settings")
|
frappe.local.response["location"] = get_url_to_form("LinkedIn Settings", "LinkedIn Settings")
|
||||||
|
|
||||||
def post(self, text, media=None):
|
def post(self, text, title, media=None):
|
||||||
if not media:
|
if not media:
|
||||||
return self.post_text(text)
|
return self.post_text(text, title)
|
||||||
else:
|
else:
|
||||||
media_id = self.upload_image(media)
|
media_id = self.upload_image(media)
|
||||||
|
|
||||||
if media_id:
|
if media_id:
|
||||||
return self.post_text(text, media_id=media_id)
|
return self.post_text(text, title, media_id=media_id)
|
||||||
else:
|
else:
|
||||||
frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
|
frappe.log_error("Failed to upload media.","LinkedIn Upload Error")
|
||||||
|
|
||||||
@ -82,9 +79,7 @@ class LinkedInSettings(Document):
|
|||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
headers = {
|
headers = self.get_headers()
|
||||||
"Authorization": "Bearer {}".format(self.access_token)
|
|
||||||
}
|
|
||||||
response = self.http_post(url=register_url, body=body, headers=headers)
|
response = self.http_post(url=register_url, body=body, headers=headers)
|
||||||
|
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
@ -100,24 +95,33 @@ class LinkedInSettings(Document):
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def post_text(self, text, media_id=None):
|
def post_text(self, text, title, media_id=None):
|
||||||
url = "https://api.linkedin.com/v2/shares"
|
url = "https://api.linkedin.com/v2/shares"
|
||||||
headers = {
|
headers = self.get_headers()
|
||||||
"X-Restli-Protocol-Version": "2.0.0",
|
headers["X-Restli-Protocol-Version"] = "2.0.0"
|
||||||
"Authorization": "Bearer {}".format(self.access_token),
|
headers["Content-Type"] = "application/json; charset=UTF-8"
|
||||||
"Content-Type": "application/json; charset=UTF-8"
|
|
||||||
}
|
|
||||||
body = {
|
body = {
|
||||||
"distribution": {
|
"distribution": {
|
||||||
"linkedInDistributionTarget": {}
|
"linkedInDistributionTarget": {}
|
||||||
},
|
},
|
||||||
"owner":"urn:li:organization:{0}".format(self.company_id),
|
"owner":"urn:li:organization:{0}".format(self.company_id),
|
||||||
"subject": "Test Share Subject",
|
"subject": title,
|
||||||
"text": {
|
"text": {
|
||||||
"text": text
|
"text": text
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reference_url = self.get_reference_url(text)
|
||||||
|
if reference_url:
|
||||||
|
body["content"] = {
|
||||||
|
"contentEntities": [
|
||||||
|
{
|
||||||
|
"entityLocation": reference_url
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
if media_id:
|
if media_id:
|
||||||
body["content"]= {
|
body["content"]= {
|
||||||
"contentEntities": [{
|
"contentEntities": [{
|
||||||
@ -141,20 +145,60 @@ class LinkedInSettings(Document):
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
content = json.loads(response.content)
|
self.api_error(response)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
return {
|
||||||
|
"Authorization": "Bearer {}".format(self.access_token)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_reference_url(self, text):
|
||||||
|
import re
|
||||||
|
regex_url = r"http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+"
|
||||||
|
urls = re.findall(regex_url, text)
|
||||||
|
if urls:
|
||||||
|
return urls[0]
|
||||||
|
|
||||||
|
def delete_post(self, post_id):
|
||||||
|
try:
|
||||||
|
response = requests.delete(url="https://api.linkedin.com/v2/shares/urn:li:share:{0}".format(post_id), headers=self.get_headers())
|
||||||
|
if response.status_code !=200:
|
||||||
|
raise
|
||||||
|
except Exception:
|
||||||
|
self.api_error(response)
|
||||||
|
|
||||||
|
def get_post(self, post_id):
|
||||||
|
url = "https://api.linkedin.com/v2/organizationalEntityShareStatistics?q=organizationalEntity&organizationalEntity=urn:li:organization:{0}&shares[0]=urn:li:share:{1}".format(self.company_id, post_id)
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url=url, headers=self.get_headers())
|
||||||
|
if response.status_code !=200:
|
||||||
|
raise
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.api_error(response)
|
||||||
|
|
||||||
|
response = frappe.parse_json(response.content.decode())
|
||||||
|
if len(response.elements):
|
||||||
|
return response.elements[0]
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def api_error(self, response):
|
||||||
|
content = frappe.parse_json(response.content.decode())
|
||||||
|
|
||||||
if response.status_code == 401:
|
if response.status_code == 401:
|
||||||
self.db_set("session_status", "Expired")
|
self.db_set("session_status", "Expired")
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.throw(content["message"], title="LinkedIn Error - Unauthorized")
|
frappe.throw(content["message"], title=_("LinkedIn Error - Unauthorized"))
|
||||||
elif response.status_code == 403:
|
elif response.status_code == 403:
|
||||||
frappe.msgprint(_("You Didn't have permission to access this API"))
|
frappe.msgprint(_("You didn't have permission to access this API"))
|
||||||
frappe.throw(content["message"], title="LinkedIn Error - Access Denied")
|
frappe.throw(content["message"], title=_("LinkedIn Error - Access Denied"))
|
||||||
else:
|
else:
|
||||||
frappe.throw(response.reason, title=response.status_code)
|
frappe.throw(response.reason, title=response.status_code)
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def callback(code=None, error=None, error_description=None):
|
def callback(code=None, error=None, error_description=None):
|
||||||
if not error:
|
if not error:
|
||||||
|
|||||||
@ -1,39 +1,117 @@
|
|||||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
frappe.ui.form.on('Social Media Post', {
|
frappe.ui.form.on('Social Media Post', {
|
||||||
validate: function(frm){
|
validate: function(frm) {
|
||||||
if (frm.doc.twitter === 0 && frm.doc.linkedin === 0){
|
if (frm.doc.twitter === 0 && frm.doc.linkedin === 0) {
|
||||||
frappe.throw(__("Select atleast one Social Media from Share on."))
|
frappe.throw(__("Select atleast one Social Media Platform to Share on."));
|
||||||
}
|
}
|
||||||
if (frm.doc.scheduled_time) {
|
if (frm.doc.scheduled_time) {
|
||||||
let scheduled_time = new Date(frm.doc.scheduled_time);
|
let scheduled_time = new Date(frm.doc.scheduled_time);
|
||||||
let date_time = new Date();
|
let date_time = new Date();
|
||||||
if (scheduled_time.getTime() < date_time.getTime()){
|
if (scheduled_time.getTime() < date_time.getTime()) {
|
||||||
frappe.throw(__("Invalid Scheduled Time"));
|
frappe.throw(__("Scheduled Time must be a future time."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (frm.doc.text?.length > 280){
|
frm.trigger('validate_tweet_length');
|
||||||
frappe.throw(__("Length Must be less than 280."))
|
},
|
||||||
|
|
||||||
|
text: function(frm) {
|
||||||
|
if (frm.doc.text) {
|
||||||
|
frm.set_df_property('text', 'description', `${frm.doc.text.length}/280`);
|
||||||
|
frm.refresh_field('text');
|
||||||
|
frm.trigger('validate_tweet_length');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
refresh: function(frm){
|
|
||||||
if (frm.doc.docstatus === 1){
|
validate_tweet_length: function(frm) {
|
||||||
if (frm.doc.post_status != "Posted"){
|
if (frm.doc.text && frm.doc.text.length > 280) {
|
||||||
add_post_btn(frm);
|
frappe.throw(__("Tweet length Must be less than 280."));
|
||||||
}
|
}
|
||||||
else if (frm.doc.post_status == "Posted"){
|
},
|
||||||
frm.set_df_property('sheduled_time', 'read_only', 1);
|
|
||||||
|
onload: function(frm) {
|
||||||
|
frm.trigger('make_dashboard');
|
||||||
|
},
|
||||||
|
|
||||||
|
make_dashboard: function(frm) {
|
||||||
|
if (frm.doc.post_status == "Posted") {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'get_post',
|
||||||
|
freeze: true,
|
||||||
|
callback: (r) => {
|
||||||
|
if (!r.message) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let datasets = [], colors = [];
|
||||||
|
if (r.message && r.message.twitter) {
|
||||||
|
colors.push('#1DA1F2');
|
||||||
|
datasets.push({
|
||||||
|
name: 'Twitter',
|
||||||
|
values: [r.message.twitter.favorite_count, r.message.twitter.retweet_count]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (r.message && r.message.linkedin) {
|
||||||
|
colors.push('#0077b5');
|
||||||
|
datasets.push({
|
||||||
|
name: 'LinkedIn',
|
||||||
|
values: [r.message.linkedin.totalShareStatistics.likeCount, r.message.linkedin.totalShareStatistics.shareCount]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (datasets.length) {
|
||||||
|
frm.dashboard.render_graph({
|
||||||
|
data: {
|
||||||
|
labels: ['Likes', 'Retweets/Shares'],
|
||||||
|
datasets: datasets
|
||||||
|
},
|
||||||
|
|
||||||
|
title: __("Post Metrics"),
|
||||||
|
type: 'bar',
|
||||||
|
height: 300,
|
||||||
|
colors: colors
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function(frm) {
|
||||||
|
frm.trigger('text');
|
||||||
|
|
||||||
|
if (frm.doc.docstatus === 1) {
|
||||||
|
if (!['Posted', 'Deleted'].includes(frm.doc.post_status)) {
|
||||||
|
frm.trigger('add_post_btn');
|
||||||
|
}
|
||||||
|
if (frm.doc.post_status !='Deleted') {
|
||||||
|
frm.add_custom_button(('Delete Post'), function() {
|
||||||
|
frappe.confirm(__('Are you sure want to delete the Post from Social Media platforms?'),
|
||||||
|
function() {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'delete_post',
|
||||||
|
freeze: true,
|
||||||
|
callback: () => {
|
||||||
|
frm.reload_doc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (frm.doc.post_status !='Deleted') {
|
||||||
let html='';
|
let html='';
|
||||||
if (frm.doc.twitter){
|
if (frm.doc.twitter) {
|
||||||
let color = frm.doc.twitter_post_id ? "green" : "red";
|
let color = frm.doc.twitter_post_id ? "green" : "red";
|
||||||
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
|
let status = frm.doc.twitter_post_id ? "Posted" : "Not Posted";
|
||||||
html += `<div class="col-xs-6">
|
html += `<div class="col-xs-6">
|
||||||
<span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span>
|
<span class="indicator whitespace-nowrap ${color}"><span>Twitter : ${status} </span></span>
|
||||||
</div>` ;
|
</div>` ;
|
||||||
}
|
}
|
||||||
if (frm.doc.linkedin){
|
if (frm.doc.linkedin) {
|
||||||
let color = frm.doc.linkedin_post_id ? "green" : "red";
|
let color = frm.doc.linkedin_post_id ? "green" : "red";
|
||||||
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
|
let status = frm.doc.linkedin_post_id ? "Posted" : "Not Posted";
|
||||||
html += `<div class="col-xs-6">
|
html += `<div class="col-xs-6">
|
||||||
@ -44,24 +122,18 @@ frappe.ui.form.on('Social Media Post', {
|
|||||||
frm.dashboard.set_headline_alert(html);
|
frm.dashboard.set_headline_alert(html);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
var add_post_btn = function(frm){
|
|
||||||
frm.add_custom_button(('Post Now'), function(){
|
|
||||||
post(frm);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
var post = function(frm){
|
|
||||||
frappe.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();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
add_post_btn: function(frm) {
|
||||||
|
frm.add_custom_button(__('Post Now'), function() {
|
||||||
|
frappe.call({
|
||||||
|
doc: frm.doc,
|
||||||
|
method: 'post',
|
||||||
|
freeze: true,
|
||||||
|
callback: function() {
|
||||||
|
frm.reload_doc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
@ -3,9 +3,11 @@
|
|||||||
"autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
|
"autoname": "format: CRM-SMP-{YYYY}-{MM}-{DD}-{###}",
|
||||||
"creation": "2020-01-30 11:53:13.872864",
|
"creation": "2020-01-30 11:53:13.872864",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"title",
|
||||||
"campaign_name",
|
"campaign_name",
|
||||||
"scheduled_time",
|
"scheduled_time",
|
||||||
"post_status",
|
"post_status",
|
||||||
@ -30,32 +32,24 @@
|
|||||||
"fieldname": "text",
|
"fieldname": "text",
|
||||||
"fieldtype": "Small Text",
|
"fieldtype": "Small Text",
|
||||||
"label": "Tweet",
|
"label": "Tweet",
|
||||||
"mandatory_depends_on": "eval:doc.twitter ==1",
|
"mandatory_depends_on": "eval:doc.twitter ==1"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "image",
|
"fieldname": "image",
|
||||||
"fieldtype": "Attach Image",
|
"fieldtype": "Attach Image",
|
||||||
"label": "Image",
|
"label": "Image"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "1",
|
||||||
"fieldname": "twitter",
|
"fieldname": "twitter",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Twitter",
|
"label": "Twitter"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "1",
|
||||||
"fieldname": "linkedin",
|
"fieldname": "linkedin",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "LinkedIn",
|
"label": "LinkedIn"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "amended_from",
|
"fieldname": "amended_from",
|
||||||
@ -64,27 +58,22 @@
|
|||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Social Media Post",
|
"options": "Social Media Post",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1,
|
"read_only": 1
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.twitter ==1",
|
"depends_on": "eval:doc.twitter ==1",
|
||||||
"fieldname": "content",
|
"fieldname": "content",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Twitter",
|
"label": "Twitter"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "post_status",
|
"fieldname": "post_status",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Post Status",
|
"label": "Post Status",
|
||||||
"options": "\nScheduled\nPosted\nError",
|
"no_copy": 1,
|
||||||
"read_only": 1,
|
"options": "\nScheduled\nPosted\nCancelled\nDeleted\nError",
|
||||||
"show_days": 1,
|
"read_only": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@ -92,9 +81,8 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "Twitter Post Id",
|
"label": "Twitter Post Id",
|
||||||
"read_only": 1,
|
"no_copy": 1,
|
||||||
"show_days": 1,
|
"read_only": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
@ -102,82 +90,69 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"label": "LinkedIn Post Id",
|
"label": "LinkedIn Post Id",
|
||||||
"read_only": 1,
|
"no_copy": 1,
|
||||||
"show_days": 1,
|
"read_only": 1
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "campaign_name",
|
"fieldname": "campaign_name",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Campaign",
|
"label": "Campaign",
|
||||||
"options": "Campaign",
|
"options": "Campaign"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_6",
|
"fieldname": "column_break_6",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break",
|
||||||
"label": "Share On",
|
"label": "Share On"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_14",
|
"fieldname": "column_break_14",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "tweet_preview",
|
"fieldname": "tweet_preview",
|
||||||
"fieldtype": "HTML",
|
"fieldtype": "HTML"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"depends_on": "eval:doc.linkedin==1",
|
"depends_on": "eval:doc.linkedin==1",
|
||||||
"fieldname": "linkedin_section",
|
"fieldname": "linkedin_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "LinkedIn",
|
"label": "LinkedIn"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"collapsible": 1,
|
"collapsible": 1,
|
||||||
"fieldname": "attachments_section",
|
"fieldname": "attachments_section",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"label": "Attachments",
|
"label": "Attachments"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "linkedin_post",
|
"fieldname": "linkedin_post",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Post",
|
"label": "Post",
|
||||||
"mandatory_depends_on": "eval:doc.linkedin ==1",
|
"mandatory_depends_on": "eval:doc.linkedin ==1"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_15",
|
"fieldname": "column_break_15",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"show_days": 1,
|
|
||||||
"show_seconds": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"fieldname": "scheduled_time",
|
"fieldname": "scheduled_time",
|
||||||
"fieldtype": "Datetime",
|
"fieldtype": "Datetime",
|
||||||
"label": "Scheduled Time",
|
"label": "Scheduled Time",
|
||||||
"read_only_depends_on": "eval:doc.post_status == \"Posted\"",
|
"read_only_depends_on": "eval:doc.post_status == \"Posted\""
|
||||||
"show_days": 1,
|
},
|
||||||
"show_seconds": 1
|
{
|
||||||
|
"fieldname": "title",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Title",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-14 10:31:33.961381",
|
"modified": "2021-04-14 14:24:59.821223",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Social Media Post",
|
"name": "Social Media Post",
|
||||||
@ -228,5 +203,6 @@
|
|||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"title_field": "title",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@ -10,17 +10,51 @@ import datetime
|
|||||||
|
|
||||||
class SocialMediaPost(Document):
|
class SocialMediaPost(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
if (not self.twitter and not self.linkedin):
|
||||||
|
frappe.throw(_("Select atleast one Social Media Platform to Share on."))
|
||||||
|
|
||||||
if self.scheduled_time:
|
if self.scheduled_time:
|
||||||
current_time = frappe.utils.now_datetime()
|
current_time = frappe.utils.now_datetime()
|
||||||
scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
|
scheduled_time = frappe.utils.get_datetime(self.scheduled_time)
|
||||||
if scheduled_time < current_time:
|
if scheduled_time < current_time:
|
||||||
frappe.throw(_("Invalid Scheduled Time"))
|
frappe.throw(_("Scheduled Time must be a future time."))
|
||||||
|
|
||||||
|
if self.text and len(self.text) > 280:
|
||||||
|
frappe.throw(_("Tweet length must be less than 280."))
|
||||||
|
|
||||||
def submit(self):
|
def submit(self):
|
||||||
if self.scheduled_time:
|
if self.scheduled_time:
|
||||||
self.post_status = "Scheduled"
|
self.post_status = "Scheduled"
|
||||||
super(SocialMediaPost, self).submit()
|
super(SocialMediaPost, self).submit()
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
self.db_set('post_status', 'Cancelled')
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def delete_post(self):
|
||||||
|
if self.twitter and self.twitter_post_id:
|
||||||
|
twitter = frappe.get_doc("Twitter Settings")
|
||||||
|
twitter.delete_tweet(self.twitter_post_id)
|
||||||
|
|
||||||
|
if self.linkedin and self.linkedin_post_id:
|
||||||
|
linkedin = frappe.get_doc("LinkedIn Settings")
|
||||||
|
linkedin.delete_post(self.linkedin_post_id)
|
||||||
|
|
||||||
|
self.db_set('post_status', 'Deleted')
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_post(self):
|
||||||
|
response = {}
|
||||||
|
if self.linkedin and self.linkedin_post_id:
|
||||||
|
linkedin = frappe.get_doc("LinkedIn Settings")
|
||||||
|
response['linkedin'] = linkedin.get_post(self.linkedin_post_id)
|
||||||
|
if self.twitter and self.twitter_post_id:
|
||||||
|
twitter = frappe.get_doc("Twitter Settings")
|
||||||
|
response['twitter'] = twitter.get_tweet(self.twitter_post_id)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def post(self):
|
def post(self):
|
||||||
try:
|
try:
|
||||||
if self.twitter and not self.twitter_post_id:
|
if self.twitter and not self.twitter_post_id:
|
||||||
@ -29,28 +63,22 @@ class SocialMediaPost(Document):
|
|||||||
self.db_set("twitter_post_id", twitter_post.id)
|
self.db_set("twitter_post_id", twitter_post.id)
|
||||||
if self.linkedin and not self.linkedin_post_id:
|
if self.linkedin and not self.linkedin_post_id:
|
||||||
linkedin = frappe.get_doc("LinkedIn Settings")
|
linkedin = frappe.get_doc("LinkedIn Settings")
|
||||||
linkedin_post = linkedin.post(self.linkedin_post, self.image)
|
linkedin_post = linkedin.post(self.linkedin_post, self.title, self.image)
|
||||||
self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'].split(":")[-1])
|
self.db_set("linkedin_post_id", linkedin_post.headers['X-RestLi-Id'])
|
||||||
self.db_set("post_status", "Posted")
|
self.db_set("post_status", "Posted")
|
||||||
|
|
||||||
except:
|
except:
|
||||||
self.db_set("post_status", "Error")
|
self.db_set("post_status", "Error")
|
||||||
title = _("Error while POSTING {0}").format(self.name)
|
title = _("Error while POSTING {0}").format(self.name)
|
||||||
traceback = frappe.get_traceback()
|
frappe.log_error(message=frappe.get_traceback(), title=title)
|
||||||
frappe.log_error(message=traceback , title=title)
|
|
||||||
|
|
||||||
def process_scheduled_social_media_posts():
|
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"])
|
posts = frappe.get_list("Social Media Post", filters={"post_status": "Scheduled", "docstatus":1}, fields= ["name", "scheduled_time"])
|
||||||
start = frappe.utils.now_datetime()
|
start = frappe.utils.now_datetime()
|
||||||
end = start + datetime.timedelta(minutes=10)
|
end = start + datetime.timedelta(minutes=10)
|
||||||
for post in posts:
|
for post in posts:
|
||||||
if post.scheduled_time:
|
if post.scheduled_time:
|
||||||
post_time = frappe.utils.get_datetime(post.scheduled_time)
|
post_time = frappe.utils.get_datetime(post.scheduled_time)
|
||||||
if post_time > start and post_time <= end:
|
if post_time > start and post_time <= end:
|
||||||
publish('Social Media Post', post.name)
|
sm_post = frappe.get_doc('Social Media Post', post.name)
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def publish(doctype, name):
|
|
||||||
sm_post = frappe.get_doc(doctype, name)
|
|
||||||
sm_post.post()
|
sm_post.post()
|
||||||
frappe.db.commit()
|
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
frappe.listview_settings['Social Media Post'] = {
|
frappe.listview_settings['Social Media Post'] = {
|
||||||
add_fields: ["status","post_status"],
|
add_fields: ["status", "post_status"],
|
||||||
get_indicator: function(doc) {
|
get_indicator: function(doc) {
|
||||||
return [__(doc.post_status), {
|
return [__(doc.post_status), {
|
||||||
"Scheduled": "orange",
|
"Scheduled": "orange",
|
||||||
"Posted": "green",
|
"Posted": "green",
|
||||||
"Error": "red"
|
"Error": "red",
|
||||||
|
"Deleted": "red"
|
||||||
}[doc.post_status]];
|
}[doc.post_status]];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
// For license information, please see license.txt
|
// For license information, please see license.txt
|
||||||
|
|
||||||
frappe.ui.form.on('Twitter Settings', {
|
frappe.ui.form.on('Twitter Settings', {
|
||||||
onload: function(frm){
|
onload: function(frm) {
|
||||||
if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
|
if (frm.doc.session_status == 'Expired' && frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||||
frappe.confirm(
|
frappe.confirm(
|
||||||
__('Session not valid, Do you want to login?'),
|
__('Session not valid, Do you want to login?'),
|
||||||
@ -14,10 +14,11 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
frm.dashboard.set_headline(__("For more information, {0}.", [`<a target='_blank' href='https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings'>${__('Click here')}</a>`]));
|
||||||
},
|
},
|
||||||
refresh: function(frm){
|
refresh: function(frm) {
|
||||||
let msg, color, flag=false;
|
let msg, color, flag=false;
|
||||||
if (frm.doc.session_status == "Active"){
|
if (frm.doc.session_status == "Active") {
|
||||||
msg = __("Session Active");
|
msg = __("Session Active");
|
||||||
color = 'green';
|
color = 'green';
|
||||||
flag = true;
|
flag = true;
|
||||||
@ -28,7 +29,7 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
flag = true;
|
flag = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (flag){
|
if (flag) {
|
||||||
frm.dashboard.set_headline_alert(
|
frm.dashboard.set_headline_alert(
|
||||||
`<div class="row">
|
`<div class="row">
|
||||||
<div class="col-xs-12">
|
<div class="col-xs-12">
|
||||||
@ -38,7 +39,7 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
login: function(frm){
|
login: function(frm) {
|
||||||
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
if (frm.doc.consumer_key && frm.doc.consumer_secret){
|
||||||
frappe.dom.freeze();
|
frappe.dom.freeze();
|
||||||
frappe.call({
|
frappe.call({
|
||||||
@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
after_save: function(frm){
|
after_save: function(frm) {
|
||||||
frm.trigger("login");
|
frm.trigger("login");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"creation": "2020-01-30 10:29:08.562108",
|
"creation": "2020-01-30 10:29:08.562108",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
|
"documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
@ -77,7 +78,7 @@
|
|||||||
"image_field": "profile_pic",
|
"image_field": "profile_pic",
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-13 17:50:47.934776",
|
"modified": "2021-02-18 15:18:07.900031",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Twitter Settings",
|
"name": "Twitter Settings",
|
||||||
|
|||||||
@ -32,7 +32,9 @@ class TwitterSettings(Document):
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
auth.get_access_token(oauth_verifier)
|
auth.get_access_token(oauth_verifier)
|
||||||
api = self.get_api(auth.access_token, auth.access_token_secret)
|
self.access_token = auth.access_token
|
||||||
|
self.access_token_secret = auth.access_token_secret
|
||||||
|
api = self.get_api()
|
||||||
user = api.me()
|
user = api.me()
|
||||||
profile_pic = (user._json["profile_image_url"]).replace("_normal","")
|
profile_pic = (user._json["profile_image_url"]).replace("_normal","")
|
||||||
|
|
||||||
@ -50,11 +52,11 @@ class TwitterSettings(Document):
|
|||||||
frappe.msgprint(_("Error! Failed to get access token."))
|
frappe.msgprint(_("Error! Failed to get access token."))
|
||||||
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
|
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
|
||||||
|
|
||||||
def get_api(self, access_token, access_token_secret):
|
def get_api(self):
|
||||||
# authentication of consumer key and secret
|
# authentication of consumer key and secret
|
||||||
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
|
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
|
||||||
# authentication of access token and secret
|
# authentication of access token and secret
|
||||||
auth.set_access_token(access_token, access_token_secret)
|
auth.set_access_token(self.access_token, self.access_token_secret)
|
||||||
|
|
||||||
return tweepy.API(auth)
|
return tweepy.API(auth)
|
||||||
|
|
||||||
@ -68,13 +70,13 @@ class TwitterSettings(Document):
|
|||||||
|
|
||||||
def upload_image(self, media):
|
def upload_image(self, media):
|
||||||
media = get_file_path(media)
|
media = get_file_path(media)
|
||||||
api = self.get_api(self.access_token, self.access_token_secret)
|
api = self.get_api()
|
||||||
media = api.media_upload(media)
|
media = api.media_upload(media)
|
||||||
|
|
||||||
return media.media_id
|
return media.media_id
|
||||||
|
|
||||||
def send_tweet(self, text, media_id=None):
|
def send_tweet(self, text, media_id=None):
|
||||||
api = self.get_api(self.access_token, self.access_token_secret)
|
api = self.get_api()
|
||||||
try:
|
try:
|
||||||
if media_id:
|
if media_id:
|
||||||
response = api.update_status(status = text, media_ids = [media_id])
|
response = api.update_status(status = text, media_ids = [media_id])
|
||||||
@ -84,12 +86,32 @@ class TwitterSettings(Document):
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
except TweepError as e:
|
except TweepError as e:
|
||||||
|
self.api_error(e)
|
||||||
|
|
||||||
|
def delete_tweet(self, tweet_id):
|
||||||
|
api = self.get_api()
|
||||||
|
try:
|
||||||
|
api.destroy_status(tweet_id)
|
||||||
|
except TweepError as e:
|
||||||
|
self.api_error(e)
|
||||||
|
|
||||||
|
def get_tweet(self, tweet_id):
|
||||||
|
api = self.get_api()
|
||||||
|
try:
|
||||||
|
response = api.get_status(tweet_id, trim_user=True, include_entities=True)
|
||||||
|
except TweepError as e:
|
||||||
|
self.api_error(e)
|
||||||
|
|
||||||
|
return response._json
|
||||||
|
|
||||||
|
def api_error(self, e):
|
||||||
content = json.loads(e.response.content)
|
content = json.loads(e.response.content)
|
||||||
content = content["errors"][0]
|
content = content["errors"][0]
|
||||||
if e.response.status_code == 401:
|
if e.response.status_code == 401:
|
||||||
self.db_set("session_status", "Expired")
|
self.db_set("session_status", "Expired")
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason))
|
frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def callback(oauth_token = None, oauth_verifier = None):
|
def callback(oauth_token = None, oauth_verifier = None):
|
||||||
|
|||||||
@ -355,7 +355,8 @@ scheduler_events = {
|
|||||||
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
|
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
|
||||||
"erpnext.controllers.accounts_controller.update_invoice_status",
|
"erpnext.controllers.accounts_controller.update_invoice_status",
|
||||||
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
|
"erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year",
|
||||||
"erpnext.hr.doctype.employee.employee.send_birthday_reminders",
|
"erpnext.hr.doctype.employee.employee_reminders.send_work_anniversary_reminders",
|
||||||
|
"erpnext.hr.doctype.employee.employee_reminders.send_birthday_reminders",
|
||||||
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
|
"erpnext.projects.doctype.task.task.set_tasks_as_overdue",
|
||||||
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
|
"erpnext.assets.doctype.asset.depreciation.post_depreciation_entries",
|
||||||
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary",
|
"erpnext.hr.doctype.daily_work_summary_group.daily_work_summary_group.send_summary",
|
||||||
@ -387,6 +388,12 @@ scheduler_events = {
|
|||||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
|
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
|
||||||
"erpnext.crm.doctype.lead.lead.daily_open_lead"
|
"erpnext.crm.doctype.lead.lead.daily_open_lead"
|
||||||
],
|
],
|
||||||
|
"weekly": [
|
||||||
|
"erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_weekly"
|
||||||
|
],
|
||||||
|
"monthly": [
|
||||||
|
"erpnext.hr.doctype.employee.employee_reminders.send_reminders_in_advance_monthly"
|
||||||
|
],
|
||||||
"monthly_long": [
|
"monthly_long": [
|
||||||
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
|
||||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
|
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
|
||||||
|
|||||||
@ -8,7 +8,7 @@ from frappe import _
|
|||||||
from frappe.utils import date_diff, add_days, getdate, cint, format_date
|
from frappe.utils import date_diff, add_days, getdate, cint, format_date
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
|
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
|
||||||
get_holidays_for_employee, create_additional_leave_ledger_entry
|
create_additional_leave_ledger_entry, get_holiday_dates_for_employee
|
||||||
|
|
||||||
class CompensatoryLeaveRequest(Document):
|
class CompensatoryLeaveRequest(Document):
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ class CompensatoryLeaveRequest(Document):
|
|||||||
frappe.throw(_("You are not present all day(s) between compensatory leave request days"))
|
frappe.throw(_("You are not present all day(s) between compensatory leave request days"))
|
||||||
|
|
||||||
def validate_holidays(self):
|
def validate_holidays(self):
|
||||||
holidays = get_holidays_for_employee(self.employee, self.work_from_date, self.work_end_date)
|
holidays = get_holiday_dates_for_employee(self.employee, self.work_from_date, self.work_end_date)
|
||||||
if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
|
if len(holidays) < date_diff(self.work_end_date, self.work_from_date) + 1:
|
||||||
if date_diff(self.work_end_date, self.work_from_date):
|
if date_diff(self.work_end_date, self.work_from_date):
|
||||||
msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))
|
msg = _("The days between {0} to {1} are not valid holidays.").format(frappe.bold(format_date(self.work_from_date)), frappe.bold(format_date(self.work_end_date)))
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from frappe.utils import getdate, validate_email_address, today, add_years, cstr
|
from frappe.utils import getdate, validate_email_address, today, add_years, cstr
|
||||||
@ -9,7 +7,6 @@ from frappe.model.naming import set_name_by_naming_series
|
|||||||
from frappe import throw, _, scrub
|
from frappe import throw, _, scrub
|
||||||
from frappe.permissions import add_user_permission, remove_user_permission, \
|
from frappe.permissions import add_user_permission, remove_user_permission, \
|
||||||
set_user_permission_if_allowed, has_permission, get_doc_permissions
|
set_user_permission_if_allowed, has_permission, get_doc_permissions
|
||||||
from frappe.model.document import Document
|
|
||||||
from erpnext.utilities.transaction_base import delete_events
|
from erpnext.utilities.transaction_base import delete_events
|
||||||
from frappe.utils.nestedset import NestedSet
|
from frappe.utils.nestedset import NestedSet
|
||||||
|
|
||||||
@ -286,94 +283,8 @@ def update_user_permissions(doc, method):
|
|||||||
employee = frappe.get_doc("Employee", {"user_id": doc.name})
|
employee = frappe.get_doc("Employee", {"user_id": doc.name})
|
||||||
employee.update_user_permissions()
|
employee.update_user_permissions()
|
||||||
|
|
||||||
def send_birthday_reminders():
|
|
||||||
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
|
|
||||||
if int(frappe.db.get_single_value("HR Settings", "stop_birthday_reminders") or 0):
|
|
||||||
return
|
|
||||||
|
|
||||||
employees_born_today = get_employees_who_are_born_today()
|
|
||||||
|
|
||||||
for company, birthday_persons in employees_born_today.items():
|
|
||||||
employee_emails = get_all_employee_emails(company)
|
|
||||||
birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons]
|
|
||||||
recipients = list(set(employee_emails) - set(birthday_person_emails))
|
|
||||||
|
|
||||||
reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons)
|
|
||||||
send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
|
|
||||||
|
|
||||||
if len(birthday_persons) > 1:
|
|
||||||
# special email for people sharing birthdays
|
|
||||||
for person in birthday_persons:
|
|
||||||
person_email = person["user_id"] or person["personal_email"] or person["company_email"]
|
|
||||||
others = [d for d in birthday_persons if d != person]
|
|
||||||
reminder_text, message = get_birthday_reminder_text_and_message(others)
|
|
||||||
send_birthday_reminder(person_email, reminder_text, others, message)
|
|
||||||
|
|
||||||
def get_employee_email(employee_doc):
|
def get_employee_email(employee_doc):
|
||||||
return employee_doc["user_id"] or employee_doc["personal_email"] or employee_doc["company_email"]
|
return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email")
|
||||||
|
|
||||||
def get_birthday_reminder_text_and_message(birthday_persons):
|
|
||||||
if len(birthday_persons) == 1:
|
|
||||||
birthday_person_text = birthday_persons[0]['name']
|
|
||||||
else:
|
|
||||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
|
||||||
person_names = [d['name'] for d in birthday_persons]
|
|
||||||
last_person = person_names[-1]
|
|
||||||
birthday_person_text = ", ".join(person_names[:-1])
|
|
||||||
birthday_person_text = _("{} & {}").format(birthday_person_text, last_person)
|
|
||||||
|
|
||||||
reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
|
|
||||||
message = _("A friendly reminder of an important date for our team.")
|
|
||||||
message += "<br>"
|
|
||||||
message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text)
|
|
||||||
|
|
||||||
return reminder_text, message
|
|
||||||
|
|
||||||
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
|
|
||||||
frappe.sendmail(
|
|
||||||
recipients=recipients,
|
|
||||||
subject=_("Birthday Reminder"),
|
|
||||||
template="birthday_reminder",
|
|
||||||
args=dict(
|
|
||||||
reminder_text=reminder_text,
|
|
||||||
birthday_persons=birthday_persons,
|
|
||||||
message=message,
|
|
||||||
),
|
|
||||||
header=_("Birthday Reminder 🎂")
|
|
||||||
)
|
|
||||||
|
|
||||||
def get_employees_who_are_born_today():
|
|
||||||
"""Get all employee born today & group them based on their company"""
|
|
||||||
from collections import defaultdict
|
|
||||||
employees_born_today = frappe.db.multisql({
|
|
||||||
"mariadb": """
|
|
||||||
SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`
|
|
||||||
FROM `tabEmployee`
|
|
||||||
WHERE
|
|
||||||
DAY(date_of_birth) = DAY(%(today)s)
|
|
||||||
AND
|
|
||||||
MONTH(date_of_birth) = MONTH(%(today)s)
|
|
||||||
AND
|
|
||||||
`status` = 'Active'
|
|
||||||
""",
|
|
||||||
"postgres": """
|
|
||||||
SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
|
|
||||||
FROM "tabEmployee"
|
|
||||||
WHERE
|
|
||||||
DATE_PART('day', "date_of_birth") = date_part('day', %(today)s)
|
|
||||||
AND
|
|
||||||
DATE_PART('month', "date_of_birth") = date_part('month', %(today)s)
|
|
||||||
AND
|
|
||||||
"status" = 'Active'
|
|
||||||
""",
|
|
||||||
}, dict(today=today()), as_dict=1)
|
|
||||||
|
|
||||||
grouped_employees = defaultdict(lambda: [])
|
|
||||||
|
|
||||||
for employee_doc in employees_born_today:
|
|
||||||
grouped_employees[employee_doc.get('company')].append(employee_doc)
|
|
||||||
|
|
||||||
return grouped_employees
|
|
||||||
|
|
||||||
def get_holiday_list_for_employee(employee, raise_exception=True):
|
def get_holiday_list_for_employee(employee, raise_exception=True):
|
||||||
if employee:
|
if employee:
|
||||||
@ -390,17 +301,40 @@ def get_holiday_list_for_employee(employee, raise_exception=True):
|
|||||||
|
|
||||||
return holiday_list
|
return holiday_list
|
||||||
|
|
||||||
def is_holiday(employee, date=None, raise_exception=True):
|
def is_holiday(employee, date=None, raise_exception=True, only_non_weekly=False, with_description=False):
|
||||||
'''Returns True if given Employee has an holiday on the given date
|
'''
|
||||||
|
Returns True if given Employee has an holiday on the given date
|
||||||
:param employee: Employee `name`
|
:param employee: Employee `name`
|
||||||
:param date: Date to check. Will check for today if None'''
|
:param date: Date to check. Will check for today if None
|
||||||
|
:param raise_exception: Raise an exception if no holiday list found, default is True
|
||||||
|
:param only_non_weekly: Check only non-weekly holidays, default is False
|
||||||
|
'''
|
||||||
|
|
||||||
holiday_list = get_holiday_list_for_employee(employee, raise_exception)
|
holiday_list = get_holiday_list_for_employee(employee, raise_exception)
|
||||||
if not date:
|
if not date:
|
||||||
date = today()
|
date = today()
|
||||||
|
|
||||||
if holiday_list:
|
if not holiday_list:
|
||||||
return frappe.get_all('Holiday List', dict(name=holiday_list, holiday_date=date)) and True or False
|
return False
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'parent': holiday_list,
|
||||||
|
'holiday_date': date
|
||||||
|
}
|
||||||
|
if only_non_weekly:
|
||||||
|
filters['weekly_off'] = False
|
||||||
|
|
||||||
|
holidays = frappe.get_all(
|
||||||
|
'Holiday',
|
||||||
|
fields=['description'],
|
||||||
|
filters=filters,
|
||||||
|
pluck='description'
|
||||||
|
)
|
||||||
|
|
||||||
|
if with_description:
|
||||||
|
return len(holidays) > 0, holidays
|
||||||
|
|
||||||
|
return len(holidays) > 0
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def deactivate_sales_person(status = None, employee = None):
|
def deactivate_sales_person(status = None, employee = None):
|
||||||
@ -503,7 +437,6 @@ def get_children(doctype, parent=None, company=None, is_root=False, is_tree=Fals
|
|||||||
|
|
||||||
return employees
|
return employees
|
||||||
|
|
||||||
|
|
||||||
def on_doctype_update():
|
def on_doctype_update():
|
||||||
frappe.db.add_index("Employee", ["lft", "rgt"])
|
frappe.db.add_index("Employee", ["lft", "rgt"])
|
||||||
|
|
||||||
|
|||||||
247
erpnext/hr/doctype/employee/employee_reminders.py
Normal file
247
erpnext/hr/doctype/employee/employee_reminders.py
Normal file
@ -0,0 +1,247 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.utils import comma_sep, getdate, today, add_months, add_days
|
||||||
|
from erpnext.hr.doctype.employee.employee import get_all_employee_emails, get_employee_email
|
||||||
|
from erpnext.hr.utils import get_holidays_for_employee
|
||||||
|
|
||||||
|
# -----------------
|
||||||
|
# HOLIDAY REMINDERS
|
||||||
|
# -----------------
|
||||||
|
def send_reminders_in_advance_weekly():
|
||||||
|
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
|
||||||
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
|
if not (to_send_in_advance and frequency == "Weekly"):
|
||||||
|
return
|
||||||
|
|
||||||
|
send_advance_holiday_reminders("Weekly")
|
||||||
|
|
||||||
|
def send_reminders_in_advance_monthly():
|
||||||
|
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
|
||||||
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
|
if not (to_send_in_advance and frequency == "Monthly"):
|
||||||
|
return
|
||||||
|
|
||||||
|
send_advance_holiday_reminders("Monthly")
|
||||||
|
|
||||||
|
def send_advance_holiday_reminders(frequency):
|
||||||
|
"""Send Holiday Reminders in Advance to Employees
|
||||||
|
`frequency` (str): 'Weekly' or 'Monthly'
|
||||||
|
"""
|
||||||
|
if frequency == "Weekly":
|
||||||
|
start_date = getdate()
|
||||||
|
end_date = add_days(getdate(), 7)
|
||||||
|
elif frequency == "Monthly":
|
||||||
|
# Sent on 1st of every month
|
||||||
|
start_date = getdate()
|
||||||
|
end_date = add_months(getdate(), 1)
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
employees = frappe.db.get_all('Employee', pluck='name')
|
||||||
|
for employee in employees:
|
||||||
|
holidays = get_holidays_for_employee(
|
||||||
|
employee,
|
||||||
|
start_date, end_date,
|
||||||
|
only_non_weekly=True,
|
||||||
|
raise_exception=False
|
||||||
|
)
|
||||||
|
|
||||||
|
if not (holidays is None):
|
||||||
|
send_holidays_reminder_in_advance(employee, holidays)
|
||||||
|
|
||||||
|
def send_holidays_reminder_in_advance(employee, holidays):
|
||||||
|
employee_doc = frappe.get_doc('Employee', employee)
|
||||||
|
employee_email = get_employee_email(employee_doc)
|
||||||
|
frequency = frappe.db.get_single_value("HR Settings", "frequency")
|
||||||
|
|
||||||
|
email_header = _("Holidays this Month.") if frequency == "Monthly" else _("Holidays this Week.")
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=[employee_email],
|
||||||
|
subject=_("Upcoming Holidays Reminder"),
|
||||||
|
template="holiday_reminder",
|
||||||
|
args=dict(
|
||||||
|
reminder_text=_("Hey {}! This email is to remind you about the upcoming holidays.").format(employee_doc.get('first_name')),
|
||||||
|
message=_("Below is the list of upcoming holidays for you:"),
|
||||||
|
advance_holiday_reminder=True,
|
||||||
|
holidays=holidays,
|
||||||
|
frequency=frequency[:-2]
|
||||||
|
),
|
||||||
|
header=email_header
|
||||||
|
)
|
||||||
|
|
||||||
|
# ------------------
|
||||||
|
# BIRTHDAY REMINDERS
|
||||||
|
# ------------------
|
||||||
|
def send_birthday_reminders():
|
||||||
|
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
|
||||||
|
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1)
|
||||||
|
if not to_send:
|
||||||
|
return
|
||||||
|
|
||||||
|
employees_born_today = get_employees_who_are_born_today()
|
||||||
|
|
||||||
|
for company, birthday_persons in employees_born_today.items():
|
||||||
|
employee_emails = get_all_employee_emails(company)
|
||||||
|
birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons]
|
||||||
|
recipients = list(set(employee_emails) - set(birthday_person_emails))
|
||||||
|
|
||||||
|
reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons)
|
||||||
|
send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
|
||||||
|
|
||||||
|
if len(birthday_persons) > 1:
|
||||||
|
# special email for people sharing birthdays
|
||||||
|
for person in birthday_persons:
|
||||||
|
person_email = person["user_id"] or person["personal_email"] or person["company_email"]
|
||||||
|
others = [d for d in birthday_persons if d != person]
|
||||||
|
reminder_text, message = get_birthday_reminder_text_and_message(others)
|
||||||
|
send_birthday_reminder(person_email, reminder_text, others, message)
|
||||||
|
|
||||||
|
def get_birthday_reminder_text_and_message(birthday_persons):
|
||||||
|
if len(birthday_persons) == 1:
|
||||||
|
birthday_person_text = birthday_persons[0]['name']
|
||||||
|
else:
|
||||||
|
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
||||||
|
person_names = [d['name'] for d in birthday_persons]
|
||||||
|
birthday_person_text = comma_sep(person_names, frappe._("{0} & {1}"), False)
|
||||||
|
|
||||||
|
reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
|
||||||
|
message = _("A friendly reminder of an important date for our team.")
|
||||||
|
message += "<br>"
|
||||||
|
message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text)
|
||||||
|
|
||||||
|
return reminder_text, message
|
||||||
|
|
||||||
|
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=recipients,
|
||||||
|
subject=_("Birthday Reminder"),
|
||||||
|
template="birthday_reminder",
|
||||||
|
args=dict(
|
||||||
|
reminder_text=reminder_text,
|
||||||
|
birthday_persons=birthday_persons,
|
||||||
|
message=message,
|
||||||
|
),
|
||||||
|
header=_("Birthday Reminder 🎂")
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_employees_who_are_born_today():
|
||||||
|
"""Get all employee born today & group them based on their company"""
|
||||||
|
return get_employees_having_an_event_today("birthday")
|
||||||
|
|
||||||
|
def get_employees_having_an_event_today(event_type):
|
||||||
|
"""Get all employee who have `event_type` today
|
||||||
|
& group them based on their company. `event_type`
|
||||||
|
can be `birthday` or `work_anniversary`"""
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# Set column based on event type
|
||||||
|
if event_type == 'birthday':
|
||||||
|
condition_column = 'date_of_birth'
|
||||||
|
elif event_type == 'work_anniversary':
|
||||||
|
condition_column = 'date_of_joining'
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
employees_born_today = frappe.db.multisql({
|
||||||
|
"mariadb": f"""
|
||||||
|
SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`, `date_of_joining`
|
||||||
|
FROM `tabEmployee`
|
||||||
|
WHERE
|
||||||
|
DAY({condition_column}) = DAY(%(today)s)
|
||||||
|
AND
|
||||||
|
MONTH({condition_column}) = MONTH(%(today)s)
|
||||||
|
AND
|
||||||
|
`status` = 'Active'
|
||||||
|
""",
|
||||||
|
"postgres": f"""
|
||||||
|
SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
|
||||||
|
FROM "tabEmployee"
|
||||||
|
WHERE
|
||||||
|
DATE_PART('day', {condition_column}) = date_part('day', %(today)s)
|
||||||
|
AND
|
||||||
|
DATE_PART('month', {condition_column}) = date_part('month', %(today)s)
|
||||||
|
AND
|
||||||
|
"status" = 'Active'
|
||||||
|
""",
|
||||||
|
}, dict(today=today(), condition_column=condition_column), as_dict=1)
|
||||||
|
|
||||||
|
grouped_employees = defaultdict(lambda: [])
|
||||||
|
|
||||||
|
for employee_doc in employees_born_today:
|
||||||
|
grouped_employees[employee_doc.get('company')].append(employee_doc)
|
||||||
|
|
||||||
|
return grouped_employees
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------
|
||||||
|
# WORK ANNIVERSARY REMINDERS
|
||||||
|
# --------------------------
|
||||||
|
def send_work_anniversary_reminders():
|
||||||
|
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
|
||||||
|
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1)
|
||||||
|
if not to_send:
|
||||||
|
return
|
||||||
|
|
||||||
|
employees_joined_today = get_employees_having_an_event_today("work_anniversary")
|
||||||
|
|
||||||
|
for company, anniversary_persons in employees_joined_today.items():
|
||||||
|
employee_emails = get_all_employee_emails(company)
|
||||||
|
anniversary_person_emails = [get_employee_email(doc) for doc in anniversary_persons]
|
||||||
|
recipients = list(set(employee_emails) - set(anniversary_person_emails))
|
||||||
|
|
||||||
|
reminder_text, message = get_work_anniversary_reminder_text_and_message(anniversary_persons)
|
||||||
|
send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message)
|
||||||
|
|
||||||
|
if len(anniversary_persons) > 1:
|
||||||
|
# email for people sharing work anniversaries
|
||||||
|
for person in anniversary_persons:
|
||||||
|
person_email = person["user_id"] or person["personal_email"] or person["company_email"]
|
||||||
|
others = [d for d in anniversary_persons if d != person]
|
||||||
|
reminder_text, message = get_work_anniversary_reminder_text_and_message(others)
|
||||||
|
send_work_anniversary_reminder(person_email, reminder_text, others, message)
|
||||||
|
|
||||||
|
def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||||
|
if len(anniversary_persons) == 1:
|
||||||
|
anniversary_person = anniversary_persons[0]['name']
|
||||||
|
persons_name = anniversary_person
|
||||||
|
# Number of years completed at the company
|
||||||
|
completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
|
||||||
|
anniversary_person += f" completed {completed_years} years"
|
||||||
|
else:
|
||||||
|
person_names_with_years = []
|
||||||
|
names = []
|
||||||
|
for person in anniversary_persons:
|
||||||
|
person_text = person['name']
|
||||||
|
names.append(person_text)
|
||||||
|
# Number of years completed at the company
|
||||||
|
completed_years = getdate().year - person['date_of_joining'].year
|
||||||
|
person_text += f" completed {completed_years} years"
|
||||||
|
person_names_with_years.append(person_text)
|
||||||
|
|
||||||
|
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
||||||
|
anniversary_person = comma_sep(person_names_with_years, frappe._("{0} & {1}"), False)
|
||||||
|
persons_name = comma_sep(names, frappe._("{0} & {1}"), False)
|
||||||
|
|
||||||
|
reminder_text = _("Today {0} at our Company! 🎉").format(anniversary_person)
|
||||||
|
message = _("A friendly reminder of an important date for our team.")
|
||||||
|
message += "<br>"
|
||||||
|
message += _("Everyone, let’s congratulate {0} on their work anniversary!").format(persons_name)
|
||||||
|
|
||||||
|
return reminder_text, message
|
||||||
|
|
||||||
|
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=recipients,
|
||||||
|
subject=_("Work Anniversary Reminder"),
|
||||||
|
template="anniversary_reminder",
|
||||||
|
args=dict(
|
||||||
|
reminder_text=reminder_text,
|
||||||
|
anniversary_persons=anniversary_persons,
|
||||||
|
message=message,
|
||||||
|
),
|
||||||
|
header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️")
|
||||||
|
)
|
||||||
@ -1,7 +1,5 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import erpnext
|
import erpnext
|
||||||
@ -12,29 +10,6 @@ from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
|
|||||||
test_records = frappe.get_test_records('Employee')
|
test_records = frappe.get_test_records('Employee')
|
||||||
|
|
||||||
class TestEmployee(unittest.TestCase):
|
class TestEmployee(unittest.TestCase):
|
||||||
def test_birthday_reminders(self):
|
|
||||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
|
||||||
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
|
|
||||||
employee.company_email = "test@example.com"
|
|
||||||
employee.company = "_Test Company"
|
|
||||||
employee.save()
|
|
||||||
|
|
||||||
from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders
|
|
||||||
|
|
||||||
employees_born_today = get_employees_who_are_born_today()
|
|
||||||
self.assertTrue(employees_born_today.get("_Test Company"))
|
|
||||||
|
|
||||||
frappe.db.sql("delete from `tabEmail Queue`")
|
|
||||||
|
|
||||||
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
|
||||||
hr_settings.stop_birthday_reminders = 0
|
|
||||||
hr_settings.save()
|
|
||||||
|
|
||||||
send_birthday_reminders()
|
|
||||||
|
|
||||||
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
|
||||||
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
|
|
||||||
|
|
||||||
def test_employee_status_left(self):
|
def test_employee_status_left(self):
|
||||||
employee1 = make_employee("test_employee_1@company.com")
|
employee1 = make_employee("test_employee_1@company.com")
|
||||||
employee2 = make_employee("test_employee_2@company.com")
|
employee2 = make_employee("test_employee_2@company.com")
|
||||||
|
|||||||
173
erpnext/hr/doctype/employee/test_employee_reminders.py
Normal file
173
erpnext/hr/doctype/employee/test_employee_reminders.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from frappe.utils import getdate
|
||||||
|
from datetime import timedelta
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
|
from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change
|
||||||
|
|
||||||
|
|
||||||
|
class TestEmployeeReminders(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
from erpnext.hr.doctype.holiday_list.test_holiday_list import make_holiday_list
|
||||||
|
|
||||||
|
# Create a test holiday list
|
||||||
|
test_holiday_dates = cls.get_test_holiday_dates()
|
||||||
|
test_holiday_list = make_holiday_list(
|
||||||
|
'TestHolidayRemindersList',
|
||||||
|
holiday_dates=[
|
||||||
|
{'holiday_date': test_holiday_dates[0], 'description': 'test holiday1'},
|
||||||
|
{'holiday_date': test_holiday_dates[1], 'description': 'test holiday2'},
|
||||||
|
{'holiday_date': test_holiday_dates[2], 'description': 'test holiday3', 'weekly_off': 1},
|
||||||
|
{'holiday_date': test_holiday_dates[3], 'description': 'test holiday4'},
|
||||||
|
{'holiday_date': test_holiday_dates[4], 'description': 'test holiday5'},
|
||||||
|
{'holiday_date': test_holiday_dates[5], 'description': 'test holiday6'},
|
||||||
|
],
|
||||||
|
from_date=getdate()-timedelta(days=10),
|
||||||
|
to_date=getdate()+timedelta(weeks=5)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create a test employee
|
||||||
|
test_employee = frappe.get_doc(
|
||||||
|
'Employee',
|
||||||
|
make_employee('test@gopher.io', company="_Test Company")
|
||||||
|
)
|
||||||
|
|
||||||
|
# Attach the holiday list to employee
|
||||||
|
test_employee.holiday_list = test_holiday_list.name
|
||||||
|
test_employee.save()
|
||||||
|
|
||||||
|
# Attach to class
|
||||||
|
cls.test_employee = test_employee
|
||||||
|
cls.test_holiday_dates = test_holiday_dates
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_test_holiday_dates(cls):
|
||||||
|
today_date = getdate()
|
||||||
|
return [
|
||||||
|
today_date,
|
||||||
|
today_date-timedelta(days=4),
|
||||||
|
today_date-timedelta(days=3),
|
||||||
|
today_date+timedelta(days=1),
|
||||||
|
today_date+timedelta(days=3),
|
||||||
|
today_date+timedelta(weeks=3)
|
||||||
|
]
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Clear Email Queue
|
||||||
|
frappe.db.sql("delete from `tabEmail Queue`")
|
||||||
|
|
||||||
|
def test_is_holiday(self):
|
||||||
|
from erpnext.hr.doctype.employee.employee import is_holiday
|
||||||
|
|
||||||
|
self.assertTrue(is_holiday(self.test_employee.name))
|
||||||
|
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[1]))
|
||||||
|
self.assertFalse(is_holiday(self.test_employee.name, date=getdate()-timedelta(days=1)))
|
||||||
|
|
||||||
|
# Test weekly_off holidays
|
||||||
|
self.assertTrue(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2]))
|
||||||
|
self.assertFalse(is_holiday(self.test_employee.name, date=self.test_holiday_dates[2], only_non_weekly=True))
|
||||||
|
|
||||||
|
# Test with descriptions
|
||||||
|
has_holiday, descriptions = is_holiday(self.test_employee.name, with_description=True)
|
||||||
|
self.assertTrue(has_holiday)
|
||||||
|
self.assertTrue('test holiday1' in descriptions)
|
||||||
|
|
||||||
|
def test_birthday_reminders(self):
|
||||||
|
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||||
|
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
|
||||||
|
employee.company_email = "test@example.com"
|
||||||
|
employee.company = "_Test Company"
|
||||||
|
employee.save()
|
||||||
|
|
||||||
|
from erpnext.hr.doctype.employee.employee_reminders import get_employees_who_are_born_today, send_birthday_reminders
|
||||||
|
|
||||||
|
employees_born_today = get_employees_who_are_born_today()
|
||||||
|
self.assertTrue(employees_born_today.get("_Test Company"))
|
||||||
|
|
||||||
|
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
||||||
|
hr_settings.send_birthday_reminders = 1
|
||||||
|
hr_settings.save()
|
||||||
|
|
||||||
|
send_birthday_reminders()
|
||||||
|
|
||||||
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
|
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
|
||||||
|
|
||||||
|
def test_work_anniversary_reminders(self):
|
||||||
|
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
||||||
|
employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:]
|
||||||
|
employee.company_email = "test@example.com"
|
||||||
|
employee.company = "_Test Company"
|
||||||
|
employee.save()
|
||||||
|
|
||||||
|
from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today, send_work_anniversary_reminders
|
||||||
|
|
||||||
|
employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
|
||||||
|
self.assertTrue(employees_having_work_anniversary.get("_Test Company"))
|
||||||
|
|
||||||
|
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
||||||
|
hr_settings.send_work_anniversary_reminders = 1
|
||||||
|
hr_settings.save()
|
||||||
|
|
||||||
|
send_work_anniversary_reminders()
|
||||||
|
|
||||||
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
|
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
|
||||||
|
|
||||||
|
def test_send_holidays_reminder_in_advance(self):
|
||||||
|
from erpnext.hr.utils import get_holidays_for_employee
|
||||||
|
from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
|
||||||
|
|
||||||
|
# Get HR settings and enable advance holiday reminders
|
||||||
|
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
||||||
|
hr_settings.send_holiday_reminders = 1
|
||||||
|
set_proceed_with_frequency_change()
|
||||||
|
hr_settings.frequency = 'Weekly'
|
||||||
|
hr_settings.save()
|
||||||
|
|
||||||
|
holidays = get_holidays_for_employee(
|
||||||
|
self.test_employee.get('name'),
|
||||||
|
getdate(), getdate() + timedelta(days=3),
|
||||||
|
only_non_weekly=True,
|
||||||
|
raise_exception=False
|
||||||
|
)
|
||||||
|
|
||||||
|
send_holidays_reminder_in_advance(
|
||||||
|
self.test_employee.get('name'),
|
||||||
|
holidays
|
||||||
|
)
|
||||||
|
|
||||||
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
|
self.assertEqual(len(email_queue), 1)
|
||||||
|
|
||||||
|
def test_advance_holiday_reminders_monthly(self):
|
||||||
|
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
|
||||||
|
# Get HR settings and enable advance holiday reminders
|
||||||
|
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
||||||
|
hr_settings.send_holiday_reminders = 1
|
||||||
|
set_proceed_with_frequency_change()
|
||||||
|
hr_settings.frequency = 'Monthly'
|
||||||
|
hr_settings.save()
|
||||||
|
|
||||||
|
send_reminders_in_advance_monthly()
|
||||||
|
|
||||||
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
|
self.assertTrue(len(email_queue) > 0)
|
||||||
|
|
||||||
|
def test_advance_holiday_reminders_weekly(self):
|
||||||
|
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
|
||||||
|
# Get HR settings and enable advance holiday reminders
|
||||||
|
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
|
||||||
|
hr_settings.send_holiday_reminders = 1
|
||||||
|
hr_settings.frequency = 'Weekly'
|
||||||
|
hr_settings.save()
|
||||||
|
|
||||||
|
send_reminders_in_advance_weekly()
|
||||||
|
|
||||||
|
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
|
||||||
|
self.assertTrue(len(email_queue) > 0)
|
||||||
@ -11,8 +11,14 @@
|
|||||||
"emp_created_by",
|
"emp_created_by",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"standard_working_hours",
|
"standard_working_hours",
|
||||||
"stop_birthday_reminders",
|
|
||||||
"expense_approver_mandatory_in_expense_claim",
|
"expense_approver_mandatory_in_expense_claim",
|
||||||
|
"reminders_section",
|
||||||
|
"send_birthday_reminders",
|
||||||
|
"column_break_9",
|
||||||
|
"send_work_anniversary_reminders",
|
||||||
|
"column_break_11",
|
||||||
|
"send_holiday_reminders",
|
||||||
|
"frequency",
|
||||||
"leave_settings",
|
"leave_settings",
|
||||||
"send_leave_notification",
|
"send_leave_notification",
|
||||||
"leave_approval_notification_template",
|
"leave_approval_notification_template",
|
||||||
@ -50,13 +56,6 @@
|
|||||||
"fieldname": "column_break_4",
|
"fieldname": "column_break_4",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"description": "Don't send employee birthday reminders",
|
|
||||||
"fieldname": "stop_birthday_reminders",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Stop Birthday Reminders"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "expense_approver_mandatory_in_expense_claim",
|
"fieldname": "expense_approver_mandatory_in_expense_claim",
|
||||||
@ -142,13 +141,53 @@
|
|||||||
"fieldname": "standard_working_hours",
|
"fieldname": "standard_working_hours",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"label": "Standard Working Hours"
|
"label": "Standard Working Hours"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"collapsible": 1,
|
||||||
|
"fieldname": "reminders_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Reminders"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "send_holiday_reminders",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Holidays"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "send_work_anniversary_reminders",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Work Anniversaries "
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Weekly",
|
||||||
|
"depends_on": "eval:doc.send_holiday_reminders",
|
||||||
|
"fieldname": "frequency",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Set the frequency for holiday reminders",
|
||||||
|
"options": "Weekly\nMonthly"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "send_birthday_reminders",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Birthdays"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_9",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_11",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-11 10:52:56.192773",
|
"modified": "2021-08-24 14:54:12.834162",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR Settings",
|
"name": "HR Settings",
|
||||||
|
|||||||
@ -1,17 +1,79 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import format_date
|
||||||
|
|
||||||
|
# Wether to proceed with frequency change
|
||||||
|
PROCEED_WITH_FREQUENCY_CHANGE = False
|
||||||
|
|
||||||
class HRSettings(Document):
|
class HRSettings(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.set_naming_series()
|
self.set_naming_series()
|
||||||
|
|
||||||
|
# Based on proceed flag
|
||||||
|
global PROCEED_WITH_FREQUENCY_CHANGE
|
||||||
|
if not PROCEED_WITH_FREQUENCY_CHANGE:
|
||||||
|
self.validate_frequency_change()
|
||||||
|
PROCEED_WITH_FREQUENCY_CHANGE = False
|
||||||
|
|
||||||
def set_naming_series(self):
|
def set_naming_series(self):
|
||||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||||
set_by_naming_series("Employee", "employee_number",
|
set_by_naming_series("Employee", "employee_number",
|
||||||
self.get("emp_created_by")=="Naming Series", hide_name_field=True)
|
self.get("emp_created_by")=="Naming Series", hide_name_field=True)
|
||||||
|
|
||||||
|
def validate_frequency_change(self):
|
||||||
|
weekly_job, monthly_job = None, None
|
||||||
|
|
||||||
|
try:
|
||||||
|
weekly_job = frappe.get_doc(
|
||||||
|
'Scheduled Job Type',
|
||||||
|
'employee_reminders.send_reminders_in_advance_weekly'
|
||||||
|
)
|
||||||
|
|
||||||
|
monthly_job = frappe.get_doc(
|
||||||
|
'Scheduled Job Type',
|
||||||
|
'employee_reminders.send_reminders_in_advance_monthly'
|
||||||
|
)
|
||||||
|
except frappe.DoesNotExistError:
|
||||||
|
return
|
||||||
|
|
||||||
|
next_weekly_trigger = weekly_job.get_next_execution()
|
||||||
|
next_monthly_trigger = monthly_job.get_next_execution()
|
||||||
|
|
||||||
|
if self.freq_changed_from_monthly_to_weekly():
|
||||||
|
if next_monthly_trigger < next_weekly_trigger:
|
||||||
|
self.show_freq_change_warning(next_monthly_trigger, next_weekly_trigger)
|
||||||
|
|
||||||
|
elif self.freq_changed_from_weekly_to_monthly():
|
||||||
|
if next_monthly_trigger > next_weekly_trigger:
|
||||||
|
self.show_freq_change_warning(next_weekly_trigger, next_monthly_trigger)
|
||||||
|
|
||||||
|
def freq_changed_from_weekly_to_monthly(self):
|
||||||
|
return self.has_value_changed("frequency") and self.frequency == "Monthly"
|
||||||
|
|
||||||
|
def freq_changed_from_monthly_to_weekly(self):
|
||||||
|
return self.has_value_changed("frequency") and self.frequency == "Weekly"
|
||||||
|
|
||||||
|
def show_freq_change_warning(self, from_date, to_date):
|
||||||
|
from_date = frappe.bold(format_date(from_date))
|
||||||
|
to_date = frappe.bold(format_date(to_date))
|
||||||
|
frappe.msgprint(
|
||||||
|
msg=frappe._('Employees will miss holiday reminders from {} until {}. <br> Do you want to proceed with this change?').format(from_date, to_date),
|
||||||
|
title='Confirm change in Frequency',
|
||||||
|
primary_action={
|
||||||
|
'label': frappe._('Yes, Proceed'),
|
||||||
|
'client_action': 'erpnext.proceed_save_with_reminders_frequency_change'
|
||||||
|
},
|
||||||
|
raise_exception=frappe.ValidationError
|
||||||
|
)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def set_proceed_with_frequency_change():
|
||||||
|
'''Enables proceed with frequency change'''
|
||||||
|
global PROCEED_WITH_FREQUENCY_CHANGE
|
||||||
|
PROCEED_WITH_FREQUENCY_CHANGE = True
|
||||||
|
|||||||
@ -10,7 +10,7 @@ from frappe import _
|
|||||||
from frappe.utils.csvutils import UnicodeWriter
|
from frappe.utils.csvutils import UnicodeWriter
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||||
from erpnext.hr.utils import get_holidays_for_employee
|
from erpnext.hr.utils import get_holiday_dates_for_employee
|
||||||
|
|
||||||
class UploadAttendance(Document):
|
class UploadAttendance(Document):
|
||||||
pass
|
pass
|
||||||
@ -94,7 +94,7 @@ def get_holidays_for_employees(employees, from_date, to_date):
|
|||||||
holidays = {}
|
holidays = {}
|
||||||
for employee in employees:
|
for employee in employees:
|
||||||
holiday_list = get_holiday_list_for_employee(employee)
|
holiday_list = get_holiday_list_for_employee(employee)
|
||||||
holiday = get_holidays_for_employee(employee, getdate(from_date), getdate(to_date))
|
holiday = get_holiday_dates_for_employee(employee, getdate(from_date), getdate(to_date))
|
||||||
if holiday_list not in holidays:
|
if holiday_list not in holidays:
|
||||||
holidays[holiday_list] = holiday
|
holidays[holiday_list] = holiday
|
||||||
|
|
||||||
|
|||||||
@ -335,20 +335,43 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
|
|||||||
total_given_benefit_amount = sum_of_given_benefit[0].total_amount
|
total_given_benefit_amount = sum_of_given_benefit[0].total_amount
|
||||||
return total_given_benefit_amount
|
return total_given_benefit_amount
|
||||||
|
|
||||||
def get_holidays_for_employee(employee, start_date, end_date):
|
def get_holiday_dates_for_employee(employee, start_date, end_date):
|
||||||
holiday_list = get_holiday_list_for_employee(employee)
|
"""return a list of holiday dates for the given employee between start_date and end_date"""
|
||||||
|
# return only date
|
||||||
|
holidays = get_holidays_for_employee(employee, start_date, end_date)
|
||||||
|
|
||||||
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
|
return [cstr(h.holiday_date) for h in holidays]
|
||||||
where
|
|
||||||
parent=%(holiday_list)s
|
|
||||||
and holiday_date >= %(start_date)s
|
|
||||||
and holiday_date <= %(end_date)s''', {
|
|
||||||
"holiday_list": holiday_list,
|
|
||||||
"start_date": start_date,
|
|
||||||
"end_date": end_date
|
|
||||||
})
|
|
||||||
|
|
||||||
holidays = [cstr(i) for i in holidays]
|
|
||||||
|
def get_holidays_for_employee(employee, start_date, end_date, raise_exception=True, only_non_weekly=False):
|
||||||
|
"""Get Holidays for a given employee
|
||||||
|
|
||||||
|
`employee` (str)
|
||||||
|
`start_date` (str or datetime)
|
||||||
|
`end_date` (str or datetime)
|
||||||
|
`raise_exception` (bool)
|
||||||
|
`only_non_weekly` (bool)
|
||||||
|
|
||||||
|
return: list of dicts with `holiday_date` and `description`
|
||||||
|
"""
|
||||||
|
holiday_list = get_holiday_list_for_employee(employee, raise_exception=raise_exception)
|
||||||
|
|
||||||
|
if not holiday_list:
|
||||||
|
return []
|
||||||
|
|
||||||
|
filters = {
|
||||||
|
'parent': holiday_list,
|
||||||
|
'holiday_date': ('between', [start_date, end_date])
|
||||||
|
}
|
||||||
|
|
||||||
|
if only_non_weekly:
|
||||||
|
filters['weekly_off'] = False
|
||||||
|
|
||||||
|
holidays = frappe.get_all(
|
||||||
|
'Holiday',
|
||||||
|
fields=['description', 'holiday_date'],
|
||||||
|
filters=filters
|
||||||
|
)
|
||||||
|
|
||||||
return holidays
|
return holidays
|
||||||
|
|
||||||
|
|||||||
@ -275,6 +275,7 @@ erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
|
|||||||
erpnext.patches.v13_0.germany_make_custom_fields
|
erpnext.patches.v13_0.germany_make_custom_fields
|
||||||
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
|
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
|
||||||
erpnext.patches.v13_0.set_pos_closing_as_failed
|
erpnext.patches.v13_0.set_pos_closing_as_failed
|
||||||
|
erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders
|
||||||
execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
|
execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
|
||||||
erpnext.patches.v13_0.update_timesheet_changes
|
erpnext.patches.v13_0.update_timesheet_changes
|
||||||
erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
|
erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
|
||||||
|
|||||||
@ -0,0 +1,23 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.model.utils.rename_field import rename_field
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc('hr', 'doctype', 'hr_settings')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Rename the field
|
||||||
|
rename_field('HR Settings', 'stop_birthday_reminders', 'send_birthday_reminders')
|
||||||
|
|
||||||
|
# Reverse the value
|
||||||
|
old_value = frappe.db.get_single_value('HR Settings', 'send_birthday_reminders')
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
'HR Settings',
|
||||||
|
'HR Settings',
|
||||||
|
'send_birthday_reminders',
|
||||||
|
1 if old_value == 0 else 0
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
if e.args[0] != 1054:
|
||||||
|
raise
|
||||||
@ -9,7 +9,7 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor
|
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor
|
||||||
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
|
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
|
||||||
from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee
|
from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holiday_dates_for_employee, get_previous_claimed_amount, validate_active_employee
|
||||||
|
|
||||||
class EmployeeBenefitApplication(Document):
|
class EmployeeBenefitApplication(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -139,7 +139,7 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
|
|||||||
# Then the sum multiply with the no of lwp in that period
|
# Then the sum multiply with the no of lwp in that period
|
||||||
# Include that amount to the prev_sal_slip_flexi_total to get the actual
|
# Include that amount to the prev_sal_slip_flexi_total to get the actual
|
||||||
if have_depends_on_payment_days and per_day_amount_total > 0:
|
if have_depends_on_payment_days and per_day_amount_total > 0:
|
||||||
holidays = get_holidays_for_employee(employee, payroll_period_obj.start_date, on_date)
|
holidays = get_holiday_dates_for_employee(employee, payroll_period_obj.start_date, on_date)
|
||||||
working_days = date_diff(on_date, payroll_period_obj.start_date) + 1
|
working_days = date_diff(on_date, payroll_period_obj.start_date) + 1
|
||||||
leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days)
|
leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days)
|
||||||
leave_days_amount = leave_days * per_day_amount_total
|
leave_days_amount = leave_days * per_day_amount_total
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
|
from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from erpnext.hr.utils import get_holidays_for_employee
|
from erpnext.hr.utils import get_holiday_dates_for_employee
|
||||||
|
|
||||||
class PayrollPeriod(Document):
|
class PayrollPeriod(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -65,7 +65,7 @@ def get_payroll_period_days(start_date, end_date, employee, company=None):
|
|||||||
actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1
|
actual_no_of_days = date_diff(getdate(payroll_period[0][2]), getdate(payroll_period[0][1])) + 1
|
||||||
working_days = actual_no_of_days
|
working_days = actual_no_of_days
|
||||||
if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")):
|
if not cint(frappe.db.get_value("Payroll Settings", None, "include_holidays_in_total_working_days")):
|
||||||
holidays = get_holidays_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2]))
|
holidays = get_holiday_dates_for_employee(employee, getdate(payroll_period[0][1]), getdate(payroll_period[0][2]))
|
||||||
working_days -= len(holidays)
|
working_days -= len(holidays)
|
||||||
return payroll_period[0][0], working_days, actual_no_of_days
|
return payroll_period[0][0], working_days, actual_no_of_days
|
||||||
return False, False, False
|
return False, False, False
|
||||||
|
|||||||
@ -11,6 +11,7 @@ from frappe.model.naming import make_autoname
|
|||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
|
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
|
||||||
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||||
|
from erpnext.hr.utils import get_holiday_dates_for_employee
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
from frappe.utils.background_jobs import enqueue
|
from frappe.utils.background_jobs import enqueue
|
||||||
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
||||||
@ -337,20 +338,7 @@ class SalarySlip(TransactionBase):
|
|||||||
return payment_days
|
return payment_days
|
||||||
|
|
||||||
def get_holidays_for_employee(self, start_date, end_date):
|
def get_holidays_for_employee(self, start_date, end_date):
|
||||||
holiday_list = get_holiday_list_for_employee(self.employee)
|
return get_holiday_dates_for_employee(self.employee, start_date, end_date)
|
||||||
holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday`
|
|
||||||
where
|
|
||||||
parent=%(holiday_list)s
|
|
||||||
and holiday_date >= %(start_date)s
|
|
||||||
and holiday_date <= %(end_date)s''', {
|
|
||||||
"holiday_list": holiday_list,
|
|
||||||
"start_date": start_date,
|
|
||||||
"end_date": end_date
|
|
||||||
})
|
|
||||||
|
|
||||||
holidays = [cstr(i) for i in holidays]
|
|
||||||
|
|
||||||
return holidays
|
|
||||||
|
|
||||||
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
|
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
|
||||||
lwp = 0
|
lwp = 0
|
||||||
|
|||||||
@ -82,6 +82,17 @@ $.extend(erpnext, {
|
|||||||
});
|
});
|
||||||
frappe.set_route('Form','Journal Entry', journal_entry.name);
|
frappe.set_route('Form','Journal Entry', journal_entry.name);
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
proceed_save_with_reminders_frequency_change: () => {
|
||||||
|
frappe.ui.hide_open_dialog();
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.hr.doctype.hr_settings.hr_settings.set_proceed_with_frequency_change',
|
||||||
|
callback: () => {
|
||||||
|
cur_frm.save();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
25
erpnext/templates/emails/anniversary_reminder.html
Normal file
25
erpnext/templates/emails/anniversary_reminder.html
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<div class="gray-container text-center">
|
||||||
|
<div>
|
||||||
|
{% for person in anniversary_persons %}
|
||||||
|
{% if person.image %}
|
||||||
|
<img
|
||||||
|
class="avatar-frame standard-image"
|
||||||
|
src="{{ person.image }}"
|
||||||
|
style="{{ css_style or '' }}"
|
||||||
|
title="{{ person.name }}">
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span
|
||||||
|
class="avatar-frame standard-image"
|
||||||
|
style="{{ css_style or '' }}"
|
||||||
|
title="{{ person.name }}">
|
||||||
|
{{ frappe.utils.get_abbr(person.name) }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 15px">
|
||||||
|
<span>{{ reminder_text }}</span>
|
||||||
|
<p class="text-muted">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
16
erpnext/templates/emails/holiday_reminder.html
Normal file
16
erpnext/templates/emails/holiday_reminder.html
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<div>
|
||||||
|
<span>{{ reminder_text }}</span>
|
||||||
|
<p class="text-muted">{{ message }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if advance_holiday_reminder %}
|
||||||
|
{% if holidays | len > 0 %}
|
||||||
|
<ol>
|
||||||
|
{% for holiday in holidays %}
|
||||||
|
<li>{{ frappe.format(holiday.holiday_date, 'Date') }} - {{ holiday.description }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% else %}
|
||||||
|
<p>You don't have no upcoming holidays this {{ frequency }}.</p>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
Loading…
x
Reference in New Issue
Block a user