@@ -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){
frappe.dom.freeze();
frappe.call({
@@ -52,7 +53,7 @@ frappe.ui.form.on('Twitter Settings', {
});
}
},
- after_save: function(frm){
+ after_save: function(frm) {
frm.trigger("login");
}
});
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.json b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
index 36776e5c20..8d05877f06 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.json
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.json
@@ -2,6 +2,7 @@
"actions": [],
"creation": "2020-01-30 10:29:08.562108",
"doctype": "DocType",
+ "documentation": "https://docs.erpnext.com/docs/user/manual/en/CRM/twitter-settings",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
@@ -77,7 +78,7 @@
"image_field": "profile_pic",
"issingle": 1,
"links": [],
- "modified": "2020-05-13 17:50:47.934776",
+ "modified": "2021-02-18 15:18:07.900031",
"modified_by": "Administrator",
"module": "CRM",
"name": "Twitter Settings",
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
index 1e1beab2d2..47756560ec 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -32,7 +32,9 @@ class TwitterSettings(Document):
try:
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()
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.throw(_('Invalid Consumer Key or Consumer Secret Key'))
- def get_api(self, access_token, access_token_secret):
- # authentication of consumer key and secret
- auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
- # authentication of access token and secret
- auth.set_access_token(access_token, access_token_secret)
+ def get_api(self):
+ # authentication of consumer key and secret
+ auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
+ # authentication of access token and secret
+ auth.set_access_token(self.access_token, self.access_token_secret)
return tweepy.API(auth)
@@ -68,13 +70,13 @@ class TwitterSettings(Document):
def upload_image(self, 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)
return media.media_id
def send_tweet(self, text, media_id=None):
- api = self.get_api(self.access_token, self.access_token_secret)
+ api = self.get_api()
try:
if media_id:
response = api.update_status(status = text, media_ids = [media_id])
@@ -84,12 +86,32 @@ class TwitterSettings(Document):
return response
except TweepError as e:
- content = json.loads(e.response.content)
- content = content["errors"][0]
- if e.response.status_code == 401:
- self.db_set("session_status", "Expired")
- frappe.db.commit()
- frappe.throw(content["message"],title="Twitter Error {0} {1}".format(e.response.status_code, e.response.reason))
+ 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 = content["errors"][0]
+ if e.response.status_code == 401:
+ self.db_set("session_status", "Expired")
+ frappe.db.commit()
+ frappe.throw(content["message"],title=_("Twitter Error {0} : {1}").format(e.response.status_code, e.response.reason))
+
@frappe.whitelist(allow_guest=True)
def callback(oauth_token = None, oauth_verifier = None):
diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
index 6ae3e12d50..82e7136d6b 100644
--- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
+++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
@@ -29,10 +29,10 @@ class TestFeeValidity(unittest.TestCase):
healthcare_settings.save(ignore_permissions=True)
patient, medical_department, practitioner = create_healthcare_docs()
- # appointment should not be invoiced. Check Fee Validity created for new patient
+ # For first appointment, invoice is generated
appointment = create_appointment(patient, practitioner, nowdate())
invoiced = frappe.db.get_value("Patient Appointment", appointment.name, "invoiced")
- self.assertEqual(invoiced, 0)
+ self.assertEqual(invoiced, 1)
# appointment should not be invoiced as it is within fee validity
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4))
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 2976ef13a1..c6e489ec17 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -241,6 +241,13 @@ frappe.ui.form.on('Patient Appointment', {
frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0);
frm.toggle_reqd('billing_item', 0);
+ } else if (data.message) {
+ frm.toggle_display('mode_of_payment', 1);
+ frm.toggle_display('paid_amount', 1);
+ frm.toggle_display('billing_item', 1);
+ frm.toggle_reqd('mode_of_payment', 1);
+ frm.toggle_reqd('paid_amount', 1);
+ frm.toggle_reqd('billing_item', 1);
} else {
// if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index 83c92af36a..6e996bd128 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -134,6 +134,7 @@
"set_only_once": 1
},
{
+ "depends_on": "eval:doc.practitioner;",
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Appointment Details"
@@ -349,7 +350,7 @@
}
],
"links": [],
- "modified": "2021-02-08 13:13:15.116833",
+ "modified": "2021-06-16 00:40:26.841794",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",
@@ -400,4 +401,4 @@
"title_field": "title",
"track_changes": 1,
"track_seen": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index cdd4ad39c8..05e2cd30df 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -135,8 +135,6 @@ def check_payment_fields_reqd(patient):
fee_validity = frappe.db.exists('Fee Validity', {'patient': patient, 'status': 'Pending'})
if fee_validity:
return {'fee_validity': fee_validity}
- if check_is_new_patient(patient):
- return False
return True
return False
@@ -151,8 +149,6 @@ def invoice_appointment(appointment_doc):
elif not fee_validity:
if frappe.db.exists('Fee Validity Reference', {'appointment': appointment_doc.name}):
return
- if check_is_new_patient(appointment_doc.patient, appointment_doc.name):
- return
else:
fee_validity = None
@@ -196,9 +192,7 @@ def check_is_new_patient(patient, name=None):
filters['name'] = ('!=', name)
has_previous_appointment = frappe.db.exists('Patient Appointment', filters)
- if has_previous_appointment:
- return False
- return True
+ return not has_previous_appointment
def get_appointment_item(appointment_doc, item):
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index 9dd4a2c73c..2df6921b14 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -4,11 +4,12 @@
from __future__ import unicode_literals
import unittest
import frappe
-from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter
+from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter, check_payment_fields_reqd, check_is_new_patient
from frappe.utils import nowdate, add_days, now_datetime
from frappe.utils.make_random import get_random
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
+
class TestPatientAppointment(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabPatient Appointment`""")
@@ -176,6 +177,28 @@ class TestPatientAppointment(unittest.TestCase):
mark_invoiced_inpatient_occupancy(ip_record1)
discharge_patient(ip_record1)
+ def test_payment_should_be_mandatory_for_new_patient_appointment(self):
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ frappe.db.set_value('Healthcare Settings', None, 'max_visits', 3)
+ frappe.db.set_value('Healthcare Settings', None, 'valid_days', 30)
+
+ patient = create_patient()
+ assert check_is_new_patient(patient)
+ payment_required = check_payment_fields_reqd(patient)
+ assert payment_required is True
+
+ def test_sales_invoice_should_be_generated_for_new_patient_appointment(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ invoice_count = frappe.db.count('Sales Invoice')
+
+ assert check_is_new_patient(patient)
+ create_appointment(patient, practitioner, nowdate())
+ new_invoice_count = frappe.db.count('Sales Invoice')
+
+ assert new_invoice_count == invoice_count + 1
+
def create_healthcare_docs():
patient = create_patient()
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 74977cd8bc..4854bfd1e1 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -355,7 +355,8 @@ scheduler_events = {
"erpnext.crm.doctype.opportunity.opportunity.auto_close_opportunity",
"erpnext.controllers.accounts_controller.update_invoice_status",
"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.assets.doctype.asset.depreciation.post_depreciation_entries",
"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.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": [
"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"
diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
index 0d7fded921..3db81654a6 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py
@@ -8,7 +8,7 @@ from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document
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):
@@ -39,7 +39,7 @@ class CompensatoryLeaveRequest(Document):
frappe.throw(_("You are not present all day(s) between compensatory leave request days"))
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 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)))
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index f4280152c5..643f3da2ff 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
import frappe
from frappe.utils import 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.permissions import add_user_permission, remove_user_permission, \
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 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.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):
- return employee_doc["user_id"] or employee_doc["personal_email"] or employee_doc["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 += "
"
- 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
+ return employee_doc.get("user_id") or employee_doc.get("personal_email") or employee_doc.get("company_email")
def get_holiday_list_for_employee(employee, raise_exception=True):
if employee:
@@ -390,17 +301,40 @@ def get_holiday_list_for_employee(employee, raise_exception=True):
return holiday_list
-def is_holiday(employee, date=None, raise_exception=True):
- '''Returns True if given Employee has an holiday on the given date
- :param employee: Employee `name`
- :param date: Date to check. Will check for today if None'''
+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
+ :param employee: Employee `name`
+ :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)
if not date:
date = today()
- if holiday_list:
- return frappe.get_all('Holiday List', dict(name=holiday_list, holiday_date=date)) and True or False
+ if not holiday_list:
+ 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()
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
-
def on_doctype_update():
frappe.db.add_index("Employee", ["lft", "rgt"])
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
new file mode 100644
index 0000000000..2155c027a9
--- /dev/null
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -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 += "
"
+ 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 += "
"
+ 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 ποΈποΈ")
+ )
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 8fc7cf1934..5feb6de8f2 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -1,7 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
import frappe
import erpnext
@@ -12,29 +10,6 @@ from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records('Employee')
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):
employee1 = make_employee("test_employee_1@company.com")
employee2 = make_employee("test_employee_2@company.com")
diff --git a/erpnext/hr/doctype/employee/test_employee_reminders.py b/erpnext/hr/doctype/employee/test_employee_reminders.py
new file mode 100644
index 0000000000..7e560f512d
--- /dev/null
+++ b/erpnext/hr/doctype/employee/test_employee_reminders.py
@@ -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)
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 2396a8eee9..8aa3c0ca9f 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -11,8 +11,14 @@
"emp_created_by",
"column_break_4",
"standard_working_hours",
- "stop_birthday_reminders",
"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",
"send_leave_notification",
"leave_approval_notification_template",
@@ -50,13 +56,6 @@
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
- {
- "default": "0",
- "description": "Don't send employee birthday reminders",
- "fieldname": "stop_birthday_reminders",
- "fieldtype": "Check",
- "label": "Stop Birthday Reminders"
- },
{
"default": "1",
"fieldname": "expense_approver_mandatory_in_expense_claim",
@@ -142,13 +141,53 @@
"fieldname": "standard_working_hours",
"fieldtype": "Int",
"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",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2021-05-11 10:52:56.192773",
+ "modified": "2021-08-24 14:54:12.834162",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py
index c99df269cc..a47409363c 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.py
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.py
@@ -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
# For license information, please see license.txt
-from __future__ import unicode_literals
import frappe
+
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):
def validate(self):
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):
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
set_by_naming_series("Employee", "employee_number",
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 {}.
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
diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.py b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
index 674c8e3eb4..9c765d7371 100644
--- a/erpnext/hr/doctype/upload_attendance/upload_attendance.py
+++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.py
@@ -10,7 +10,7 @@ from frappe import _
from frappe.utils.csvutils import UnicodeWriter
from frappe.model.document import Document
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
-from erpnext.hr.utils import get_holidays_for_employee
+from erpnext.hr.utils import get_holiday_dates_for_employee
class UploadAttendance(Document):
pass
@@ -94,7 +94,7 @@ def get_holidays_for_employees(employees, from_date, to_date):
holidays = {}
for employee in employees:
holiday_list = get_holiday_list_for_employee(employee)
- holiday = get_holidays_for_employee(employee, getdate(from_date), getdate(to_date))
+ holiday = get_holiday_dates_for_employee(employee, getdate(from_date), getdate(to_date))
if holiday_list not in holidays:
holidays[holiday_list] = holiday
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index a1026ce055..15b237d93c 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -335,21 +335,44 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
total_given_benefit_amount = sum_of_given_benefit[0].total_amount
return total_given_benefit_amount
-def get_holidays_for_employee(employee, start_date, end_date):
- holiday_list = get_holiday_list_for_employee(employee)
+def get_holiday_dates_for_employee(employee, start_date, end_date):
+ """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)
+
+ return [cstr(h.holiday_date) for h in holidays]
- 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]
+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
@erpnext.allow_regional
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 0ba85078ea..eb1dfc8cae 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -148,6 +148,7 @@ class BOM(WebsiteGenerator):
self.set_plc_conversion_rate()
self.validate_uom_is_interger()
self.set_bom_material_details()
+ self.set_bom_scrap_items_detail()
self.validate_materials()
self.set_routing_operations()
self.validate_operations()
@@ -200,7 +201,7 @@ class BOM(WebsiteGenerator):
def set_bom_material_details(self):
for item in self.get("items"):
- self.validate_bom_currecny(item)
+ self.validate_bom_currency(item)
ret = self.get_bom_material_detail({
"company": self.company,
@@ -219,6 +220,19 @@ class BOM(WebsiteGenerator):
if not item.get(r):
item.set(r, ret[r])
+ def set_bom_scrap_items_detail(self):
+ for item in self.get("scrap_items"):
+ args = {
+ "item_code": item.item_code,
+ "company": self.company,
+ "scrap_items": True,
+ "bom_no": '',
+ }
+ ret = self.get_bom_material_detail(args)
+ for key, value in ret.items():
+ if not item.get(key):
+ item.set(key, value)
+
@frappe.whitelist()
def get_bom_material_detail(self, args=None):
""" Get raw material details like uom, desc and rate"""
@@ -255,7 +269,7 @@ class BOM(WebsiteGenerator):
return ret_item
- def validate_bom_currecny(self, item):
+ def validate_bom_currency(self, item):
if item.get('bom_no') and frappe.db.get_value('BOM', item.get('bom_no'), 'currency') != self.currency:
frappe.throw(_("Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2}")
.format(item.idx, item.bom_no, self.currency))
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index b86c236a7f..bf0446bb68 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -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_fill_debtor_creditor_number
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)
erpnext.patches.v13_0.update_timesheet_changes
erpnext.patches.v13_0.add_doctype_to_sla #14-06-2021
diff --git a/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py
new file mode 100644
index 0000000000..1787a56025
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_stop_to_send_birthday_reminders.py
@@ -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
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
index c7fbb06b10..a1cde08a74 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py
@@ -9,7 +9,7 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt
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.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):
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
# 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:
- 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
leave_days = calculate_lwp(employee, payroll_period_obj.start_date, holidays, working_days)
leave_days_amount = leave_days * per_day_amount_total
diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py
index ef3a6cc006..66dec075d8 100644
--- a/erpnext/payroll/doctype/payroll_period/payroll_period.py
+++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py
@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
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):
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
working_days = actual_no_of_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)
return payroll_period[0][0], working_days, actual_no_of_days
return False, False, False
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 5f5fdd5c83..6325351deb 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -11,6 +11,7 @@ from frappe.model.naming import make_autoname
from frappe import msgprint, _
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.utils import get_holiday_dates_for_employee
from erpnext.utilities.transaction_base import TransactionBase
from frappe.utils.background_jobs import enqueue
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
@@ -337,20 +338,7 @@ class SalarySlip(TransactionBase):
return payment_days
def get_holidays_for_employee(self, start_date, end_date):
- holiday_list = get_holiday_list_for_employee(self.employee)
- 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
+ return get_holiday_dates_for_employee(self.employee, start_date, end_date)
def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
lwp = 0
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f240b8ca91..9caf1defe9 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -82,6 +82,17 @@ $.extend(erpnext, {
});
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();
+ }
+ });
}
});
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 4b7309440c..9d4f9206f5 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -588,7 +588,7 @@ def get_json(filters, report_name, data):
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
- gst_json = {"version": "GST2.2.9",
+ gst_json = {"version": "GST3.0.4",
"hash": "hash", "gstin": gstin, "fp": fp}
res = {}
@@ -765,7 +765,7 @@ def get_cdnr_reg_json(res, gstin):
"ntty": invoice[0]["document_type"],
"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
"rchrg": invoice[0]["reverse_charge"],
- "inv_type": get_invoice_type_for_cdnr(invoice[0])
+ "inv_typ": get_invoice_type_for_cdnr(invoice[0])
}
inv_item["itms"] = []
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index cd94ee101a..0d839fc822 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -20,6 +20,7 @@
"tax_withholding_category",
"default_bank_account",
"lead_name",
+ "opportunity_name",
"image",
"column_break0",
"account_manager",
@@ -493,6 +494,14 @@
"fieldtype": "Link",
"label": "Tax Withholding Category",
"options": "Tax Withholding Category"
+ },
+ {
+ "fieldname": "opportunity_name",
+ "fieldtype": "Link",
+ "label": "From Opportunity",
+ "no_copy": 1,
+ "options": "Opportunity",
+ "print_hide": 1
}
],
"icon": "fa fa-user",
@@ -500,7 +509,7 @@
"image_field": "image",
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-28 12:54:57.258959",
+ "modified": "2021-08-25 18:56:09.929905",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 45d5ce0c1c..6dee2ad92a 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -423,11 +423,11 @@ def replace_abbr(company, old, new):
_rename_record(d)
try:
frappe.db.auto_commit_on_many_writes = 1
- frappe.db.set_value("Company", company, "abbr", new)
for dt in ["Warehouse", "Account", "Cost Center", "Department",
"Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
_rename_records(dt)
frappe.db.commit()
+ frappe.db.set_value("Company", company, "abbr", new)
except Exception:
frappe.log_error(title=_('Abbreviation Rename Error'))
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 36dfa6d795..706ca36598 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -356,3 +356,23 @@ erpnext.stock.delivery_note.set_print_hide = function(doc, cdt, cdn){
dn_fields['taxes'].print_hide = 0;
}
}
+
+
+frappe.tour['Delivery Note'] = [
+ {
+ fieldname: "customer",
+ title: __("Customer"),
+ description: __("This field is used to set the 'Customer'.")
+ },
+ {
+ fieldname: "items",
+ title: __("Items"),
+ description: __("This table is used to set details about the 'Item', 'Qty', 'Basic Rate', etc.") + " " +
+ __("Different 'Source Warehouse' and 'Target Warehouse' can be set for each row.")
+ },
+ {
+ fieldname: "set_posting_time",
+ title: __("Edit Posting Date and Time"),
+ description: __("This option can be checked to edit the 'Posting Date' and 'Posting Time' fields.")
+ }
+]
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index c5bc9f14fb..c587dd5c7e 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -792,4 +792,4 @@ frappe.ui.form.on("UOM Conversion Detail", {
});
}
}
-})
+});
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 908020d02b..8f34794db9 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1101,3 +1101,4 @@ function check_should_not_attach_bom_items(bom_no) {
}
$.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));
+
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index 84f65a077e..aa502a432d 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -302,3 +302,4 @@ erpnext.stock.StockReconciliation = class StockReconciliation extends erpnext.st
};
cur_frm.cscript = new erpnext.stock.StockReconciliation({frm: cur_frm});
+
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.js b/erpnext/stock/doctype/stock_settings/stock_settings.js
index 48624e0f25..6167becdaa 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.js
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.js
@@ -16,36 +16,3 @@ frappe.ui.form.on('Stock Settings', {
}
});
-frappe.tour['Stock Settings'] = [
- {
- fieldname: "item_naming_by",
- title: __("Item Naming By"),
- description: __("By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a ") + "
Naming Series" + __(" choose the 'Naming Series' option."),
- },
- {
- fieldname: "default_warehouse",
- title: __("Default Warehouse"),
- description: __("Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.")
- },
- {
- fieldname: "allow_negative_stock",
- title: __("Allow Negative Stock"),
- description: __("This will allow stock items to be displayed in negative values. Using this option depends on your use case. With this option unchecked, the system warns before obstructing a transaction that is causing negative stock.")
-
- },
- {
- fieldname: "valuation_method",
- title: __("Valuation Method"),
- description: __("Choose between FIFO and Moving Average Valuation Methods. Click ") + "
here" + __(" to know more about them.")
- },
- {
- fieldname: "show_barcode_field",
- title: __("Show Barcode Field"),
- description: __("Show 'Scan Barcode' field above every child table to insert Items with ease.")
- },
- {
- fieldname: "automatically_set_serial_nos_based_on_fifo",
- title: __("Automatically Set Serial Nos based on FIFO"),
- description: __("Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.")
- }
-];
diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js
index 9243e1ed84..4e1679c411 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.js
+++ b/erpnext/stock/doctype/warehouse/warehouse.js
@@ -86,3 +86,4 @@ function convert_to_group_or_ledger(frm){
})
}
+
diff --git a/erpnext/stock/form_tour/stock_entry/stock_entry.json b/erpnext/stock/form_tour/stock_entry/stock_entry.json
new file mode 100644
index 0000000000..6363c6ad4d
--- /dev/null
+++ b/erpnext/stock/form_tour/stock_entry/stock_entry.json
@@ -0,0 +1,56 @@
+{
+ "creation": "2021-08-24 14:44:22.292652",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-08-25 16:31:31.441194",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Entry",
+ "owner": "Administrator",
+ "reference_doctype": "Stock Entry",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select the type of Stock Entry to be made. For now, to receive stock into a warehouses select
Material Receipt.",
+ "field": "",
+ "fieldname": "stock_entry_type",
+ "fieldtype": "Link",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Stock Entry Type",
+ "next_step_condition": "eval: doc.stock_entry_type === \"Material Receipt\"",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Stock Entry Type"
+ },
+ {
+ "description": "Select a target warehouse where the stock will be received.",
+ "field": "",
+ "fieldname": "to_warehouse",
+ "fieldtype": "Link",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Default Target Warehouse",
+ "next_step_condition": "eval: doc.to_warehouse",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Default Target Warehouse"
+ },
+ {
+ "description": "Select an item and entry quantity to be delivered.",
+ "field": "",
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Items",
+ "next_step_condition": "eval: doc.items[0]?.item_code",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Items"
+ }
+ ],
+ "title": "Stock Entry"
+}
\ No newline at end of file
diff --git a/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json
new file mode 100644
index 0000000000..5b7fd72c08
--- /dev/null
+++ b/erpnext/stock/form_tour/stock_reconciliation/stock_reconciliation.json
@@ -0,0 +1,55 @@
+{
+ "creation": "2021-08-24 14:44:46.770952",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-08-25 16:26:11.718664",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Reconciliation",
+ "owner": "Administrator",
+ "reference_doctype": "Stock Reconciliation",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Set Purpose to Opening Stock to set the stock opening balance.",
+ "field": "",
+ "fieldname": "purpose",
+ "fieldtype": "Select",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Purpose",
+ "next_step_condition": "eval: doc.purpose === \"Opening Stock\"",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Purpose"
+ },
+ {
+ "description": "Select the items for which the opening stock has to be set.",
+ "field": "",
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Items",
+ "next_step_condition": "eval: doc.items[0]?.item_code",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Items"
+ },
+ {
+ "description": "Edit the Posting Date by clicking on the Edit Posting Date and Time checkbox below.",
+ "field": "",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Posting Date",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Posting Date"
+ }
+ ],
+ "title": "Stock Reconciliation"
+}
\ No newline at end of file
diff --git a/erpnext/stock/form_tour/stock_settings/stock_settings.json b/erpnext/stock/form_tour/stock_settings/stock_settings.json
new file mode 100644
index 0000000000..3d164e33b3
--- /dev/null
+++ b/erpnext/stock/form_tour/stock_settings/stock_settings.json
@@ -0,0 +1,89 @@
+{
+ "creation": "2021-08-20 15:20:59.336585",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-08-25 16:19:37.699528",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock Settings",
+ "owner": "Administrator",
+ "reference_doctype": "Stock Settings",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "By default, the Item Name is set as per the Item Code entered. If you want Items to be named by a Naming Series choose the 'Naming Series' option.",
+ "field": "",
+ "fieldname": "item_naming_by",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Item Naming By",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Item Naming By"
+ },
+ {
+ "description": "Set a Default Warehouse for Inventory Transactions. This will be fetched into the Default Warehouse in the Item master.",
+ "field": "",
+ "fieldname": "default_warehouse",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Default Warehouse",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Default Warehouse"
+ },
+ {
+ "description": "Quality inspection is performed on the inward and outward movement of goods. Receipt and delivery transactions will be stopped or the user will be warned if the quality inspection is not performed.",
+ "field": "",
+ "fieldname": "action_if_quality_inspection_is_not_submitted",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Action If Quality Inspection Is Not Submitted",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Action if Quality Inspection Is Not Submitted"
+ },
+ {
+ "description": "Serial numbers for stock will be set automatically based on the Items entered based on first in first out in transactions like Purchase/Sales Invoices, Delivery Notes, etc.",
+ "field": "",
+ "fieldname": "automatically_set_serial_nos_based_on_fifo",
+ "fieldtype": "Check",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Automatically Set Serial Nos Based on FIFO",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Automatically Set Serial Nos based on FIFO"
+ },
+ {
+ "description": "Show 'Scan Barcode' field above every child table to insert Items with ease.",
+ "field": "",
+ "fieldname": "show_barcode_field",
+ "fieldtype": "Check",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Show Barcode Field in Stock Transactions",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Show Barcode Field"
+ },
+ {
+ "description": "Choose between FIFO and Moving Average Valuation Methods. Click
here to know more about them.",
+ "field": "",
+ "fieldname": "valuation_method",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Default Valuation Method",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Default Valuation Method"
+ }
+ ],
+ "title": "Stock Settings"
+}
\ No newline at end of file
diff --git a/erpnext/stock/form_tour/warehouse/warehouse.json b/erpnext/stock/form_tour/warehouse/warehouse.json
new file mode 100644
index 0000000000..23ff2aebba
--- /dev/null
+++ b/erpnext/stock/form_tour/warehouse/warehouse.json
@@ -0,0 +1,54 @@
+{
+ "creation": "2021-08-24 14:43:44.465237",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-08-24 14:50:31.988256",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Warehouse",
+ "owner": "Administrator",
+ "reference_doctype": "Warehouse",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select a name for the warehouse. This should reflect its location or purpose.",
+ "field": "",
+ "fieldname": "warehouse_name",
+ "fieldtype": "Data",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Warehouse Name",
+ "next_step_condition": "eval: doc.warehouse_name",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Warehouse Name"
+ },
+ {
+ "description": "Select a warehouse type to categorize the warehouse into a sub-group.",
+ "field": "",
+ "fieldname": "warehouse_type",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Warehouse Type",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Warehouse Type"
+ },
+ {
+ "description": "Select an account to set a default account for all transactions with this warehouse.",
+ "field": "",
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Account",
+ "parent_field": "",
+ "position": "Top",
+ "title": "Account"
+ }
+ ],
+ "title": "Warehouse"
+}
\ No newline at end of file
diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json
index 847464822b..c246747a5b 100644
--- a/erpnext/stock/module_onboarding/stock/stock.json
+++ b/erpnext/stock/module_onboarding/stock/stock.json
@@ -19,32 +19,26 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock",
"idx": 0,
"is_complete": 0,
- "modified": "2020-10-14 14:54:42.741971",
+ "modified": "2021-08-20 14:38:55.570067",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
"owner": "Administrator",
"steps": [
{
- "step": "Setup your Warehouse"
+ "step": "Stock Settings"
},
{
- "step": "Create a Product"
- },
- {
- "step": "Create a Supplier"
- },
- {
- "step": "Introduction to Stock Entry"
+ "step": "Create a Warehouse"
},
{
"step": "Create a Stock Entry"
},
{
- "step": "Create a Purchase Receipt"
+ "step": "Stock Opening Balance"
},
{
- "step": "Stock Settings"
+ "step": "View Stock Projected Qty"
}
],
"subtitle": "Inventory, Warehouses, Analysis, and more.",
diff --git a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json
deleted file mode 100644
index 9012493f57..0000000000
--- a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json
+++ /dev/null
@@ -1,19 +0,0 @@
-{
- "action": "Create Entry",
- "creation": "2020-05-19 18:59:13.266713",
- "docstatus": 0,
- "doctype": "Onboarding Step",
- "idx": 0,
- "is_complete": 0,
- "is_mandatory": 0,
- "is_single": 0,
- "is_skipped": 0,
- "modified": "2020-10-14 14:53:25.618434",
- "modified_by": "Administrator",
- "name": "Create a Purchase Receipt",
- "owner": "Administrator",
- "reference_document": "Purchase Receipt",
- "show_full_form": 1,
- "title": "Create a Purchase Receipt",
- "validate_action": 1
-}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json
index 09902b8844..3cb522c893 100644
--- a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json
+++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json
@@ -1,19 +1,21 @@
{
"action": "Create Entry",
+ "action_label": "Create a Material Transfer Entry",
"creation": "2020-05-15 03:20:16.277043",
+ "description": "# Manage Stock Movements\nStock entry allows you to register the movement of stock for various purposes like transfer, received, issues, repacked, etc. To address issues related to theft and pilferages, you can always ensure that the movement of goods happens against a document reference Stock Entry in ERPNext.\n\nLet\u2019s get a quick walk-through on the various scenarios covered in Stock Entry by watching [*this video*](https://www.youtube.com/watch?v=Njt107hlY3I).",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-10-14 14:53:00.105905",
+ "modified": "2021-06-18 13:57:11.434063",
"modified_by": "Administrator",
"name": "Create a Stock Entry",
"owner": "Administrator",
"reference_document": "Stock Entry",
+ "show_form_tour": 1,
"show_full_form": 1,
- "title": "Create a Stock Entry",
+ "title": "Manage Stock Movements",
"validate_action": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json
index ef61fa3b2e..49efe578a2 100644
--- a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json
+++ b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json
@@ -1,18 +1,19 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
"creation": "2020-05-14 22:09:10.043554",
+ "description": "# Create a Supplier\nIn this step we will create a **Supplier**. If you have already created a **Supplier** you can skip this step.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-10-14 14:53:00.120455",
+ "modified": "2021-05-17 16:37:37.697077",
"modified_by": "Administrator",
"name": "Create a Supplier",
"owner": "Administrator",
"reference_document": "Supplier",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Create a Supplier",
"validate_action": 1
diff --git a/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json
new file mode 100644
index 0000000000..22c88bf10e
--- /dev/null
+++ b/erpnext/stock/onboarding_step/create_a_warehouse/create_a_warehouse.json
@@ -0,0 +1,21 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create your first warehouse ",
+ "creation": "2021-05-17 16:13:19.297789",
+ "description": "# Setup a Warehouse\nThe warehouse can be your location/godown/store where you maintain the item's inventory, and receive/deliver them to various parties.\n\nIn ERPNext, you can maintain a Warehouse in the tree structure, so that location and sub-location of an item can be tracked. Also, you can link a Warehouse to a specific Accounting ledger, where the real-time stock value of that warehouse\u2019s item will be reflected.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-08-18 12:23:36.675572",
+ "modified_by": "Administrator",
+ "name": "Create a Warehouse",
+ "owner": "Administrator",
+ "reference_document": "Warehouse",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Setup a Warehouse",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_step/create_an_item/create_an_item.json b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json
new file mode 100644
index 0000000000..016cbd566d
--- /dev/null
+++ b/erpnext/stock/onboarding_step/create_an_item/create_an_item.json
@@ -0,0 +1,22 @@
+{
+ "action": "Create Entry",
+ "action_label": "",
+ "creation": "2021-05-17 13:47:18.515052",
+ "description": "# Create an Item\nThe Stock module deals with the movement of items.\n\nIn this step we will create an [**Item**](https://docs.erpnext.com/docs/user/manual/en/stock/item).",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "intro_video_url": "",
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-05-18 16:15:20.695028",
+ "modified_by": "Administrator",
+ "name": "Create an Item",
+ "owner": "Administrator",
+ "reference_document": "Item",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Create an Item",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json
index 212e5055ed..384950e8b9 100644
--- a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json
+++ b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json
@@ -1,17 +1,18 @@
{
"action": "Watch Video",
"creation": "2020-05-15 02:47:17.958806",
+ "description": "# Introduction to Stock Entry\nThis video will give a quick introduction to [**Stock Entry**](https://docs.erpnext.com/docs/user/manual/en/stock/stock-entry).",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-10-14 14:53:00.075177",
+ "modified": "2021-05-18 15:13:43.306064",
"modified_by": "Administrator",
"name": "Introduction to Stock Entry",
"owner": "Administrator",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Introduction to Stock Entry",
"validate_action": 1,
diff --git a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json
index 75940ed2a6..5d33a64910 100644
--- a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json
+++ b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json
@@ -5,15 +5,15 @@
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-10-14 14:53:25.538900",
+ "modified": "2021-05-17 13:53:06.936579",
"modified_by": "Administrator",
"name": "Setup your Warehouse",
"owner": "Administrator",
"path": "Tree/Warehouse",
"reference_document": "Warehouse",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Set up your Warehouse",
"validate_action": 1
diff --git a/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json
new file mode 100644
index 0000000000..48fd1fddee
--- /dev/null
+++ b/erpnext/stock/onboarding_step/stock_opening_balance/stock_opening_balance.json
@@ -0,0 +1,22 @@
+{
+ "action": "Create Entry",
+ "action_label": "Let\u2019s create a stock opening entry",
+ "creation": "2021-05-17 16:13:47.511883",
+ "description": "# Update Stock Opening Balance\nIt\u2019s an entry to update the stock balance of an item, in a warehouse, on a date and time you are going live on ERPNext.\n\nOnce opening stocks are updated, you can create transactions like manufacturing and stock deliveries, where this opening stock will be consumed.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-06-18 13:59:36.021097",
+ "modified_by": "Administrator",
+ "name": "Stock Opening Balance",
+ "owner": "Administrator",
+ "reference_document": "Stock Reconciliation",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Update Stock Opening Balance",
+ "validate_action": 1,
+ "video_url": "https://www.youtube.com/watch?v=nlHX0ZZ84Lw"
+}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json
index ae34afa695..2cf90e806c 100644
--- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json
+++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json
@@ -1,19 +1,21 @@
{
"action": "Show Form Tour",
+ "action_label": "Take a walk through Stock Settings",
"creation": "2020-05-15 02:53:57.209967",
+ "description": "# Review Stock Settings\n\nIn ERPNext, the Stock module\u2019s features are configurable as per your business needs. Stock Settings is the place where you can set your preferences for:\n- Default values for Item and Pricing\n- Default valuation method for inventory valuation\n- Set preference for serialization and batching of item\n- Set tolerance for over-receipt and delivery of items",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 1,
"is_skipped": 0,
- "modified": "2020-10-14 14:53:00.092504",
+ "modified": "2021-08-18 12:06:51.139387",
"modified_by": "Administrator",
"name": "Stock Settings",
"owner": "Administrator",
"reference_document": "Stock Settings",
+ "show_form_tour": 0,
"show_full_form": 0,
- "title": "Explore Stock Settings",
+ "title": "Review Stock Settings",
"validate_action": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json
new file mode 100644
index 0000000000..e684780751
--- /dev/null
+++ b/erpnext/stock/onboarding_step/view_stock_projected_qty/view_stock_projected_qty.json
@@ -0,0 +1,24 @@
+{
+ "action": "View Report",
+ "action_label": "Check Stock Projected Qty",
+ "creation": "2021-08-20 14:38:41.649103",
+ "description": "# Check Stock Reports\nBased on the various stock transactions, you can get a host of one-click Stock Reports in ERPNext like Stock Ledger, Stock Balance, Projected Quantity, and Ageing analysis.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-08-20 14:38:41.649103",
+ "modified_by": "Administrator",
+ "name": "View Stock Projected Qty",
+ "owner": "Administrator",
+ "reference_report": "Stock Projected Qty",
+ "report_description": "You can set the filters to narrow the results, then click on Generate New Report to see the updated report.",
+ "report_reference_doctype": "Item",
+ "report_type": "Script Report",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "Check Stock Projected Qty",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json
new file mode 100644
index 0000000000..c46c4bdab8
--- /dev/null
+++ b/erpnext/stock/onboarding_step/view_warehouses/view_warehouses.json
@@ -0,0 +1,20 @@
+{
+ "action": "Go to Page",
+ "creation": "2021-05-17 16:12:43.427579",
+ "description": "# View Warehouse\nIn ERPNext the term 'warehouse' can be thought of as a storage location.\n\nWarehouses are arranged in ERPNext in a tree like structure, where multiple sub-warehouses can be grouped under a single warehouse.\n\nIn this step we will view the [**Warehouse Tree**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse#21-tree-view) to view the [**Warehouses**](https://docs.erpnext.com/docs/user/manual/en/stock/warehouse) that are set by default.",
+ "docstatus": 0,
+ "doctype": "Onboarding Step",
+ "idx": 0,
+ "is_complete": 0,
+ "is_single": 0,
+ "is_skipped": 0,
+ "modified": "2021-05-18 15:04:41.198413",
+ "modified_by": "Administrator",
+ "name": "View Warehouses",
+ "owner": "Administrator",
+ "path": "Tree/Warehouse",
+ "show_form_tour": 0,
+ "show_full_form": 0,
+ "title": "View Warehouses",
+ "validate_action": 1
+}
\ No newline at end of file
diff --git a/erpnext/templates/emails/anniversary_reminder.html b/erpnext/templates/emails/anniversary_reminder.html
new file mode 100644
index 0000000000..ac9f7e4993
--- /dev/null
+++ b/erpnext/templates/emails/anniversary_reminder.html
@@ -0,0 +1,25 @@
+
+
+ {% for person in anniversary_persons %}
+ {% if person.image %}
+
+
+ {% else %}
+
+ {{ frappe.utils.get_abbr(person.name) }}
+
+ {% endif %}
+ {% endfor %}
+
+
+
{{ reminder_text }}
+
{{ message }}
+
+
\ No newline at end of file
diff --git a/erpnext/templates/emails/holiday_reminder.html b/erpnext/templates/emails/holiday_reminder.html
new file mode 100644
index 0000000000..e38d27bf8b
--- /dev/null
+++ b/erpnext/templates/emails/holiday_reminder.html
@@ -0,0 +1,16 @@
+
+
{{ reminder_text }}
+
{{ message }}
+
+
+{% if advance_holiday_reminder %}
+ {% if holidays | len > 0 %}
+
+ {% for holiday in holidays %}
+ - {{ frappe.format(holiday.holiday_date, 'Date') }} - {{ holiday.description }}
+ {% endfor %}
+
+ {% else %}
+
You don't have no upcoming holidays this {{ frequency }}.
+ {% endif %}
+{% endif %}
diff --git a/erpnext/templates/includes/rfq/rfq_items.html b/erpnext/templates/includes/rfq/rfq_items.html
index caa15f386b..04cf922664 100644
--- a/erpnext/templates/includes/rfq/rfq_items.html
+++ b/erpnext/templates/includes/rfq/rfq_items.html
@@ -1,4 +1,4 @@
-{% from "erpnext/templates/includes/rfq/rfq_macros.html" import item_name_and_description %}
+{% from "templates/includes/rfq/rfq_macros.html" import item_name_and_description %}
{% for d in doc.items %}
diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html
index 6e2edb6391..6516482c23 100644
--- a/erpnext/templates/pages/rfq.html
+++ b/erpnext/templates/pages/rfq.html
@@ -86,7 +86,7 @@
{{d.transaction_date}}