diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json index 2c7363ecca..ff400703f0 100644 --- a/erpnext/buying/doctype/supplier/supplier.json +++ b/erpnext/buying/doctype/supplier/supplier.json @@ -1,244 +1,244 @@ { - "allow_import": 1, - "allow_rename": 1, - "autoname": "naming_series:", - "creation": "2013-01-10 16:34:11", - "description": "Supplier of Goods or Services.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Master", + "allow_import": 1, + "allow_rename": 1, + "autoname": "naming_series:", + "creation": "2013-01-10 16:34:11", + "description": "Supplier of Goods or Services.", + "docstatus": 0, + "doctype": "DocType", + "document_type": "Master", "fields": [ { - "fieldname": "basic_info", - "fieldtype": "Section Break", - "label": "Basic Info", - "oldfieldtype": "Section Break", - "options": "icon-user", + "fieldname": "basic_info", + "fieldtype": "Section Break", + "label": "Basic Info", + "oldfieldtype": "Section Break", + "options": "icon-user", "permlevel": 0 - }, + }, { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Series", - "no_copy": 1, - "oldfieldname": "naming_series", - "oldfieldtype": "Select", - "options": "SUPP-", + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "oldfieldname": "naming_series", + "oldfieldtype": "Select", + "options": "SUPP-", "permlevel": 0 - }, + }, { - "fieldname": "supplier_name", - "fieldtype": "Data", - "in_list_view": 0, - "label": "Supplier Name", - "no_copy": 1, - "oldfieldname": "supplier_name", - "oldfieldtype": "Data", - "permlevel": 0, + "fieldname": "supplier_name", + "fieldtype": "Data", + "in_list_view": 0, + "label": "Supplier Name", + "no_copy": 1, + "oldfieldname": "supplier_name", + "oldfieldtype": "Data", + "permlevel": 0, "reqd": 1 - }, + }, { - "fieldname": "column_break0", - "fieldtype": "Column Break", - "permlevel": 0, + "fieldname": "column_break0", + "fieldtype": "Column Break", + "permlevel": 0, "width": "50%" - }, + }, { - "fieldname": "supplier_type", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Supplier Type", - "oldfieldname": "supplier_type", - "oldfieldtype": "Link", - "options": "Supplier Type", - "permlevel": 0, + "fieldname": "supplier_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Supplier Type", + "oldfieldname": "supplier_type", + "oldfieldtype": "Link", + "options": "Supplier Type", + "permlevel": 0, "reqd": 1 - }, + }, { - "depends_on": "eval:!doc.__islocal", - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "label": "Address & Contacts", - "oldfieldtype": "Column Break", - "options": "icon-map-marker", + "depends_on": "eval:!doc.__islocal", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address & Contacts", + "oldfieldtype": "Column Break", + "options": "icon-map-marker", "permlevel": 0 - }, + }, { - "fieldname": "address_html", - "fieldtype": "HTML", - "label": "Address HTML", - "permlevel": 0, + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML", + "permlevel": 0, "read_only": 1 - }, + }, { - "fieldname": "column_break1", - "fieldtype": "Column Break", - "permlevel": 0, + "fieldname": "column_break1", + "fieldtype": "Column Break", + "permlevel": 0, "width": "50%" - }, + }, { - "fieldname": "contact_html", - "fieldtype": "HTML", - "label": "Contact HTML", - "permlevel": 0, + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML", + "permlevel": 0, "read_only": 1 - }, + }, { - "fieldname": "default_payable_accounts", - "fieldtype": "Section Break", - "label": "Default Payable Accounts", + "fieldname": "default_payable_accounts", + "fieldtype": "Section Break", + "label": "Default Payable Accounts", "permlevel": 0 - }, + }, { - "fieldname": "accounts", - "fieldtype": "Table", - "label": "Accounts", - "options": "Party Account", + "fieldname": "accounts", + "fieldtype": "Table", + "label": "Accounts", + "options": "Party Account", "permlevel": 0 - }, + }, { - "fieldname": "more_info", - "fieldtype": "Section Break", - "label": "More Info", - "oldfieldtype": "Section Break", - "options": "icon-file-text", + "fieldname": "more_info", + "fieldtype": "Section Break", + "label": "More Info", + "oldfieldtype": "Section Break", + "options": "icon-file-text", "permlevel": 0 - }, + }, { - "fieldname": "default_currency", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Default Currency", - "no_copy": 1, - "options": "Currency", + "fieldname": "default_currency", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Default Currency", + "no_copy": 1, + "options": "Currency", "permlevel": 0 - }, + }, { - "fieldname": "default_price_list", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Price List", - "options": "Price List", + "fieldname": "default_price_list", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Price List", + "options": "Price List", "permlevel": 0 - }, + }, { - "fieldname": "default_taxes_and_charges", - "fieldtype": "Link", - "ignore_user_permissions": 1, - "label": "Taxes and Charges", - "options": "Purchase Taxes and Charges Master", + "fieldname": "default_taxes_and_charges", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Taxes and Charges", + "options": "Purchase Taxes and Charges Master", "permlevel": 0 - }, + }, { - "fieldname": "credit_days", - "fieldtype": "Int", - "label": "Credit Days", + "fieldname": "credit_days", + "fieldtype": "Int", + "label": "Credit Days", "permlevel": 0 - }, + }, { - "fieldname": "column_break2", - "fieldtype": "Column Break", - "permlevel": 0, + "fieldname": "column_break2", + "fieldtype": "Column Break", + "permlevel": 0, "width": "50%" - }, + }, { - "fieldname": "website", - "fieldtype": "Data", - "label": "Website", - "oldfieldname": "website", - "oldfieldtype": "Data", + "fieldname": "website", + "fieldtype": "Data", + "label": "Website", + "oldfieldname": "website", + "oldfieldtype": "Data", "permlevel": 0 - }, + }, { - "description": "Statutory info and other general information about your Supplier", - "fieldname": "supplier_details", - "fieldtype": "Text", - "label": "Supplier Details", - "oldfieldname": "supplier_details", - "oldfieldtype": "Code", + "description": "Statutory info and other general information about your Supplier", + "fieldname": "supplier_details", + "fieldtype": "Text", + "label": "Supplier Details", + "oldfieldname": "supplier_details", + "oldfieldtype": "Code", "permlevel": 0 - }, + }, { - "fieldname": "communications", - "fieldtype": "Table", - "hidden": 1, - "label": "Communications", - "options": "Communication", - "permlevel": 0, + "fieldname": "communications", + "fieldtype": "Table", + "hidden": 1, + "label": "Communications", + "options": "Communication", + "permlevel": 0, "print_hide": 1 } - ], - "icon": "icon-user", - "idx": 1, - "modified": "2015-01-06 17:32:39.936580", - "modified_by": "Administrator", - "module": "Buying", - "name": "Supplier", - "owner": "Administrator", + ], + "icon": "icon-user", + "idx": 1, + "modified": "2015-01-20 17:35:41.423211", + "modified_by": "Administrator", + "module": "Buying", + "name": "Supplier", + "owner": "Administrator", "permissions": [ { - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, "role": "Purchase User" - }, + }, { - "amend": 0, - "create": 0, - "delete": 0, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Manager", - "submit": 0, + "amend": 0, + "create": 0, + "delete": 0, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager", + "submit": 0, "write": 0 - }, + }, { - "amend": 0, - "create": 1, - "delete": 1, - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Master Manager", - "submit": 0, + "amend": 0, + "create": 1, + "delete": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Master Manager", + "submit": 0, "write": 1 - }, + }, { - "apply_user_permissions": 1, - "permlevel": 0, - "read": 1, + "apply_user_permissions": 1, + "permlevel": 0, + "read": 1, "role": "Material User" - }, + }, { - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, "role": "Material Manager" - }, + }, { - "apply_user_permissions": 1, - "permlevel": 0, - "read": 1, + "apply_user_permissions": 1, + "permlevel": 0, + "read": 1, "role": "Accounts User" - }, + }, { - "email": 1, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, + "email": 1, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, "role": "Accounts Manager" } - ], - "search_fields": "supplier_name,supplier_type", + ], + "search_fields": "supplier_name, supplier_type", "title_field": "supplier_name" -} +} \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 54f2044e12..2499a61a8f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -55,7 +55,8 @@ scheduler_events = { "erpnext.stock.reorder_item.reorder_item", "erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.support.doctype.issue.issue.auto_close_tickets", - "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" ], "daily_long": [ "erpnext.setup.doctype.backup_manager.backup_manager.take_backups_daily" diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index b5bb7554fa..042dcd79e6 100644 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -51,8 +51,6 @@ class Employee(Document): self.update_user() self.update_user_permissions() - self.update_dob_event() - def update_user_permissions(self): frappe.permissions.add_user_permission("Employee", self.name, self.user_id) frappe.permissions.set_user_permission_if_allowed("Company", self.company, self.user_id) @@ -136,8 +134,6 @@ class Employee(Document): throw(_("User {0} is already assigned to Employee {1}").format(self.user_id, employee[0])) def validate_employee_leave_approver(self): - from erpnext.hr.doctype.leave_application.leave_application import InvalidLeaveApproverError - for l in self.get("leave_approvers")[:]: if "Leave Approver" not in frappe.get_roles(l.leave_approver): self.get("leave_approvers").remove(l) @@ -147,39 +143,6 @@ class Employee(Document): if self.reports_to == self.name: throw(_("Employee cannot report to himself.")) - def update_dob_event(self): - if self.status == "Active" and self.date_of_birth \ - and not cint(frappe.db.get_value("HR Settings", None, "stop_birthday_reminders")): - birthday_event = frappe.db.sql("""select name from `tabEvent` where repeat_on='Every Year' - and ref_type='Employee' and ref_name=%s""", self.name) - - starts_on = self.date_of_birth + " 00:00:00" - ends_on = self.date_of_birth + " 00:15:00" - - if birthday_event: - event = frappe.get_doc("Event", birthday_event[0][0]) - event.starts_on = starts_on - event.ends_on = ends_on - event.save() - else: - frappe.get_doc({ - "doctype": "Event", - "subject": _("Birthday") + ": " + self.employee_name, - "description": _("Happy Birthday!") + " " + self.employee_name, - "starts_on": starts_on, - "ends_on": ends_on, - "event_type": "Public", - "all_day": 1, - "send_reminder": 1, - "repeat_this_event": 1, - "repeat_on": "Every Year", - "ref_type": "Employee", - "ref_name": self.name - }).insert() - else: - frappe.db.sql("""delete from `tabEvent` where repeat_on='Every Year' and - ref_type='Employee' and ref_name=%s""", self.name) - def on_trash(self): delete_events(self.doctype, self.name) @@ -217,3 +180,31 @@ def update_user_permissions(doc, method): if "Employee" in [d.role for d in doc.get("user_roles")]: 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 + + from frappe.utils.user import get_enabled_system_users + users = None + + birthdays = get_employees_who_are_born_today() + + if birthdays: + if not users: + users = [u.email_id or u.name for u in get_enabled_system_users()] + + for e in birthdays: + frappe.sendmail(recipients=filter(lambda u: u not in (e.company_email, e.personal_email), users), + subject=_("Birthday Reminder for {0}").format(e.employee_name), + message=_("""Today is {0}'s birthday!""").format(e.employee_name), + reply_to=e.company_email or e.personal_email, + bulk=True) + +def get_employees_who_are_born_today(): + """Get Employee properties whose birthday is today.""" + return frappe.db.sql("""select name, personal_email, company_email, employee_name + from tabEmployee where day(date_of_birth) = day(curdate()) + and month(date_of_birth) = month(curdate()) + and status = 'Active'""", as_dict=True) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 699e439c53..8b2549dd7f 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -3,4 +3,31 @@ import frappe -test_records = frappe.get_test_records('Employee') \ No newline at end of file +import unittest +import frappe.utils + +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 = "1990" + frappe.utils.nowdate()[4:] + employee.company_email = "test@example.com" + employee.save() + + from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders + + self.assertTrue(employee.name in [e.name for e in get_employees_who_are_born_today()]) + + frappe.db.sql("delete from `tabBulk Email`") + + hr_settings = frappe.get_doc("HR Settings", "HR Settings") + hr_settings.stop_birthday_reminders = 0 + hr_settings.save() + + send_birthday_reminders() + + bulk_mails = frappe.db.sql("""select * from `tabBulk Email`""", as_dict=True) + self.assertTrue("Subject: Birthday Reminder for {0}".format(employee.employee_name) \ + in bulk_mails[0].message) + diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.py b/erpnext/hr/doctype/hr_settings/hr_settings.py index 5cafbdaaa7..89614cb9ef 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.py +++ b/erpnext/hr/doctype/hr_settings/hr_settings.py @@ -6,30 +6,10 @@ from __future__ import unicode_literals import frappe -from frappe.utils import cint - from frappe.model.document import Document class HRSettings(Document): - def validate(self): - self.update_birthday_reminders() - 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) - - def update_birthday_reminders(self): - original_stop_birthday_reminders = cint(frappe.db.get_value("HR Settings", - None, "stop_birthday_reminders")) - - # reset birthday reminders - if cint(self.stop_birthday_reminders) != original_stop_birthday_reminders: - frappe.db.sql("""delete from `tabEvent` where repeat_on='Every Year' and ref_type='Employee'""") - - if not self.stop_birthday_reminders: - for employee in frappe.db.sql_list("""select name from `tabEmployee` where status='Active' and - ifnull(date_of_birth, '')!=''"""): - frappe.get_doc("Employee", employee).update_dob_event() - - frappe.msgprint(frappe._("Updated Birthday Reminders")) \ No newline at end of file diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py index ddbf97f683..a0b2388458 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant.py @@ -11,3 +11,7 @@ from frappe.utils import extract_email_id class JobApplicant(Document): def validate(self): self.set_status() + + def set_sender(self, sender): + """Will be called by **Communication** when a Job Application is created from an incoming email.""" + self.email_id = sender diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 97216460aa..ccbf676229 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -104,3 +104,4 @@ erpnext.patches.v4_1.fix_jv_remarks erpnext.patches.v5_0.recalculate_total_amount_in_jv erpnext.patches.v5_0.remove_shopping_cart_app erpnext.patches.v5_0.update_companywise_payment_account +erpnext.patches.v5_0.remove_birthday_events diff --git a/erpnext/patches/v5_0/remove_birthday_events.py b/erpnext/patches/v5_0/remove_birthday_events.py new file mode 100644 index 0000000000..589792a04a --- /dev/null +++ b/erpnext/patches/v5_0/remove_birthday_events.py @@ -0,0 +1,6 @@ +import frappe + +def execute(): + for e in frappe.db.sql_list("""select name from tabEvent where + repeat_on='Every Year' and ref_type='Employee'"""): + frappe.delete_doc("Event", e, force=True) diff --git a/erpnext/selling/doctype/lead/lead.py b/erpnext/selling/doctype/lead/lead.py index 47d9b02a12..e0a148ef52 100644 --- a/erpnext/selling/doctype/lead/lead.py +++ b/erpnext/selling/doctype/lead/lead.py @@ -15,6 +15,10 @@ class Lead(SellingController): def get_feed(self): return '{0}: {1}'.format(_(self.status), self.lead_name) + def set_sender(self, sender): + """Will be called by **Communication** when a Lead is created from an incoming email.""" + self.email_id = sender + def onload(self): customer = frappe.db.get_value("Customer", {"lead_name": self.name}) self.get("__onload").is_customer = customer diff --git a/erpnext/setup/doctype/sales_person/sales_person.py b/erpnext/setup/doctype/sales_person/sales_person.py index ce7b572fbe..a4a1f6a0e0 100644 --- a/erpnext/setup/doctype/sales_person/sales_person.py +++ b/erpnext/setup/doctype/sales_person/sales_person.py @@ -11,7 +11,7 @@ class SalesPerson(NestedSet): nsm_parent_field = 'parent_sales_person'; def validate(self): - for d in self.get('targets'): + for d in self.get('targets') or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory.")) diff --git a/erpnext/setup/doctype/territory/territory.py b/erpnext/setup/doctype/territory/territory.py index 8eb4dcef1a..e4fe9c5d5a 100644 --- a/erpnext/setup/doctype/territory/territory.py +++ b/erpnext/setup/doctype/territory/territory.py @@ -14,7 +14,7 @@ class Territory(NestedSet): nsm_parent_field = 'parent_territory' def validate(self): - for d in self.get('targets'): + for d in self.get('targets') or []: if not flt(d.target_qty) and not flt(d.target_amount): frappe.throw(_("Either target qty or target amount is mandatory")) diff --git a/erpnext/setup/page/setup_wizard/install_fixtures.py b/erpnext/setup/page/setup_wizard/install_fixtures.py index 4bdf15eeef..531219281d 100644 --- a/erpnext/setup/page/setup_wizard/install_fixtures.py +++ b/erpnext/setup/page/setup_wizard/install_fixtures.py @@ -187,7 +187,11 @@ def install(country=None): {'doctype': 'Industry Type', 'industry': _('Telecommunications')}, {'doctype': 'Industry Type', 'industry': _('Television')}, {'doctype': 'Industry Type', 'industry': _('Transportation')}, - {'doctype': 'Industry Type', 'industry': _('Venture Capital')} + {'doctype': 'Industry Type', 'industry': _('Venture Capital')}, + + {'doctype': "Email Account", "email_id": "sales@example.com", "append_to": "Lead"}, + {'doctype': "Email Account", "email_id": "support@example.com", "append_to": "Issue"}, + {'doctype': "Email Account", "email_id": "jobs@example.com", "append_to": "Job Applicant"} ] from frappe.modules import scrub diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 2000ea059d..e322c9cd12 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -15,6 +15,10 @@ class Issue(Document): def get_portal_page(self): return "ticket" + def set_sender(self, sender): + """Will be called by **Communication** when the Issue is created from an incoming email.""" + self.raised_by = sender + def validate(self): self.update_status() self.set_lead_contact(self.raised_by) diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html index c52629fbca..8597864461 100644 --- a/erpnext/templates/generators/item_group.html +++ b/erpnext/templates/generators/item_group.html @@ -17,7 +17,7 @@ {% endfor %} {% if (items|length)==100 %} -