diff --git a/frappe/contacts/doctype/contact/contact.js b/frappe/contacts/doctype/contact/contact.js index f1a40e106f..71571c79e5 100644 --- a/frappe/contacts/doctype/contact/contact.js +++ b/frappe/contacts/doctype/contact/contact.js @@ -88,6 +88,17 @@ frappe.ui.form.on("Contact", { ); } } + + if (!frm.is_dirty()) { + frm.page.add_menu_item(__("Download vCard"), function () { + window.open( + `/api/method/frappe.contacts.doctype.contact.contact.download_vcard?contact=${encodeURIComponent( + frm.doc.name + )}`, + "_blank" + ); + }); + } }, validate: function (frm) { // clear linked customer / supplier / sales partner on saving... diff --git a/frappe/contacts/doctype/contact/contact.py b/frappe/contacts/doctype/contact/contact.py index 683d7e6537..53d9cb1794 100644 --- a/frappe/contacts/doctype/contact/contact.py +++ b/frappe/contacts/doctype/contact/contact.py @@ -3,6 +3,7 @@ import frappe from frappe import _ from frappe.contacts.address_and_contact import set_link_title +from frappe.core.doctype.access_log.access_log import make_access_log from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links from frappe.model.document import Document from frappe.model.naming import append_number_if_name_exists @@ -170,6 +171,88 @@ class Contact(Document): def _get_full_name(self) -> str: return get_full_name(self.first_name, self.middle_name, self.last_name, self.company_name) + def get_vcard(self): + from vobject import vCard + from vobject.vcard import Name + + vcard = vCard() + vcard.add("fn").value = self.full_name + + name = Name() + if self.first_name: + name.given = self.first_name + + if self.last_name: + name.family = self.last_name + + if self.middle_name: + name.additional = self.middle_name + + vcard.add("n").value = name + + if self.designation: + vcard.add("title").value = self.designation + + for row in self.email_ids: + email = vcard.add("email") + email.value = row.email_id + if row.is_primary: + email.type_param = "pref" + + for row in self.phone_nos: + tel = vcard.add("tel") + tel.value = row.phone + if row.is_primary_phone: + tel.type_param = "home" + + if row.is_primary_mobile_no: + tel.type_param = "cell" + + return vcard + + +@frappe.whitelist() +def download_vcard(contact: str): + """Download vCard for the contact""" + contact = frappe.get_doc("Contact", contact) + contact.check_permission() + + vcard = contact.get_vcard() + make_access_log(doctype="Contact", document=contact.name, file_type="vcf") + + frappe.response["filename"] = f"{contact.name}.vcf" + frappe.response["filecontent"] = vcard.serialize().encode("utf-8") + frappe.response["type"] = "binary" + + +@frappe.whitelist() +def download_vcards(contacts: str): + """Download vCard for the contact""" + import json + + from frappe.utils.data import now + + contact_ids = frappe.parse_json(contacts) + + vcards = [] + for contact_id in contact_ids: + contact = frappe.get_doc("Contact", contact_id) + contact.check_permission() + vcard = contact.get_vcard() + vcards.append(vcard.serialize()) + + make_access_log( + doctype="Contact", + filters=json.dumps([["name", "in", contact_ids]], ensure_ascii=False, indent="\t"), + file_type="vcf", + ) + + timestamp = now()[:19] # remove milliseconds + + frappe.response["filename"] = f"{timestamp} Contacts.vcf" + frappe.response["filecontent"] = "\n".join(vcards).encode("utf-8") + frappe.response["type"] = "binary" + def get_default_contact(doctype, name): """Return default contact for the given doctype, name.""" diff --git a/frappe/contacts/doctype/contact/contact_list.js b/frappe/contacts/doctype/contact/contact_list.js index 2b3cd8a062..07b4aeb570 100644 --- a/frappe/contacts/doctype/contact/contact_list.js +++ b/frappe/contacts/doctype/contact/contact_list.js @@ -1,3 +1,11 @@ frappe.listview_settings["Contact"] = { add_fields: ["image"], + onload: function (listview) { + listview.page.add_action_item(__("Download vCards"), function () { + const contacts = listview.get_checked_items(); + open_url_post("/api/method/frappe.contacts.doctype.contact.contact.download_vcards", { + contacts: contacts.map((c) => c.name), + }); + }); + }, }; diff --git a/pyproject.toml b/pyproject.toml index 67d98546e1..79c4d519b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -86,6 +86,7 @@ dependencies = [ "google-auth-oauthlib~=0.4.4", "google-auth~=1.29.0", "posthog~=3.0.1", + "vobject~=0.9.7", ] [project.urls]