Repository: frappe/frappe

Issue:
"Get Contacts" in Auto Repeat does not work
## Reproduce

1. Create a Customer
2. Create a primary Contact for this customer (with Email ID)
3. Create a Sales Invoice against this Customer and Contact
4. Create an Auto Repeat for this Sales Invoice
5. Tick the "Notify by Email" checkbox
6. Click the "Get Contacts" button

## Problem

No contact will be pulled.

## Cause 

The current code only pulls **Contacts** that link to the document to be repeated. But in this case the **Sales Invoice** links to **Contact**, not the other way round. Usually transaction data links to master data, so the current button is not very useful. It only works if the **Contact** references the **Sales Invoice**.

## Solution

Also check links that point from the repeated document to **Contact**.

`version-12-hotfix`: #12319
`develop`: #12320


Discussion:

Resolving patch:
diff --git a/frappe/automation/doctype/auto_repeat/auto_repeat.py b/frappe/automation/doctype/auto_repeat/auto_repeat.py
--- a/frappe/automation/doctype/auto_repeat/auto_repeat.py
+++ b/frappe/automation/doctype/auto_repeat/auto_repeat.py
@@ -13,6 +13,8 @@
 from frappe.model.document import Document
 from frappe.core.doctype.communication.email import make
 from frappe.utils.background_jobs import get_jobs
+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}
 
@@ -243,13 +245,8 @@ def send_notification(self, new_doc):
 
 	def fetch_linked_contacts(self):
 		if self.reference_doctype and self.reference_document:
-			res = frappe.db.get_all('Contact',
-				fields=['email_id'],
-				filters=[
-					['Dynamic Link', 'link_doctype', '=', self.reference_doctype],
-					['Dynamic Link', 'link_name', '=', 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)
diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py
--- a/frappe/contacts/doctype/contact/contact.py
+++ b/frappe/contacts/doctype/contact/contact.py
@@ -254,3 +254,27 @@ def get_contact_with_phone_number(number):
 def get_contact_name(email_id):
 	contact = frappe.get_list("Contact Email", filters={"email_id": email_id}, fields=["parent"], limit=1)
 	return contact[0].parent if contact else None
+
+def get_contacts_linking_to(doctype, docname, fields=None):
+	"""Return a list of contacts containing a link to the given document."""
+	return frappe.get_list('Contact', fields=fields, filters=[
+		['Dynamic Link', 'link_doctype', '=', doctype],
+		['Dynamic Link', 'link_name', '=', docname]
+	])
+
+def get_contacts_linked_from(doctype, docname, fields=None):
+	"""Return a list of contacts that are contained in (linked from) the given document."""
+	link_fields = frappe.get_meta(doctype).get('fields', {
+		'fieldtype': 'Link',
+		'options': 'Contact'
+	})
+	if not link_fields:
+		return []
+
+	contact_names = frappe.get_value(doctype, docname, fieldname=[f.fieldname for f in link_fields])
+	if not contact_names:
+		return []
+
+	return frappe.get_list('Contact', fields=fields, filters={
+		'name': ('in', contact_names)
+	})


Test patch: