diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 4a984704a1..3ebe6f0095 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -183,7 +183,8 @@ doc_events = { scheduler_events = { "hourly": [ - "erpnext.controllers.recurring_document.create_recurring_documents" + "erpnext.controllers.recurring_document.create_recurring_documents", + 'erpnext.hr.doctype.daily_work_summary_settings.daily_work_summary_settings.trigger_emails' ], "daily": [ "erpnext.stock.reorder_item.reorder_item", @@ -193,7 +194,8 @@ scheduler_events = { "erpnext.accounts.doctype.fiscal_year.fiscal_year.auto_create_fiscal_year", "erpnext.hr.doctype.employee.employee.send_birthday_reminders", "erpnext.projects.doctype.task.task.set_tasks_as_overdue", - "erpnext.accounts.doctype.asset.depreciation.post_depreciation_entries" + "erpnext.accounts.doctype.asset.depreciation.post_depreciation_entries", + 'erpnext.hr.doctype.daily_work_summary_settings.daily_work_summary_settings.send_summary' ] } diff --git a/erpnext/hr/doctype/daily_work_summary/__init__.py b/erpnext/hr/doctype/daily_work_summary/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.js b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.js new file mode 100644 index 0000000000..1ac173a8d3 --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.js @@ -0,0 +1,8 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Daily Work Summary', { + refresh: function(frm) { + + } +}); diff --git a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.json b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.json new file mode 100644 index 0000000000..43cef68203 --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.json @@ -0,0 +1,171 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-11-08 04:58:20.001780", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "Open", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Status", + "length": 0, + "no_copy": 0, + "options": "Open\nSent", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "email_sent_to", + "fieldtype": "Code", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Email Sent To", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 1, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 0, + "max_attachments": 0, + "modified": "2016-11-18 12:09:01.580414", + "modified_by": "Administrator", + "module": "HR", + "name": "Daily Work Summary", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 0, + "delete": 0, + "email": 0, + "export": 0, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 0, + "read": 1, + "report": 0, + "role": "Employee", + "set_user_permissions": 0, + "share": 0, + "submit": 0, + "write": 0 + }, + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 1, + "role": "HR User", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py new file mode 100644 index 0000000000..fe5fd8e561 --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary/daily_work_summary.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +from frappe import _ +from email_reply_parser import EmailReplyParser +from erpnext.hr.doctype.employee.employee import is_holiday +from frappe.utils import formatdate + +class DailyWorkSummary(Document): + def send_mails(self, settings, emails): + '''Send emails to get daily work summary to all employees''' + incoming_email_account = frappe.db.get_value('Email Account', + dict(enable_incoming=1, default_incoming=1), 'email_id') + + self.db_set('email_sent_to', '\n'.join(emails)) + frappe.sendmail(recipients = emails, message = settings.message, + subject = settings.subject, reference_doctype=self.doctype, + reference_name=self.name, reply_to = incoming_email_account) + + def send_summary(self): + '''Send summary of all replies. Called at midnight''' + message = self.get_summary_message() + + frappe.sendmail(recipients = get_employee_emails(self.company, False), + message = message, + subject = _('Daily Work Summary for {0}').format(self.company), + reference_doctype=self.doctype, reference_name=self.name) + + self.db_set('status', 'Sent') + + def get_summary_message(self): + '''Return summary of replies as HTML''' + settings = frappe.get_doc('Daily Work Summary Settings') + + replies = frappe.get_all('Communication', fields=['content', 'text_content', 'sender'], + filters=dict(reference_doctype=self.doctype, reference_name=self.name, + communication_type='Communication', sent_or_received='Received')) + + did_not_reply = self.email_sent_to.split() + + for d in replies: + if d.sender in did_not_reply: + did_not_reply.remove(d.sender) + if d.text_content: + d.content = EmailReplyParser.parse_reply(d.text_content) + + + did_not_reply = [(frappe.db.get_value("Employee", {"user_id": email}, "employee_name") or email) + for email in did_not_reply] + + return frappe.render_template(self.get_summary_template(), + dict(replies=replies, + original_message=settings.message, + title=_('Daily Work Summary for {0}'.format(formatdate(self.creation))), + did_not_reply= ', '.join(did_not_reply) or '', + did_not_reply_title = _('No replies from'))) + + def get_summary_template(self): + return ''' +
+ {{ reply.content }} +
+{% endfor %} + +{% if did_not_reply %} +{{ did_not_reply_title }}: {{ did_not_reply }}
+{% endif %} + +''' + +def get_employee_emails(company, only_working=True): + '''Returns list of Employee user ids for the given company who are working today + + :param company: Company `name`''' + employee_list = frappe.get_all('Employee', fields=['name', 'user_id'], + filters={'status': 'Active', 'company': company}) + + out = [] + for e in employee_list: + if e.user_id: + if only_working and is_holiday(e.name): + # don't add if holiday + pass + out.append(e.user_id) + + return out + + diff --git a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py new file mode 100644 index 0000000000..b8e70e2431 --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import os +import frappe +import unittest +import frappe.utils + +# test_records = frappe.get_test_records('Daily Work Summary') + +class TestDailyWorkSummary(unittest.TestCase): + def test_email_trigger(self): + settings, employees, emails = self.setup_and_prepare_test() + + for d in employees: + # check that email is sent to this employee + self.assertTrue(d.user_id in [d.recipient for d in emails + if settings.subject in d.message]) + + def test_email_trigger_failed(self): + hour = '00' + if frappe.utils.nowtime().split(':')[0]=='00': + hour = '01' + + settings, employees, emails = self.setup_and_prepare_test(hour) + + for d in employees: + # check that email is sent to this employee + self.assertFalse(d.user_id in [d.recipient for d in emails + if settings.subject in d.message]) + + def test_incoming(self): + settings, employees, emails = self.setup_and_prepare_test() + + # get test mail with message-id as in-reply-to + with open(os.path.join(os.path.dirname(__file__), "test_data", "test-reply.raw"), "r") as f: + test_mails = [f.read().replace('{{ sender }}', employees[-1].user_id)\ + .replace('{{ message_id }}', emails[-1].message_id)] + + # pull the mail + email_account = frappe.get_doc("Email Account", "_Test Email Account 1") + email_account.db_set('enable_incoming', 1) + email_account.receive(test_mails=test_mails) + + daily_work_summary = frappe.get_doc('Daily Work Summary', + frappe.get_all('Daily Work Summary')[0].name) + + summary = daily_work_summary.get_summary_message() + + self.assertTrue('I built Daily Work Summary!' in summary) + + def setup_and_prepare_test(self, hour=None): + if not hour: + hour = frappe.utils.nowtime().split(':')[0] + frappe.db.sql('delete from `tabDaily Work Summary`') + frappe.db.sql('delete from `tabEmail Queue`') + frappe.db.sql('delete from `tabCommunication`') + + # setup email to trigger at this our + settings = frappe.get_doc('Daily Work Summary Settings') + settings.companies = [] + + settings.append('companies', dict(company='_Test Company', + send_emails_at=hour + ':00')) + settings.test_subject = 'this is a subject for testing summary emails' + settings.save() + + from erpnext.hr.doctype.daily_work_summary_settings.daily_work_summary_settings \ + import trigger_emails + trigger_emails() + + # check if emails are created + employees = frappe.get_all('Employee', fields = ['user_id'], + filters=dict(company='_Test Company', status='Active')) + + emails = frappe.get_all('Email Queue', fields=['recipient', 'message', 'message_id']) + + return settings, employees, emails \ No newline at end of file diff --git a/erpnext/hr/doctype/daily_work_summary/test_data/test-reply.raw b/erpnext/hr/doctype/daily_work_summary/test_data/test-reply.raw new file mode 100644 index 0000000000..ba01bc2827 --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary/test_data/test-reply.raw @@ -0,0 +1,75 @@ +From: {{ sender }} +Content-Type: multipart/alternative; + boundary="Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361" +Message-Id: <07D687F6-10AA-4B9F-82DE-27753096164E@gmail.com> +Mime-Version: 1.0 (Mac OS X Mail 9.3 \(3124\)) +X-Smtp-Server: 73CC8281-7E8F-4B47-8324-D5DA86EEDD4F +Subject: Re: What did you work on today? +Date: Thu, 10 Nov 2016 16:04:43 +0530 +X-Universally-Unique-Identifier: A4D9669F-179C-42D8-A3D3-AA6A8C49A6F2 +References: <{{ message_id }}> +To: test_in@iwebnotes.com +In-Reply-To: <{{ message_id }}> + + +--Apple-Mail=_29597CF7-20DD-4184-B3FA-85582C5C4361 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; + charset=us-ascii + +I built Daily Work Summary! + +> On 10-Nov-2016, at 3:20 PM, FrappeOn 10-Nov-2016, at 3:20 PM, Frappe <test@erpnext.com> wrote:+ + + + +What did you work on today? + ++ +++ + +Please share what did you do today. If you reply by midnight, your response will be recorded!
+ +++ + + ++ This email was sent to rmehta@gmail.com + ++
Please share what did you do today. If you reply by midnight, your response will be recorded!
", + "fieldname": "message", + "fieldtype": "Text Editor", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 0, + "in_standard_filter": 0, + "label": "Message", + "length": 0, + "no_copy": 0, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 1, + "istable": 0, + "max_attachments": 0, + "modified": "2016-11-08 05:48:53.068957", + "modified_by": "Administrator", + "module": "HR", + "name": "Daily Work Summary Settings", + "name_case": "", + "owner": "Administrator", + "permissions": [ + { + "amend": 0, + "apply_user_permissions": 0, + "cancel": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 0, + "if_owner": 0, + "import": 0, + "is_custom": 0, + "permlevel": 0, + "print": 1, + "read": 1, + "report": 0, + "role": "HR Manager", + "set_user_permissions": 0, + "share": 1, + "submit": 0, + "write": 1 + } + ], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/daily_work_summary_settings/daily_work_summary_settings.py b/erpnext/hr/doctype/daily_work_summary_settings/daily_work_summary_settings.py new file mode 100644 index 0000000000..aea4c354ed --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary_settings/daily_work_summary_settings.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document +import frappe.utils +from frappe import _ +from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_employee_emails + +class DailyWorkSummarySettings(Document): + def validate(self): + if self.companies: + if not frappe.flags.in_test and not frappe.db.get_value('Email Account', dict(enable_incoming=1, + default_incoming=1)): + frappe.throw(_('There must be a default incoming Email Account enabled for this to work. Please setup a default incoming Email Account (POP/IMAP) and try again.')) + +def trigger_emails(): + '''Send emails to Employees of the enabled companies at the give hour asking + them what did they work on today''' + settings = frappe.get_doc('Daily Work Summary Settings') + for d in settings.companies: + # if current hour + if frappe.utils.nowtime().split(':')[0] == d.send_emails_at.split(':')[0]: + emails = get_employee_emails(d.company) + # find emails relating to a company + if emails: + daily_work_summary = frappe.get_doc(dict(doctype='Daily Work Summary', + company=d.company)).insert() + daily_work_summary.send_mails(settings, emails) + +def send_summary(): + '''Send summary to everyone''' + for d in frappe.get_all('Daily Work Summary', dict(status='Open')): + daily_work_summary = frappe.get_doc('Daily Work Summary', d.name) + daily_work_summary.send_summary() \ No newline at end of file diff --git a/erpnext/hr/doctype/daily_work_summary_settings_company/__init__.py b/erpnext/hr/doctype/daily_work_summary_settings_company/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/hr/doctype/daily_work_summary_settings_company/daily_work_summary_settings_company.json b/erpnext/hr/doctype/daily_work_summary_settings_company/daily_work_summary_settings_company.json new file mode 100644 index 0000000000..be27fa3dcb --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary_settings_company/daily_work_summary_settings_company.json @@ -0,0 +1,97 @@ +{ + "allow_copy": 0, + "allow_import": 0, + "allow_rename": 0, + "beta": 0, + "creation": "2016-11-08 05:44:02.502527", + "custom": 0, + "docstatus": 0, + "doctype": "DocType", + "document_type": "", + "editable_grid": 1, + "engine": "InnoDB", + "fields": [ + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "fieldname": "company", + "fieldtype": "Link", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Company", + "length": 0, + "no_copy": 0, + "options": "Company", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + }, + { + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": "17:00", + "fieldname": "send_emails_at", + "fieldtype": "Select", + "hidden": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_filter": 0, + "in_list_view": 1, + "in_standard_filter": 0, + "label": "Send Emails At", + "length": 0, + "no_copy": 0, + "options": "00:00\n01:00\n02:00\n03:00\n04:00\n05:00\n06:00\n07:00\n08:00\n09:00\n10:00\n11:00\n12:00\n13:00\n14:00\n15:00\n16:00\n17:00\n18:00\n19:00\n20:00\n21:00\n22:00\n23:00", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "remember_last_selected_value": 0, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "set_only_once": 0, + "unique": 0 + } + ], + "hide_heading": 0, + "hide_toolbar": 0, + "idx": 0, + "image_view": 0, + "in_create": 0, + "in_dialog": 0, + "is_submittable": 0, + "issingle": 0, + "istable": 1, + "max_attachments": 0, + "modified": "2016-11-08 05:46:09.198788", + "modified_by": "Administrator", + "module": "HR", + "name": "Daily Work Summary Settings Company", + "name_case": "", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "read_only": 0, + "read_only_onload": 0, + "sort_field": "modified", + "sort_order": "DESC", + "track_seen": 0 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/daily_work_summary_settings_company/daily_work_summary_settings_company.py b/erpnext/hr/doctype/daily_work_summary_settings_company/daily_work_summary_settings_company.py new file mode 100644 index 0000000000..cd051b4457 --- /dev/null +++ b/erpnext/hr/doctype/daily_work_summary_settings_company/daily_work_summary_settings_company.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.document import Document + +class DailyWorkSummarySettingsCompany(Document): + pass diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 2e5fb26093..e2e541bdb0 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -9,7 +9,6 @@ from frappe.model.naming import make_autoname from frappe import throw, _ import frappe.permissions from frappe.model.document import Document -from frappe.model.mapper import get_mapped_doc from erpnext.utilities.transaction_base import delete_events @@ -164,7 +163,6 @@ def get_timeline_data(doctype, name): @frappe.whitelist() def get_retirement_date(date_of_birth=None): - import datetime ret = {} if date_of_birth: try: @@ -233,3 +231,16 @@ def get_holiday_list_for_employee(employee, raise_exception=True): return holiday_list +def is_holiday(employee, date=None): + '''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''' + + holiday_list = get_holiday_list_for_employee(employee) + 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 + diff --git a/erpnext/patches/v7_1/set_prefered_contact_email.py b/erpnext/patches/v7_1/set_prefered_contact_email.py index d083811c84..3b68e22269 100644 --- a/erpnext/patches/v7_1/set_prefered_contact_email.py +++ b/erpnext/patches/v7_1/set_prefered_contact_email.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals import frappe def execute(): + frappe.reload_doctype('User') for d in frappe.get_all("Employee"): employee = frappe.get_doc("Employee", d.name) if employee.company_email: @@ -13,5 +14,4 @@ def execute(): elif employee.user_id: employee.prefered_contact_email = "User ID" employee.prefered_email = employee.user_id - - employee.db_update() \ No newline at end of file + employee.db_update() diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py index 9971baaaa1..7b71675f75 100644 --- a/erpnext/setup/install.py +++ b/erpnext/setup/install.py @@ -27,7 +27,7 @@ def check_setup_wizard_not_completed(): def set_single_defaults(): for dt in ('Accounts Settings', 'Print Settings', 'HR Settings', 'Buying Settings', - 'Selling Settings', 'Stock Settings'): + 'Selling Settings', 'Stock Settings', 'Daily Work Summary Settings'): default_values = frappe.db.sql("""select fieldname, `default` from `tabDocField` where parent=%s""", dt) if default_values: