Program: CWE-79
Project: CVE-2022-23059
Buggy source:
# -*- 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
from frappe import _
from datetime import timedelta
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, month_diff
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to

month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}

class AutoRepeat(Document):
	def validate(self):
		self.update_status()
		self.validate_reference_doctype()
		self.validate_submit_on_creation()
		self.validate_dates()
		self.validate_email_id()
		self.validate_auto_repeat_days()
		self.set_dates()
		self.update_auto_repeat_id()
		self.unlink_if_applicable()

		validate_template(self.subject or "")
		validate_template(self.message or "")

	def before_insert(self):
		if not frappe.flags.in_test:
			start_date = getdate(self.start_date)
			today_date = getdate(today())
			if start_date <= today_date:
				self.start_date = today_date

	def after_save(self):
		frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()

	def on_trash(self):
		frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
		frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()

	def set_dates(self):
		if self.disabled:
			self.next_schedule_date = None
		else:
			self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)

	def unlink_if_applicable(self):
		if self.status == 'Completed' or self.disabled:
			frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')

	def validate_reference_doctype(self):
		if frappe.flags.in_test or frappe.flags.in_patch:
			return
		if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
			frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))

	def validate_submit_on_creation(self):
		if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
			frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
				frappe.bold('Submit on Creation')))

	def validate_dates(self):
		if frappe.flags.in_patch:
			return

		if self.end_date:
			self.validate_from_to_dates('start_date', 'end_date')

		if self.end_date == self.start_date:
			frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date')))

	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_address

				for email in email_list:
					if not validate_email_address(email):
						frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
			else:
				frappe.throw(_("'Recipients' not specified"))

	def validate_auto_repeat_days(self):
		auto_repeat_days = self.get_auto_repeat_days()
		if not len(set(auto_repeat_days)) == len(auto_repeat_days):
			repeated_days = get_repeated(auto_repeat_days)
			frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))

	def update_auto_repeat_id(self):
		#check if document is already on auto repeat
		auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
		if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch:
			frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat))
		else:
			frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)

	def update_status(self):
		if self.disabled:
			self.status = "Disabled"
		elif self.is_completed():
			self.status = "Completed"
		else:
			self.status = "Active"

	def is_completed(self):
		return self.end_date and getdate(self.end_date) < getdate(today())

	def get_auto_repeat_schedule(self):
		schedule_details = []
		start_date = getdate(self.start_date)
		end_date = getdate(self.end_date)

		if not self.end_date:
			next_date = self.get_next_schedule_date(schedule_date=start_date)
			row = {
				"reference_document": self.reference_document,
				"frequency": self.frequency,
				"next_scheduled_date": next_date
			}
			schedule_details.append(row)

		if self.end_date:
			next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)

			while (getdate(next_date) < getdate(end_date)):
				row = {
					"reference_document" : self.reference_document,
					"frequency" : self.frequency,
					"next_scheduled_date" : next_date
				}
				schedule_details.append(row)
				next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)

		return schedule_details

	def create_documents(self):
		try:
			new_doc = self.make_new_document()
			if self.notify_by_email and self.recipients:
				self.send_notification(new_doc)
		except Exception:
			error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure"))

			self.disable_auto_repeat()

			if self.reference_document and not frappe.flags.in_test:
				self.notify_error_to_user(error_log)

	def make_new_document(self):
		reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
		new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
		self.update_doc(new_doc, reference_doc)
		new_doc.insert(ignore_permissions = True)

		if self.submit_on_creation:
			new_doc.submit()

		return new_doc

	def update_doc(self, new_doc, reference_doc):
		new_doc.docstatus = 0
		if new_doc.meta.get_field('set_posting_time'):
			new_doc.set('set_posting_time', 1)

		if new_doc.meta.get_field('auto_repeat'):
			new_doc.set('auto_repeat', self.name)

		for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
			if new_doc.meta.get_field(fieldname):
				new_doc.set(fieldname, reference_doc.get(fieldname))

		for data in new_doc.meta.fields:
			if data.fieldtype == 'Date' and data.reqd:
				new_doc.set(data.fieldname, self.next_schedule_date)

		self.set_auto_repeat_period(new_doc)

		auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name)

		#for any action that needs to take place after the recurring document creation
		#on recurring method of that doctype is triggered
		new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc)

	def set_auto_repeat_period(self, new_doc):
		mcount = month_map.get(self.frequency)
		if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'):
			last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype,
				fields = ['name', 'from_date', 'to_date'],
				filters = [
					['auto_repeat', '=', self.name],
					['docstatus', '<', 2],
				],
				order_by = 'creation desc',
				limit = 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_doc.set('from_date', from_date)
			new_doc.set('to_date', to_date)

	def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
		"""
			Returns the next schedule date for auto repeat after a recurring document has been created.
			Adds required offset to the schedule_date param and returns the next schedule date.

			:param schedule_date: The date when the last recurring document was created.
			:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
		"""
		if month_map.get(self.frequency):
			month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
		else:
			month_count = 0

		day_count = 0
		if month_count and self.repeat_on_last_day:
			day_count = 31
			next_date = get_next_date(self.start_date, month_count, day_count)
		elif month_count and self.repeat_on_day:
			day_count = self.repeat_on_day
			next_date = get_next_date(self.start_date, month_count, day_count)
		elif month_count:
			next_date = get_next_date(self.start_date, month_count)
		else:
			days = self.get_days(schedule_date)
			next_date = add_days(schedule_date, days)

		# next schedule date should be after or on current date
		if not for_full_schedule:
			while getdate(next_date) < getdate(today()):
				if month_count:
					month_count += month_map.get(self.frequency, 0)
					next_date = get_next_date(self.start_date, month_count, day_count)
				else:
					days = self.get_days(next_date)
					next_date = add_days(next_date, days)

		return next_date

	def get_days(self, schedule_date):
		if self.frequency == "Weekly":
			days = self.get_offset_for_weekly_frequency(schedule_date)
		else:
			# daily frequency
			days = 1

		return days

	def get_offset_for_weekly_frequency(self, schedule_date):
		# if weekdays are not set, offset is 7 from current schedule date
		if not self.repeat_on_days:
			return 7

		repeat_on_days = self.get_auto_repeat_days()
		current_schedule_day = getdate(schedule_date).weekday()
		weekdays = list(week_map.keys())

		# if repeats on more than 1 day or
		# start date's weekday is not in repeat days, then get next weekday
		# else offset is 7
		if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days:
			weekday = get_next_weekday(current_schedule_day, repeat_on_days)
			next_weekday_number = week_map.get(weekday, 0)
			# offset for upcoming weekday
			return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days
		return 7

	def get_auto_repeat_days(self):
		return [d.day for d in self.get('repeat_on_days', [])]

	def send_notification(self, new_doc):
		"""Notify concerned people about recurring document generation"""
		subject = self.subject or ''
		message = self.message or ''

		if not self.subject:
			subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
		elif "{" in self.subject:
			subject = frappe.render_template(self.subject, {'doc': new_doc})

		print_format = self.print_format or 'Standard'
		error_string = None

		try:
			attachments = [frappe.attach_print(new_doc.doctype, new_doc.name,
				file_name=new_doc.name, print_format=print_format)]

		except frappe.PermissionError:
			error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(new_doc.doctype, new_doc.name, self.name)
			error_string += "<br><br>"

			error_string += _("{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings").format(
				frappe.bold(_('Note')),
				frappe.bold(_('Allow Print for Draft'))
			)
			attachments = '[]'

		if error_string:
			message = error_string
		elif not self.message:
			message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
		elif "{" in self.message:
			message = frappe.render_template(self.message, {'doc': new_doc})

		recipients = self.recipients.split('\n')

		make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients,
			subject=subject, content=message, attachments=attachments, send_email=1)

	def fetch_linked_contacts(self):
		if self.reference_doctype and self.reference_document:
			res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
			res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
			email_ids = list(set([d.email_id for d in res]))
			if not email_ids:
				frappe.msgprint(_('No contacts linked to document'), alert=True)
			else:
				self.recipients = ', '.join(email_ids)

	def disable_auto_repeat(self):
		frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1)

	def notify_error_to_user(self, error_log):
		recipients = list(get_system_managers(only_name=True))
		recipients.append(self.owner)
		subject = _("Auto Repeat Document Creation Failed")

		form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
		auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link)

		error_log_link = frappe.utils.get_link_to_form('Error Log', error_log.name)
		error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link)

		frappe.sendmail(
			recipients=recipients,
			subject=subject,
			template="auto_repeat_fail",
			args={
				'auto_repeat_failed_for': auto_repeat_failed_for,
				'error_log_message': error_log_message
			},
			header=[subject, 'red']
		)


def get_next_date(dt, mcount, day=None):
	dt = getdate(dt)
	dt += relativedelta(months=mcount, day=day)
	return dt


def get_next_weekday(current_schedule_day, weekdays):
	days = list(week_map.keys())
	if current_schedule_day > 0:
		days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
	else:
		days = days[(current_schedule_day + 1):]

	for entry in days:
		if entry in weekdays:
			return entry


#called through hooks
def make_auto_repeat_entry():
	enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
	jobs = get_jobs()

	if not jobs or enqueued_method not in jobs[frappe.local.site]:
		date = getdate(today())
		data = get_auto_repeat_entries(date)
		frappe.enqueue(enqueued_method, data=data)


def create_repeated_entries(data):
	for d in data:
		doc = frappe.get_doc('Auto Repeat', d.name)

		current_date = getdate(today())
		schedule_date = getdate(doc.next_schedule_date)

		if schedule_date == current_date and not doc.disabled:
			doc.create_documents()
			schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
			if schedule_date and not doc.disabled:
				frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)


def get_auto_repeat_entries(date=None):
	if not date:
		date = getdate(today())
	return frappe.db.get_all('Auto Repeat', filters=[
		['next_schedule_date', '<=', date],
		['status', '=', 'Active']
	])


#called through hooks
def set_auto_repeat_as_completed():
	auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
	for entry in auto_repeat:
		doc = frappe.get_doc("Auto Repeat", entry.name)
		if doc.is_completed():
			doc.status = 'Completed'
			doc.save()


@frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
	if not start_date:
		start_date = getdate(today())
	doc = frappe.new_doc('Auto Repeat')
	doc.reference_doctype = doctype
	doc.reference_document = docname
	doc.frequency = frequency
	doc.start_date = start_date
	if end_date:
		doc.end_date = end_date
	doc.save()
	return doc

# method for reference_doctype filter
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
	res = frappe.db.get_all('Property Setter', {
		'property': 'allow_auto_repeat',
		'value': '1',
	}, ['doc_type'])
	docs = [r.doc_type for r in res]

	res = frappe.db.get_all('DocType', {
		'allow_auto_repeat': 1,
	}, ['name'])
	docs += [r.name for r in res]
	docs = set(list(docs))

	return [[d] for d in docs]

@frappe.whitelist()
def update_reference(docname, reference):
	result = ""
	try:
		frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
		result = "success"
	except Exception as e:
		result = "error"
		raise e
	return result

@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
	frappe.has_permission("Auto Repeat", "write", throw=True)
	doc = frappe.get_doc(reference_dt, reference_doc)
	subject_preview = _("Please add a subject to your email")
	msg_preview = frappe.render_template(message, {'doc': doc})
	if subject:
		subject_preview = frappe.render_template(subject, {'doc': doc})

	return {'message': msg_preview, 'subject': subject_preview}


Fixed version (ground truth):
# -*- 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
from frappe import _
from datetime import timedelta
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, month_diff
from frappe.model.document import Document
from frappe.core.doctype.communication.email import make
from frappe.utils.background_jobs import get_jobs
from frappe.automation.doctype.assignment_rule.assignment_rule import get_repeated
from frappe.contacts.doctype.contact.contact import get_contacts_linked_from
from frappe.contacts.doctype.contact.contact import get_contacts_linking_to

month_map = {'Monthly': 1, 'Quarterly': 3, 'Half-yearly': 6, 'Yearly': 12}
week_map = {'Monday': 0, 'Tuesday': 1, 'Wednesday': 2, 'Thursday': 3, 'Friday': 4, 'Saturday': 5, 'Sunday': 6}

class AutoRepeat(Document):
	def validate(self):
		self.update_status()
		self.validate_reference_doctype()
		self.validate_submit_on_creation()
		self.validate_dates()
		self.validate_email_id()
		self.validate_auto_repeat_days()
		self.set_dates()
		self.update_auto_repeat_id()
		self.unlink_if_applicable()

		validate_template(self.subject or "")
		validate_template(self.message or "")

	def before_insert(self):
		if not frappe.flags.in_test:
			start_date = getdate(self.start_date)
			today_date = getdate(today())
			if start_date <= today_date:
				self.start_date = today_date

	def after_save(self):
		frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()

	def on_trash(self):
		frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')
		frappe.get_doc(self.reference_doctype, self.reference_document).notify_update()

	def set_dates(self):
		if self.disabled:
			self.next_schedule_date = None
		else:
			self.next_schedule_date = self.get_next_schedule_date(schedule_date=self.start_date)

	def unlink_if_applicable(self):
		if self.status == 'Completed' or self.disabled:
			frappe.db.set_value(self.reference_doctype, self.reference_document, 'auto_repeat', '')

	def validate_reference_doctype(self):
		if frappe.flags.in_test or frappe.flags.in_patch:
			return
		if not frappe.get_meta(self.reference_doctype).allow_auto_repeat:
			frappe.throw(_("Enable Allow Auto Repeat for the doctype {0} in Customize Form").format(self.reference_doctype))

	def validate_submit_on_creation(self):
		if self.submit_on_creation and not frappe.get_meta(self.reference_doctype).is_submittable:
			frappe.throw(_('Cannot enable {0} for a non-submittable doctype').format(
				frappe.bold('Submit on Creation')))

	def validate_dates(self):
		if frappe.flags.in_patch:
			return

		if self.end_date:
			self.validate_from_to_dates('start_date', 'end_date')

		if self.end_date == self.start_date:
			frappe.throw(_('{0} should not be same as {1}').format(frappe.bold('End Date'), frappe.bold('Start Date')))

	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_address

				for email in email_list:
					if not validate_email_address(email):
						frappe.throw(_("{0} is an invalid email address in 'Recipients'").format(email))
			else:
				frappe.throw(_("'Recipients' not specified"))

	def validate_auto_repeat_days(self):
		auto_repeat_days = self.get_auto_repeat_days()
		if not len(set(auto_repeat_days)) == len(auto_repeat_days):
			repeated_days = get_repeated(auto_repeat_days)
			frappe.throw(_('Auto Repeat Day {0} has been repeated.').format(frappe.bold(repeated_days)))

	def update_auto_repeat_id(self):
		#check if document is already on auto repeat
		auto_repeat = frappe.db.get_value(self.reference_doctype, self.reference_document, "auto_repeat")
		if auto_repeat and auto_repeat != self.name and not frappe.flags.in_patch:
			frappe.throw(_("The {0} is already on auto repeat {1}").format(self.reference_document, auto_repeat))
		else:
			frappe.db.set_value(self.reference_doctype, self.reference_document, "auto_repeat", self.name)

	def update_status(self):
		if self.disabled:
			self.status = "Disabled"
		elif self.is_completed():
			self.status = "Completed"
		else:
			self.status = "Active"

	def is_completed(self):
		return self.end_date and getdate(self.end_date) < getdate(today())

	@frappe.whitelist()
	def get_auto_repeat_schedule(self):
		schedule_details = []
		start_date = getdate(self.start_date)
		end_date = getdate(self.end_date)

		if not self.end_date:
			next_date = self.get_next_schedule_date(schedule_date=start_date)
			row = {
				"reference_document": self.reference_document,
				"frequency": self.frequency,
				"next_scheduled_date": next_date
			}
			schedule_details.append(row)

		if self.end_date:
			next_date = self.get_next_schedule_date(schedule_date=start_date, for_full_schedule=True)

			while (getdate(next_date) < getdate(end_date)):
				row = {
					"reference_document" : self.reference_document,
					"frequency" : self.frequency,
					"next_scheduled_date" : next_date
				}
				schedule_details.append(row)
				next_date = self.get_next_schedule_date(schedule_date=next_date, for_full_schedule=True)

		return schedule_details

	def create_documents(self):
		try:
			new_doc = self.make_new_document()
			if self.notify_by_email and self.recipients:
				self.send_notification(new_doc)
		except Exception:
			error_log = frappe.log_error(frappe.get_traceback(), _("Auto Repeat Document Creation Failure"))

			self.disable_auto_repeat()

			if self.reference_document and not frappe.flags.in_test:
				self.notify_error_to_user(error_log)

	def make_new_document(self):
		reference_doc = frappe.get_doc(self.reference_doctype, self.reference_document)
		new_doc = frappe.copy_doc(reference_doc, ignore_no_copy = False)
		self.update_doc(new_doc, reference_doc)
		new_doc.insert(ignore_permissions = True)

		if self.submit_on_creation:
			new_doc.submit()

		return new_doc

	def update_doc(self, new_doc, reference_doc):
		new_doc.docstatus = 0
		if new_doc.meta.get_field('set_posting_time'):
			new_doc.set('set_posting_time', 1)

		if new_doc.meta.get_field('auto_repeat'):
			new_doc.set('auto_repeat', self.name)

		for fieldname in ['naming_series', 'ignore_pricing_rule', 'posting_time', 'select_print_heading', 'user_remark', 'remarks', 'owner']:
			if new_doc.meta.get_field(fieldname):
				new_doc.set(fieldname, reference_doc.get(fieldname))

		for data in new_doc.meta.fields:
			if data.fieldtype == 'Date' and data.reqd:
				new_doc.set(data.fieldname, self.next_schedule_date)

		self.set_auto_repeat_period(new_doc)

		auto_repeat_doc = frappe.get_doc('Auto Repeat', self.name)

		#for any action that needs to take place after the recurring document creation
		#on recurring method of that doctype is triggered
		new_doc.run_method('on_recurring', reference_doc = reference_doc, auto_repeat_doc = auto_repeat_doc)

	def set_auto_repeat_period(self, new_doc):
		mcount = month_map.get(self.frequency)
		if mcount and new_doc.meta.get_field('from_date') and new_doc.meta.get_field('to_date'):
			last_ref_doc = frappe.db.get_all(doctype = self.reference_doctype,
				fields = ['name', 'from_date', 'to_date'],
				filters = [
					['auto_repeat', '=', self.name],
					['docstatus', '<', 2],
				],
				order_by = 'creation desc',
				limit = 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_doc.set('from_date', from_date)
			new_doc.set('to_date', to_date)

	def get_next_schedule_date(self, schedule_date, for_full_schedule=False):
		"""
			Returns the next schedule date for auto repeat after a recurring document has been created.
			Adds required offset to the schedule_date param and returns the next schedule date.

			:param schedule_date: The date when the last recurring document was created.
			:param for_full_schedule: If True, returns the immediate next schedule date, else the full schedule.
		"""
		if month_map.get(self.frequency):
			month_count = month_map.get(self.frequency) + month_diff(schedule_date, self.start_date) - 1
		else:
			month_count = 0

		day_count = 0
		if month_count and self.repeat_on_last_day:
			day_count = 31
			next_date = get_next_date(self.start_date, month_count, day_count)
		elif month_count and self.repeat_on_day:
			day_count = self.repeat_on_day
			next_date = get_next_date(self.start_date, month_count, day_count)
		elif month_count:
			next_date = get_next_date(self.start_date, month_count)
		else:
			days = self.get_days(schedule_date)
			next_date = add_days(schedule_date, days)

		# next schedule date should be after or on current date
		if not for_full_schedule:
			while getdate(next_date) < getdate(today()):
				if month_count:
					month_count += month_map.get(self.frequency, 0)
					next_date = get_next_date(self.start_date, month_count, day_count)
				else:
					days = self.get_days(next_date)
					next_date = add_days(next_date, days)

		return next_date

	def get_days(self, schedule_date):
		if self.frequency == "Weekly":
			days = self.get_offset_for_weekly_frequency(schedule_date)
		else:
			# daily frequency
			days = 1

		return days

	def get_offset_for_weekly_frequency(self, schedule_date):
		# if weekdays are not set, offset is 7 from current schedule date
		if not self.repeat_on_days:
			return 7

		repeat_on_days = self.get_auto_repeat_days()
		current_schedule_day = getdate(schedule_date).weekday()
		weekdays = list(week_map.keys())

		# if repeats on more than 1 day or
		# start date's weekday is not in repeat days, then get next weekday
		# else offset is 7
		if len(repeat_on_days) > 1 or weekdays[current_schedule_day] not in repeat_on_days:
			weekday = get_next_weekday(current_schedule_day, repeat_on_days)
			next_weekday_number = week_map.get(weekday, 0)
			# offset for upcoming weekday
			return timedelta((7 + next_weekday_number - current_schedule_day) % 7).days
		return 7

	def get_auto_repeat_days(self):
		return [d.day for d in self.get('repeat_on_days', [])]

	def send_notification(self, new_doc):
		"""Notify concerned people about recurring document generation"""
		subject = self.subject or ''
		message = self.message or ''

		if not self.subject:
			subject = _("New {0}: {1}").format(new_doc.doctype, new_doc.name)
		elif "{" in self.subject:
			subject = frappe.render_template(self.subject, {'doc': new_doc})

		print_format = self.print_format or 'Standard'
		error_string = None

		try:
			attachments = [frappe.attach_print(new_doc.doctype, new_doc.name,
				file_name=new_doc.name, print_format=print_format)]

		except frappe.PermissionError:
			error_string = _("A recurring {0} {1} has been created for you via Auto Repeat {2}.").format(new_doc.doctype, new_doc.name, self.name)
			error_string += "<br><br>"

			error_string += _("{0}: Failed to attach new recurring document. To enable attaching document in the auto repeat notification email, enable {1} in Print Settings").format(
				frappe.bold(_('Note')),
				frappe.bold(_('Allow Print for Draft'))
			)
			attachments = '[]'

		if error_string:
			message = error_string
		elif not self.message:
			message = _("Please find attached {0}: {1}").format(new_doc.doctype, new_doc.name)
		elif "{" in self.message:
			message = frappe.render_template(self.message, {'doc': new_doc})

		recipients = self.recipients.split('\n')

		make(doctype=new_doc.doctype, name=new_doc.name, recipients=recipients,
			subject=subject, content=message, attachments=attachments, send_email=1)

	@frappe.whitelist()
	def fetch_linked_contacts(self):
		if self.reference_doctype and self.reference_document:
			res = get_contacts_linking_to(self.reference_doctype, self.reference_document, fields=['email_id'])
			res += get_contacts_linked_from(self.reference_doctype, self.reference_document, fields=['email_id'])
			email_ids = list(set([d.email_id for d in res]))
			if not email_ids:
				frappe.msgprint(_('No contacts linked to document'), alert=True)
			else:
				self.recipients = ', '.join(email_ids)

	def disable_auto_repeat(self):
		frappe.db.set_value('Auto Repeat', self.name, 'disabled', 1)

	def notify_error_to_user(self, error_log):
		recipients = list(get_system_managers(only_name=True))
		recipients.append(self.owner)
		subject = _("Auto Repeat Document Creation Failed")

		form_link = frappe.utils.get_link_to_form(self.reference_doctype, self.reference_document)
		auto_repeat_failed_for = _('Auto Repeat failed for {0}').format(form_link)

		error_log_link = frappe.utils.get_link_to_form('Error Log', error_log.name)
		error_log_message = _('Check the Error Log for more information: {0}').format(error_log_link)

		frappe.sendmail(
			recipients=recipients,
			subject=subject,
			template="auto_repeat_fail",
			args={
				'auto_repeat_failed_for': auto_repeat_failed_for,
				'error_log_message': error_log_message
			},
			header=[subject, 'red']
		)


def get_next_date(dt, mcount, day=None):
	dt = getdate(dt)
	dt += relativedelta(months=mcount, day=day)
	return dt


def get_next_weekday(current_schedule_day, weekdays):
	days = list(week_map.keys())
	if current_schedule_day > 0:
		days = days[(current_schedule_day + 1):] + days[:current_schedule_day]
	else:
		days = days[(current_schedule_day + 1):]

	for entry in days:
		if entry in weekdays:
			return entry


#called through hooks
def make_auto_repeat_entry():
	enqueued_method = 'frappe.automation.doctype.auto_repeat.auto_repeat.create_repeated_entries'
	jobs = get_jobs()

	if not jobs or enqueued_method not in jobs[frappe.local.site]:
		date = getdate(today())
		data = get_auto_repeat_entries(date)
		frappe.enqueue(enqueued_method, data=data)


def create_repeated_entries(data):
	for d in data:
		doc = frappe.get_doc('Auto Repeat', d.name)

		current_date = getdate(today())
		schedule_date = getdate(doc.next_schedule_date)

		if schedule_date == current_date and not doc.disabled:
			doc.create_documents()
			schedule_date = doc.get_next_schedule_date(schedule_date=schedule_date)
			if schedule_date and not doc.disabled:
				frappe.db.set_value('Auto Repeat', doc.name, 'next_schedule_date', schedule_date)


def get_auto_repeat_entries(date=None):
	if not date:
		date = getdate(today())
	return frappe.db.get_all('Auto Repeat', filters=[
		['next_schedule_date', '<=', date],
		['status', '=', 'Active']
	])


#called through hooks
def set_auto_repeat_as_completed():
	auto_repeat = frappe.get_all("Auto Repeat", filters = {'status': ['!=', 'Disabled']})
	for entry in auto_repeat:
		doc = frappe.get_doc("Auto Repeat", entry.name)
		if doc.is_completed():
			doc.status = 'Completed'
			doc.save()


@frappe.whitelist()
def make_auto_repeat(doctype, docname, frequency = 'Daily', start_date = None, end_date = None):
	if not start_date:
		start_date = getdate(today())
	doc = frappe.new_doc('Auto Repeat')
	doc.reference_doctype = doctype
	doc.reference_document = docname
	doc.frequency = frequency
	doc.start_date = start_date
	if end_date:
		doc.end_date = end_date
	doc.save()
	return doc

# method for reference_doctype filter
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_auto_repeat_doctypes(doctype, txt, searchfield, start, page_len, filters):
	res = frappe.db.get_all('Property Setter', {
		'property': 'allow_auto_repeat',
		'value': '1',
	}, ['doc_type'])
	docs = [r.doc_type for r in res]

	res = frappe.db.get_all('DocType', {
		'allow_auto_repeat': 1,
	}, ['name'])
	docs += [r.name for r in res]
	docs = set(list(docs))

	return [[d] for d in docs]

@frappe.whitelist()
def update_reference(docname, reference):
	result = ""
	try:
		frappe.db.set_value("Auto Repeat", docname, "reference_document", reference)
		result = "success"
	except Exception as e:
		result = "error"
		raise e
	return result

@frappe.whitelist()
def generate_message_preview(reference_dt, reference_doc, message=None, subject=None):
	frappe.has_permission("Auto Repeat", "write", throw=True)
	doc = frappe.get_doc(reference_dt, reference_doc)
	subject_preview = _("Please add a subject to your email")
	msg_preview = frappe.render_template(message, {'doc': doc})
	if subject:
		subject_preview = frappe.render_template(subject, {'doc': doc})

	return {'message': msg_preview, 'subject': subject_preview}