diff --git a/frappe/desk/doctype/auto_repeat/__init__.py b/frappe/desk/doctype/auto_repeat/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat.js b/frappe/desk/doctype/auto_repeat/auto_repeat.js
new file mode 100644
index 0000000000..9781f56ef1
--- /dev/null
+++ b/frappe/desk/doctype/auto_repeat/auto_repeat.js
@@ -0,0 +1,75 @@
+// Copyright (c) 2018, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Auto Repeat', {
+ setup: function(frm) {
+ frm.fields_dict['reference_doctype'].get_query = function(doc) {
+ return {
+ query: "frappe.desk.doctype.auto_repeat.auto_repeat.auto_repeat_doctype_query"
+ };
+ };
+
+ frm.fields_dict['reference_document'].get_query = function() {
+ return {
+ filters: {
+ "docstatus": 1,
+ "auto_repeat": ''
+ }
+ };
+ };
+
+ frm.fields_dict['print_format'].get_query = function() {
+ return {
+ filters: {
+ "doc_type": frm.doc.reference_doctype
+ }
+ };
+ };
+ },
+
+ refresh: function(frm) {
+ if(frm.doc.docstatus == 1) {
+ let label = __('View {0}', [frm.doc.reference_doctype]);
+ frm.add_custom_button(__(label),
+ function() {
+ frappe.route_options = {
+ "auto_repeat": frm.doc.name,
+ };
+ frappe.set_route("List", frm.doc.reference_doctype);
+ }
+ );
+
+ if(frm.doc.status != 'Stopped') {
+ frm.add_custom_button(__("Stop"),
+ function() {
+ frm.events.stop_resume_auto_repeat(frm, "Stopped");
+ }
+ );
+ }
+
+ if(frm.doc.status == 'Stopped') {
+ frm.add_custom_button(__("Resume"),
+ function() {
+ frm.events.stop_resume_auto_repeat(frm, "Resumed");
+ }
+ );
+ }
+ }
+ },
+
+ stop_resume_auto_repeat: function(frm, status) {
+ frappe.call({
+ method: "frappe.desk.doctype.auto_repeat.auto_repeat.stop_resume_auto_repeat",
+ args: {
+ subscription: frm.doc.name,
+ status: status
+ },
+ callback: function(r) {
+ if(r.message) {
+ frm.set_value("status", r.message);
+ frm.reload_doc();
+ }
+ }
+ });
+ }
+});
\ No newline at end of file
diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat.json b/frappe/desk/doctype/auto_repeat/auto_repeat.json
new file mode 100644
index 0000000000..8939e98a26
--- /dev/null
+++ b/frappe/desk/doctype/auto_repeat/auto_repeat.json
@@ -0,0 +1,888 @@
+{
+ "allow_copy": 0,
+ "allow_guest_to_view": 0,
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "naming_series:",
+ "beta": 0,
+ "creation": "2018-03-09 11:22:31.192349",
+ "custom": 0,
+ "docstatus": 0,
+ "doctype": "DocType",
+ "document_type": "",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "fields": [
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_1",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "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": "Series",
+ "length": 0,
+ "no_copy": 0,
+ "options": "SUB-",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "reference_doctype",
+ "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": "Reference Doctype",
+ "length": 0,
+ "no_copy": 0,
+ "options": "DocType",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "reference_document",
+ "fieldtype": "Dynamic Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Reference Document",
+ "length": 0,
+ "no_copy": 1,
+ "options": "reference_doctype",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "Start Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "end_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 1,
+ "in_standard_filter": 0,
+ "label": "End Date",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "submit_on_creation",
+ "fieldtype": "Check",
+ "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": "Submit on Creation",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "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": "Disabled",
+ "length": 0,
+ "no_copy": 1,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "frequency",
+ "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": "Frequency",
+ "length": 0,
+ "no_copy": 0,
+ "options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 1,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_13",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval: in_list([\"Monthly\", \"Quarterly\", \"Yearly\"], doc.frequency)",
+ "fieldname": "repeat_on_day",
+ "fieldtype": "Int",
+ "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": "Repeat on Day",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "next_schedule_date",
+ "fieldtype": "Date",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Next Schedule Date",
+ "length": 0,
+ "no_copy": 1,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 1,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
+ "columns": 0,
+ "fieldname": "notification",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Notification",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "notify_by_email",
+ "fieldtype": "Check",
+ "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": "Notify by Email",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "eval: doc.notify_by_email",
+ "description": "To add dynamic subject, use jinja tags like\n\n
New {{ doc.doctype }} #{{ doc.name }}
",
+ "fieldname": "subject",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Subject",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "notify_by_email",
+ "fieldname": "recipients",
+ "fieldtype": "Small Text",
+ "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": "Recipients",
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "depends_on": "notify_by_email",
+ "fieldname": "print_format",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Print Format",
+ "length": 0,
+ "no_copy": 0,
+ "options": "Print Format",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
+ "columns": 0,
+ "depends_on": "eval:doc.notify_by_email",
+ "fieldname": "section_break_20",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "default": "Please find attached {{ doc.doctype }} #{{ doc.name }}",
+ "fieldname": "message",
+ "fieldtype": "Text",
+ "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": "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,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 1,
+ "columns": 0,
+ "depends_on": "eval: !doc.__islocal",
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "length": 0,
+ "no_copy": 0,
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 0,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "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": "\nDraft\nStopped\nSubmitted\nCancelled\nCompleted",
+ "permlevel": 0,
+ "precision": "",
+ "print_hide": 0,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ },
+ {
+ "allow_bulk_edit": 0,
+ "allow_on_submit": 0,
+ "bold": 0,
+ "collapsible": 0,
+ "columns": 0,
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "ignore_user_permissions": 0,
+ "ignore_xss_filter": 0,
+ "in_filter": 0,
+ "in_global_search": 0,
+ "in_list_view": 0,
+ "in_standard_filter": 0,
+ "label": "Amended From ",
+ "length": 0,
+ "no_copy": 1,
+ "options": "Auto Repeat",
+ "permlevel": 0,
+ "print_hide": 1,
+ "print_hide_if_no_value": 0,
+ "read_only": 1,
+ "remember_last_selected_value": 0,
+ "report_hide": 0,
+ "reqd": 0,
+ "search_index": 0,
+ "set_only_once": 0,
+ "translatable": 0,
+ "unique": 0
+ }
+ ],
+ "has_web_view": 0,
+ "hide_heading": 0,
+ "hide_toolbar": 0,
+ "idx": 0,
+ "image_view": 0,
+ "in_create": 0,
+ "is_submittable": 1,
+ "issingle": 0,
+ "istable": 0,
+ "max_attachments": 0,
+ "modified": "2018-03-10 14:45:49.645884",
+ "modified_by": "Administrator",
+ "module": "Desk",
+ "name": "Auto Repeat",
+ "name_case": "",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 0,
+ "apply_user_permissions": 0,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "if_owner": 0,
+ "import": 0,
+ "permlevel": 0,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "set_user_permissions": 0,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 0,
+ "read_only": 0,
+ "read_only_onload": 0,
+ "search_fields": "reference_document",
+ "show_name_in_global_search": 0,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "reference_document",
+ "track_changes": 1,
+ "track_seen": 0
+}
\ No newline at end of file
diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat.py b/frappe/desk/doctype/auto_repeat/auto_repeat.py
new file mode 100644
index 0000000000..db02ed7266
--- /dev/null
+++ b/frappe/desk/doctype/auto_repeat/auto_repeat.py
@@ -0,0 +1,332 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import calendar
+from frappe import _
+from frappe.desk.form import assign_to
+from frappe.utils.jinja import validate_template
+from dateutil.relativedelta import relativedelta
+from frappe.utils.user import get_system_managers
+from frappe.utils import cstr, getdate, split_emails, add_days, today, get_last_day, get_first_day
+from frappe.model.document import Document
+
+month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
+
+
+class AutoRepeat(Document):
+ def validate(self):
+ self.update_status()
+ self.validate_reference_doctype()
+ self.validate_dates()
+ self.validate_next_schedule_date()
+ self.validate_email_id()
+
+ validate_template(self.subject or "")
+ validate_template(self.message or "")
+
+ def before_submit(self):
+ if not self.next_schedule_date:
+ self.next_schedule_date = get_next_schedule_date(
+ self.start_date, self.frequency, self.repeat_on_day)
+
+ def on_submit(self):
+ self.update_auto_repeat_id()
+
+ def on_update_after_submit(self):
+ self.validate_dates()
+ self.set_next_schedule_date()
+
+ def before_cancel(self):
+ self.unlink_auto_repeat_id()
+ self.next_schedule_date = None
+
+ def unlink_auto_repeat_id(self):
+ frappe.db.sql(
+ "update `tab{0}` set auto_repeat = null where auto_repeat=%s".format(self.reference_doctype), self.name)
+
+ def validate_reference_doctype(self):
+ if not frappe.get_meta(self.reference_doctype).has_field('auto_repeat'):
+ frappe.throw(_("Add custom field Auto Repeat in the doctype {0}").format(self.reference_doctype))
+
+ def validate_dates(self):
+ if self.end_date and getdate(self.start_date) > getdate(self.end_date):
+ frappe.throw(_("End date must be greater than start date"))
+
+ def validate_next_schedule_date(self):
+ if self.repeat_on_day and self.next_schedule_date:
+ next_date = getdate(self.next_schedule_date)
+ if next_date.day != self.repeat_on_day:
+ # if the repeat day is the last day of the month (31)
+ # and the current month does not have as many days,
+ # then the last day of the current month is a valid date
+ lastday = calendar.monthrange(next_date.year, next_date.month)[1]
+ if self.repeat_on_day < lastday:
+ # the specified day of the month is not same as the day specified
+ # or the last day of the month
+ frappe.throw(_("Next Date's day and Repeat on Day of Month must be equal"))
+
+ def validate_email_id(self):
+ if self.notify_by_email:
+ if self.recipients:
+ email_list = split_emails(self.recipients.replace("\n", ""))
+
+ from frappe.utils import validate_email_add
+ for email in email_list:
+ if not validate_email_add(email):
+ frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
+ else:
+ frappe.throw(_("'Recipients' not specified"))
+
+ def set_next_schedule_date(self):
+ if self.repeat_on_day:
+ self.next_schedule_date = get_next_date(self.next_schedule_date, 0, self.repeat_on_day)
+
+ def update_auto_repeat_id(self):
+ frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)
+
+ def update_status(self, status=None):
+ self.status = {
+ '0': 'Draft',
+ '1': 'Submitted',
+ '2': 'Cancelled'
+ }[cstr(self.docstatus or 0)]
+
+ if status and status != 'Resumed':
+ self.status = status
+
+
+def get_next_schedule_date(start_date, frequency, repeat_on_day):
+ mcount = month_map.get(frequency)
+ if mcount:
+ next_date = get_next_date(start_date, mcount, repeat_on_day)
+ else:
+ days = 7 if frequency == 'Weekly' else 1
+ next_date = add_days(start_date, days)
+ return next_date
+
+
+def make_auto_repeat_entry(date=None):
+ date = date or today()
+ for data in get_auto_repeat_entries(date):
+ schedule_date = getdate(data.next_schedule_date)
+ while schedule_date <= getdate(today()):
+ create_documents(data, schedule_date)
+ schedule_date = get_next_schedule_date(
+ schedule_date, data.frequency, data.repeat_on_day)
+
+ if schedule_date and not frappe.db.get_value('Auto Repeat', data.name, 'disabled'):
+ frappe.db.set_value('Auto Repeat', data.name, 'next_schedule_date', schedule_date)
+
+
+def get_auto_repeat_entries(date):
+ return frappe.db.sql(""" select * from `tabAuto Repeat`
+ where docstatus = 1 and next_schedule_date <=%s
+ and reference_document is not null and reference_document != ''
+ and next_schedule_date <= ifnull(end_date, '2199-12-31')
+ and ifnull(disabled, 0) = 0 and status != 'Stopped' """, (date), as_dict=1)
+
+
+def create_documents(data, schedule_date):
+ try:
+ doc = make_new_document(data, schedule_date)
+ if data.notify_by_email and data.recipients:
+ print_format = data.print_format or "Standard"
+ send_notification(doc, data, print_format=print_format)
+
+ frappe.db.commit()
+ except Exception:
+ frappe.db.rollback()
+ frappe.db.begin()
+ frappe.log_error(frappe.get_traceback())
+ disable_auto_repeat(data)
+ frappe.db.commit()
+ if data.reference_document and not frappe.flags.in_test:
+ notify_error_to_user(data)
+
+
+def disable_auto_repeat(data):
+ auto_repeat = frappe.get_doc('Auto Repeat', data.name)
+ auto_repeat.db_set('disabled', 1)
+
+
+def notify_error_to_user(data):
+ party = ''
+ party_type = ''
+
+ if data.reference_doctype in ['Sales Order', 'Sales Invoice', 'Delivery Note']:
+ party_type = 'customer'
+ elif data.reference_doctype in ['Purchase Order', 'Purchase Invoice', 'Purchase Receipt']:
+ party_type = 'supplier'
+
+ if party_type:
+ party = frappe.db.get_value(data.reference_doctype, data.reference_document, party_type)
+
+ notify_errors(data.reference_document, data.reference_doctype, party, data.owner, data.name)
+
+
+def make_new_document(args, schedule_date):
+ doc = frappe.get_doc(args.reference_doctype, args.reference_document)
+ new_doc = frappe.copy_doc(doc, ignore_no_copy=False)
+ update_doc(new_doc, doc, args, schedule_date)
+ new_doc.insert(ignore_permissions=True)
+
+ if args.submit_on_creation:
+ new_doc.submit()
+
+ return new_doc
+
+
+def update_doc(new_document, reference_doc, args, schedule_date):
+ new_document.docstatus = 0
+ if new_document.meta.get_field('set_posting_time'):
+ new_document.set('set_posting_time', 1)
+
+ mcount = month_map.get(args.frequency)
+
+ if new_document.meta.get_field('auto_repeat'):
+ new_document.set('auto_repeat', args.name)
+
+ for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time',
+ 'select_print_heading', 'remarks', 'owner']:
+ if new_document.meta.get_field(fieldname):
+ new_document.set(fieldname, reference_doc.get(fieldname))
+
+ # copy item fields
+ if new_document.meta.get_field('items'):
+ for i, item in enumerate(new_document.items):
+ for fieldname in ("page_break",):
+ item.set(fieldname, reference_doc.items[i].get(fieldname))
+
+ for data in new_document.meta.fields:
+ if data.fieldtype == 'Date' and data.reqd:
+ new_document.set(data.fieldname, schedule_date)
+
+ set_auto_repeat_period(args, mcount, new_document)
+
+ new_document.run_method("on_recurring", reference_doc=reference_doc, auto_repeat_doc=args)
+
+
+def set_auto_repeat_period(args, mcount, new_document):
+ if mcount and new_document.meta.get_field('from_date') and new_document.meta.get_field('to_date'):
+ last_ref_doc = frappe.db.sql("""
+ select name, from_date, to_date
+ from `tab{0}`
+ where auto_repeat=%s and docstatus < 2
+ order by creation desc
+ limit 1
+ """.format(args.reference_doctype), args.name, as_dict=1)
+
+ if not last_ref_doc:
+ return
+
+ from_date = get_next_date(last_ref_doc[0].from_date, mcount)
+
+ if (cstr(get_first_day(last_ref_doc[0].from_date)) == cstr(last_ref_doc[0].from_date)) and \
+ (cstr(get_last_day(last_ref_doc[0].to_date)) == cstr(last_ref_doc[0].to_date)):
+ to_date = get_last_day(get_next_date(last_ref_doc[0].to_date, mcount))
+ else:
+ to_date = get_next_date(last_ref_doc[0].to_date, mcount)
+
+ new_document.set('from_date', from_date)
+ new_document.set('to_date', to_date)
+
+
+def get_next_date(dt, mcount, day=None):
+ dt = getdate(dt)
+ dt += relativedelta(months=mcount, day=day)
+
+ return dt
+
+
+def send_notification(new_rv, auto_repeat_doc, print_format='Standard'):
+ """Notify concerned persons about recurring document generation"""
+ print_format = print_format
+ subject = auto_repeat_doc.subject or ''
+ message = auto_repeat_doc.message or ''
+
+ if not auto_repeat_doc.subject:
+ subject = _("New {0}: #{1}").format(new_rv.doctype, new_rv.name)
+ elif "{" in auto_repeat_doc.subject:
+ subject = frappe.render_template(auto_repeat_doc.subject, {'doc': new_rv})
+
+ if not auto_repeat_doc.message:
+ message = _("Please find attached {0} #{1}").format(new_rv.doctype, new_rv.name)
+ elif "{" in auto_repeat_doc.message:
+ message = frappe.render_template(auto_repeat_doc.message, {'doc': new_rv})
+
+ attachments = [frappe.attach_print(new_rv.doctype, new_rv.name,
+ file_name=new_rv.name, print_format=print_format)]
+
+ frappe.sendmail(auto_repeat_doc.recipients,
+ subject=subject, message=message, attachments=attachments)
+
+
+def notify_errors(doc, doctype, party, owner, name):
+ recipients = get_system_managers(only_name=True)
+ frappe.sendmail(recipients + [frappe.db.get_value("User", owner, "email")],
+ subject=_("[Urgent] Error while creating recurring %s for %s" % (doctype, doc)),
+ message=frappe.get_template("templates/emails/recurring_document_failed.html").render({
+ "type": _(doctype),
+ "name": doc,
+ "party": party or "",
+ "auto_repeat": name
+ }))
+
+ assign_task_to_owner(name, "Recurring Documents Failed", recipients)
+
+
+def assign_task_to_owner(name, msg, users):
+ for d in users:
+ args = {
+ 'doctype': 'Auto Repeat',
+ 'assign_to': d,
+ 'name': name,
+ 'description': msg,
+ 'priority': 'High'
+ }
+ assign_to.add(args)
+
+
+@frappe.whitelist()
+def make_auto_repeat(doctype, docname):
+ doc = frappe.new_doc('Auto Repeat')
+
+ reference_doc = frappe.get_doc(doctype, docname)
+ doc.reference_doctype = doctype
+ doc.reference_document = docname
+ doc.start_date = reference_doc.get('posting_date') or reference_doc.get('transaction_date')
+ return doc
+
+
+@frappe.whitelist()
+def stop_resume_auto_repeat(auto_repeat, status):
+ doc = frappe.get_doc('Auto Repeat', auto_repeat)
+ frappe.msgprint(_("Auto Repeat has been {0}").format(status))
+ if status == 'Resumed':
+ doc.next_schedule_date = get_next_schedule_date(today(),
+ doc.frequency, doc.repeat_on_day)
+
+ doc.update_status(status)
+ doc.save()
+
+ return doc.status
+
+
+def auto_repeat_doctype_query(doctype, txt, searchfield, start, page_len, filters):
+ return frappe.db.sql("""select parent from `tabDocField`
+ where fieldname = 'auto_repeat'
+ and parent like %(txt)s
+ order by
+ if(locate(%(_txt)s, parent), locate(%(_txt)s, parent), 99999),
+ parent
+ limit %(start)s, %(page_len)s""".format(**{
+ 'key': searchfield,
+ }), {
+ 'txt': "%%%s%%" % txt,
+ '_txt': txt.replace("%", ""),
+ 'start': start,
+ 'page_len': page_len
+ })
diff --git a/frappe/desk/doctype/auto_repeat/auto_repeat_list.js b/frappe/desk/doctype/auto_repeat/auto_repeat_list.js
new file mode 100644
index 0000000000..295d9391b0
--- /dev/null
+++ b/frappe/desk/doctype/auto_repeat/auto_repeat_list.js
@@ -0,0 +1,16 @@
+frappe.listview_settings['Auto Repeat'] = {
+ add_fields: ["next_schedule_date"],
+ get_indicator: function(doc) {
+ if(doc.disabled) {
+ return [__("Disabled"), "red"];
+ } else if(doc.next_schedule_date >= frappe.datetime.get_today() && doc.status != 'Stopped') {
+ return [__("Active"), "green"];
+ } else if(doc.docstatus === 0) {
+ return [__("Draft"), "red", "docstatus,=,0"];
+ } else if(doc.status === 'Stopped') {
+ return [__("Stopped"), "red"];
+ } else {
+ return [__("Expired"), "darkgrey"];
+ }
+ }
+};
\ No newline at end of file
diff --git a/frappe/desk/doctype/auto_repeat/test_auto_repeat.py b/frappe/desk/doctype/auto_repeat/test_auto_repeat.py
new file mode 100644
index 0000000000..c800509d4c
--- /dev/null
+++ b/frappe/desk/doctype/auto_repeat/test_auto_repeat.py
@@ -0,0 +1,98 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2018, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import unittest
+
+import frappe
+from erpnext.accounts.report.financial_statements import get_months
+from erpnext.accounts.utils import get_fiscal_year
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
+from frappe.desk.doctype.auto_repeat.auto_repeat import make_auto_repeat_entry, disable_auto_repeat
+from frappe.utils import today, add_days, getdate
+
+
+def add_custom_fields():
+ df = dict(
+ fieldname='auto_repeat', label='Auto Repeat', fieldtype='Link', insert_after='sender',
+ options='Auto Repeat')
+ create_custom_field('ToDo', df)
+
+
+class TestAutoRepeat(unittest.TestCase):
+ def setUp(self):
+ if not frappe.db.sql('select name from `tabCustom Field` where name="auto_repeat"'):
+ add_custom_fields()
+
+ def test_daily_auto_repeat(self):
+ todo = frappe.get_doc(
+ dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
+
+ doc = make_auto_repeat(reference_document=todo.name)
+ self.assertEqual(doc.next_schedule_date, today())
+ make_auto_repeat_entry()
+ frappe.db.commit()
+
+ todo = frappe.get_doc(doc.reference_doctype, doc.reference_document)
+ self.assertEqual(todo.auto_repeat, doc.name)
+
+ new_todo = frappe.db.get_value('ToDo',
+ {'auto_repeat': doc.name, 'name': ('!=', todo.name)}, 'name')
+
+ new_todo = frappe.get_doc('ToDo', new_todo)
+
+ self.assertEqual(todo.get('description'), new_todo.get('description'))
+
+ def test_monthly_auto_repeat(self):
+ current_fiscal_year = get_fiscal_year(today(), as_dict=True)
+ start_date = current_fiscal_year.year_start_date
+ end_date = current_fiscal_year.year_end_date
+
+ todo = frappe.get_doc(
+ dict(doctype='ToDo', description='test recurring todo', assigned_by='Administrator')).insert()
+
+ self.monthly_auto_repeat('ToDo', todo.name, start_date, end_date)
+
+ def monthly_auto_repeat(self, doctype, docname, start_date, end_date):
+ doc = make_auto_repeat(
+ reference_doctype=doctype, frequency='Monthly', reference_document=docname, start_date=start_date,
+ end_date=end_date)
+
+ disable_auto_repeat(doc)
+ # doc.disabled = 1
+ # doc.save()
+ # frappe.db.commit()
+
+ make_auto_repeat_entry()
+ docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
+ self.assertEqual(len(docnames), 1)
+
+ doc = frappe.get_doc('Auto Repeat', doc.name)
+ doc.db_set('disabled', 0)
+ # doc.save()
+
+ months = get_months(getdate(start_date), getdate(today()))
+ make_auto_repeat_entry()
+
+ docnames = frappe.get_all(doc.reference_doctype, {'auto_repeat': doc.name})
+ self.assertEqual(len(docnames), months)
+
+
+def make_auto_repeat(**args):
+ args = frappe._dict(args)
+ doc = frappe.get_doc({
+ 'doctype': 'Auto Repeat',
+ 'reference_doctype': args.reference_doctype or 'ToDo',
+ 'reference_document': args.reference_document or \
+ frappe.db.get_value('ToDo', {'docstatus': 1}, 'name'),
+ 'frequency': args.frequency or 'Daily',
+ 'start_date': args.start_date or add_days(today(), -1),
+ 'end_date': args.end_date or add_days(today(), 1),
+ 'submit_on_creation': args.submit_on_creation or 0
+ }).insert(ignore_permissions=True)
+
+ if not args.do_not_submit:
+ doc.submit()
+
+ return doc
diff --git a/frappe/hooks.py b/frappe/hooks.py
index 1d02b64ac2..a24a7b7e66 100755
--- a/frappe/hooks.py
+++ b/frappe/hooks.py
@@ -141,7 +141,8 @@ scheduler_events = {
'frappe.model.utils.user_settings.sync_user_settings',
"frappe.utils.error.collect_error_snapshots",
"frappe.desk.page.backups.backups.delete_downloadable_backups",
- "frappe.limits.update_space_usage"
+ "frappe.limits.update_space_usage",
+ "frappe.desk.doctype.auto_repeat.auto_repeat.make_auto_repeat_entry",
],
"daily": [
"frappe.email.queue.clear_outbox",
diff --git a/frappe/templates/emails/recurring_document_failed.html b/frappe/templates/emails/recurring_document_failed.html
new file mode 100644
index 0000000000..ee4e024f96
--- /dev/null
+++ b/frappe/templates/emails/recurring_document_failed.html
@@ -0,0 +1,11 @@
+{{_("Recurring")}} {{ type }} {{ _("Failed")}}
+
+{{_("An error occured while creating recurring")}} {{ type }} {{ name }} {{_("for")}} {{ party }}.
+{{_("This could be because of some invalid Email Addresses in the")}} {{ type }}.
+{{_("To stop sending repetitive error notifications from the system, we have checked Disabled field in the Auto Repeat document")}} {{ auto_repeat}} {{_("for the")}} {{ type }} {{ name }}.
+{{_("Please correct the")}} {{ type }} {{_('and uncheck Disabled in the Auto Repeat document')}} {{ auto_repeat }} {{_("for automatic creating of the recurring document to continue.")}}
+
+{{_("It is necessary to take this action today itself for the above mentioned recurring")}} {{ type }}
+{{_('to be generated. If delayed, you will have to manually change the "Repeat on Day of Month" field
+of this')}} {{ type }} {{_("for generating the recurring")}} {{ type }} {{_("in the Auto Repeat document")}} {{ auto_repeat }}.
+[{{_("This email is autogenerated")}}]