From a16b24d50a7580862e1610a95838de2545bd1a58 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:06:42 +0530 Subject: [PATCH 01/19] feat: add enable or disable invoicing and print format field --- .../membership_settings.js | 9 +++++++ .../membership_settings.json | 26 +++++++++++++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 8c0e3a4fa7..bbfece31d4 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -10,6 +10,15 @@ frappe.ui.form.on("Membership Settings", { }) }); } + + frm.set_query('print_format', function(doc) { + return { + filters: { + "doc_type": "Sales Invoice" + } + }; + }); + frm.trigger("add_generate_button"); }, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 52b9d01088..29013fafdc 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -9,7 +9,10 @@ "razorpay_settings_section", "billing_cycle", "billing_frequency", - "webhook_secret" + "webhook_secret", + "column_break_6", + "enable_auto_invoicing", + "print_format" ], "fields": [ { @@ -41,11 +44,30 @@ "fieldtype": "Password", "label": "Webhook Secret", "read_only": 1 + }, + { + "fieldname": "column_break_6", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "enable_auto_invoicing", + "fieldtype": "Check", + "label": "Enable Auto Invoicing" + }, + { + "depends_on": "eval:doc.enable_auto_invoicing", + "fieldname": "print_format", + "fieldtype": "Link", + "label": "Print Format", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", + "options": "Print Format" } ], + "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-05-22 12:38:27.103759", + "modified": "2020-07-28 11:01:40.125896", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3a1868dae813a41ee9e124ae05f20374214c35db Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:07:35 +0530 Subject: [PATCH 02/19] feat: add item field --- .../doctype/membership_type/membership_type.js | 6 +++++- .../doctype/membership_type/membership_type.json | 12 ++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 226981dc78..43311a2c96 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -5,6 +5,10 @@ frappe.ui.form.on('Membership Type', { refresh: function(frm) { frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); - }) + }); + + frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => { + if (val) frm.set_df_property('linked_item', 'hidden', false); + }); } }); diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index 319078fd6c..a163568bb9 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -8,7 +8,8 @@ "field_order": [ "membership_type", "amount", - "razorpay_plan_id" + "razorpay_plan_id", + "linked_item" ], "fields": [ { @@ -33,10 +34,17 @@ "hidden": 1, "label": "Razorpay Plan ID", "unique": 1 + }, + { + "fieldname": "linked_item", + "fieldtype": "Link", + "label": "Linked Item", + "options": "Item" } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-03-30 12:54:07.850857", + "modified": "2020-07-28 10:57:50.821375", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", From 96dc67c35edf360721c83ac4ae87e8d76d3851f0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 11:43:54 +0530 Subject: [PATCH 03/19] feat: add copy button and docs link --- .../membership_settings/membership_settings.js | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index bbfece31d4..f5e0274c3a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,7 +19,12 @@ frappe.ui.form.on("Membership Settings", { }; }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; + + frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); + frm.trigger("add_generate_button"); + frm.trigger("add_copy_buttonn"); }, add_generate_button: function(frm) { @@ -36,4 +41,12 @@ frappe.ui.form.on("Membership Settings", { }); }); }, + + add_copy_buttonn: function(frm) { + if (frm.doc.webhook_secret) { + frm.add_custom_button(__("Copy Webhook URL"), () => { + frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`); + }); + } + } }); From 811ac909f93d543cd537241fa8522393281c9bcf Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:27:37 +0530 Subject: [PATCH 04/19] feat: add auto invoice creation fields and controllers --- .../doctype/membership/membership.py | 29 ++++++++++ .../membership_settings.js | 10 ++++ .../membership_settings.json | 57 +++++++++++++++++-- 3 files changed, 91 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 729e111e57..eea18393f1 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,6 +57,35 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) + def generate_and_send_invoice(self): + if not self.paid: + frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + settings = frappe.get_doc("Membership Settings") + + invoice = make_invoice(self, member, plan, settings) + +def make_invoice(membership, member, plan, settings): + invoice = frappe.get_doc({ + 'doctype': 'Sales Invoice', + 'customer': member.customer, + 'debit_to': settings.debit_account, + 'currency': membership.currency, + 'is_pos': 0, + 'items': [ + { + 'item_code': plan.linked_item, + 'rate': membership.amount, + 'qty': 1 + } + ] + }) + + invoice.insert(ignore_permissions=True) + invoice.submit() + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index f5e0274c3a..02ef0292ea 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -19,6 +19,16 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('debit_account', function(doc) { + return { + filters: { + 'account_type': 'Receivable', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); + let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership"; frm.set_intro(__("You can learn more about memberships in the manual. ") + `${__('ERPNext Docs')}`, true); diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 29013fafdc..37bea49826 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,8 +11,15 @@ "billing_frequency", "webhook_secret", "column_break_6", + "print_format", + "company", + "debit_account", + "column_break_9", "enable_auto_invoicing", - "print_format" + "send_invoice", + "section_break_10", + "new_member", + "renewal" ], "fields": [ { @@ -47,7 +54,8 @@ }, { "fieldname": "column_break_6", - "fieldtype": "Column Break" + "fieldtype": "Section Break", + "label": "Invoicing" }, { "default": "0", @@ -56,18 +64,57 @@ "label": "Enable Auto Invoicing" }, { - "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "print_format", "fieldtype": "Link", "label": "Print Format", - "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Print Format" + }, + { + "fieldname": "new_member", + "fieldtype": "Text Editor", + "label": "Message for New Member", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "fieldname": "renewal", + "fieldtype": "Text Editor", + "label": "Message for Renewal", + "mandatory_depends_on": "eval:doc.send_invoice" + }, + { + "depends_on": "eval:doc.company", + "fieldname": "debit_account", + "fieldtype": "Link", + "label": "Debit Account", + "options": "Account" + }, + { + "depends_on": "eval:doc.send_invoice", + "fieldname": "section_break_10", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company" + }, + { + "default": "0", + "fieldname": "send_invoice", + "fieldtype": "Check", + "label": "Send Email with Invoice" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 11:01:40.125896", + "modified": "2020-07-28 12:18:35.289893", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 15340e0c7f7c21da75403a54b66b502650f04a07 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 28 Jul 2020 12:56:14 +0530 Subject: [PATCH 05/19] feat: send invoice via email --- .../doctype/membership/membership.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index eea18393f1..82b3145a07 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,6 +67,25 @@ class Membership(Document): invoice = make_invoice(self, member, plan, settings) + if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: + print("Sending") + message = settings.new_message if self.membership_status == "New" else settings.renewal + email = member.email_id if member.email_id else member.email + + email_args = { + "recipients": [email], + "message": message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ 'doctype': 'Sales Invoice', @@ -86,6 +105,8 @@ def make_invoice(membership, member, plan, settings): invoice.insert(ignore_permissions=True) invoice.submit() + return invoice + def get_member_based_on_subscription(subscription_id, email): members = frappe.get_all("Member", filters={ 'subscription_id': subscription_id, From 62e344188a2f82f70eca1910081dc6218b1655c0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 13:46:50 +0530 Subject: [PATCH 06/19] feat: update settings and link filters --- .../membership_settings.js | 10 ++- .../membership_settings.json | 83 ++++++++++++------- 2 files changed, 60 insertions(+), 33 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js index 02ef0292ea..1d894027b0 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js @@ -11,7 +11,7 @@ frappe.ui.form.on("Membership Settings", { }); } - frm.set_query('print_format', function(doc) { + frm.set_query('inv_print_format', function(doc) { return { filters: { "doc_type": "Sales Invoice" @@ -19,6 +19,14 @@ frappe.ui.form.on("Membership Settings", { }; }); + frm.set_query('membership_print_format', function(doc) { + return { + filters: { + "doc_type": "Membership" + } + }; + }); + frm.set_query('debit_account', function(doc) { return { filters: { diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 37bea49826..7eeb7fbde2 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -11,15 +11,14 @@ "billing_frequency", "webhook_secret", "column_break_6", - "print_format", + "enable_auto_invoicing", "company", "debit_account", "column_break_9", - "enable_auto_invoicing", + "send_email", "send_invoice", - "section_break_10", - "new_member", - "renewal" + "membership_print_format", + "inv_print_format" ], "fields": [ { @@ -61,60 +60,63 @@ "default": "0", "fieldname": "enable_auto_invoicing", "fieldtype": "Check", - "label": "Enable Auto Invoicing" - }, - { - "fieldname": "print_format", - "fieldtype": "Link", - "label": "Print Format", - "options": "Print Format" - }, - { - "fieldname": "new_member", - "fieldtype": "Text Editor", - "label": "Message for New Member", + "label": "Enable Auto Invoicing", "mandatory_depends_on": "eval:doc.send_invoice" }, { - "fieldname": "renewal", - "fieldtype": "Text Editor", - "label": "Message for Renewal", - "mandatory_depends_on": "eval:doc.send_invoice" - }, - { - "depends_on": "eval:doc.company", + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "debit_account", "fieldtype": "Link", "label": "Debit Account", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Account" }, - { - "depends_on": "eval:doc.send_invoice", - "fieldname": "section_break_10", - "fieldtype": "Section Break", - "hide_border": 1 - }, { "fieldname": "column_break_9", "fieldtype": "Column Break" }, { + "depends_on": "eval:doc.enable_auto_invoicing", "fieldname": "company", "fieldtype": "Link", "label": "Company", + "mandatory_depends_on": "eval:doc.enable_auto_invoicing", "options": "Company" }, { "default": "0", + "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email", "fieldname": "send_invoice", "fieldtype": "Check", - "label": "Send Email with Invoice" + "label": "Send Invoice with Email" + }, + { + "default": "0", + "fieldname": "send_email", + "fieldtype": "Check", + "label": "Send Acknowledge Email" + }, + { + "depends_on": "eval: doc.send_invoice", + "fieldname": "inv_print_format", + "fieldtype": "Link", + "label": "Invoice Print Format", + "mandatory_depends_on": "eval: doc.send_invoice", + "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "membership_print_format", + "fieldtype": "Link", + "label": "Membership Print Format", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Print Format" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-28 12:18:35.289893", + "modified": "2020-07-31 13:45:28.868235", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", @@ -129,6 +131,23 @@ "role": "System Manager", "share": 1, "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Manager", + "share": 1, + "write": 1 + }, + { + "email": 1, + "print": 1, + "read": 1, + "role": "Non Profit Member", + "share": 1 } ], "quick_entry": 1, From 1678eb9b506af6a3f4a1e20c1e91136c17493a13 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:00:22 +0530 Subject: [PATCH 07/19] feat: drop payload column --- .../doctype/membership/membership.json | 16 +++++++--------- erpnext/patches.txt | 1 + .../v13_0/drop_razorpay_payload_column.py | 7 +++++++ 3 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 erpnext/patches/v13_0/drop_razorpay_payload_column.py diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 238f4c31fd..95bb3a5d84 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -19,10 +19,10 @@ "paid", "currency", "amount", + "invoice", "razorpay_details_section", "subscription_id", - "payment_id", - "webhook_payload" + "payment_id" ], "fields": [ { @@ -118,17 +118,15 @@ "read_only": 1 }, { - "fieldname": "webhook_payload", - "fieldtype": "Code", - "hidden": 1, - "label": "Webhook Payload", - "options": "JSON", - "read_only": 1 + "fieldname": "invoice", + "fieldtype": "Link", + "label": "Invoice", + "options": "Sales Invoice" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-27 14:28:11.532696", + "modified": "2020-07-31 13:57:02.328995", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 361fe8352a..17b46ab649 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -721,3 +721,4 @@ erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes erpnext.patches.v13_0.stock_entry_enhancements erpnext.patches.v12_0.update_state_code_for_daman_and_diu erpnext.patches.v12_0.rename_lost_reason_detail +erpnext.patches.v13_0.drop_razorpay_payload_column diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py new file mode 100644 index 0000000000..62f0373008 --- /dev/null +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -0,0 +1,7 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + if frappe.db.exists("DocType", "Membership Settings"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): + frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file From c8d9e7f77bb82284eeb322290f28273a6c2d46e6 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:24 +0530 Subject: [PATCH 08/19] feat: add message field --- .../membership_settings.json | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 7eeb7fbde2..2452a763ce 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -18,7 +18,9 @@ "send_email", "send_invoice", "membership_print_format", - "inv_print_format" + "inv_print_format", + "section_break_15", + "message" ], "fields": [ { @@ -111,12 +113,24 @@ "label": "Membership Print Format", "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "section_break_15", + "fieldtype": "Section Break", + "hide_border": 1 + }, + { + "depends_on": "eval:doc.send_email", + "fieldname": "message", + "fieldtype": "Text Editor", + "label": "Message" } ], "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 13:45:28.868235", + "modified": "2020-07-31 14:30:15.701767", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 8e0314e7f684f0ae52963d4daf5a692ccd0308fa Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:31:39 +0530 Subject: [PATCH 09/19] feat: separate invoice generation and email --- .../doctype/membership/membership.py | 69 ++++++++++++++----- 1 file changed, 50 insertions(+), 19 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 82b3145a07..c960daa1d3 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -57,34 +57,65 @@ class Membership(Document): self.load_from_db() self.db_set('paid', 1) - def generate_and_send_invoice(self): - if not self.paid: - frappe.throw(_("The payment for this membership is not paid. To generate invoice mark the paid check")) + def generate_invoice(self, save=True): + if not (self.paid or self.currency or self.amount): + frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details")) + + if self.invoice: + frappe.throw(_("An invoice is already linked to this document")) member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") + attachments = [] + + if not member.customer: + frappe.throw(_("No customer linked to member {}", [member.name])) + + if not settings.debit_account: + frappe.throw(_("You need to set Debit Account in Membership Settings")) + + if not settings.company: + frappe.throw(_("You need to set Default Company for invoicing in Membership Settings")) invoice = make_invoice(self, member, plan, settings) + self.invoice = invoice.name - if invoice and settings.send_invoice and self.membership_status in ["New", "Current"]: - print("Sending") - message = settings.new_message if self.membership_status == "New" else settings.renewal - email = member.email_id if member.email_id else member.email + if save: + self.save() - email_args = { - "recipients": [email], - "message": message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.print_format)], - "reference_doctype": self.doctype, - "reference_name": self.name - } - if not frappe.flags.in_test: - frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) - else: - frappe.sendmail(**email_args) + return invoice + def send_acknowlement(self): + settings = frappe.get_doc("Membership Settings") + if not settings.send_email: + frappe.throw(_("You need to enable Send Acknowledge Email in Membership Settings")) + + member = frappe.get_doc("Member", self.member) + plan = frappe.get_doc("Membership Type", self.membership_type) + email = member.email_id if member.email_id else member.email + attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)] + + if self.invoice and settings.send_invoice: + attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + + email_args = { + "recipients": [email], + "message": settings.message, + "subject": _('Here is your invoice'), + "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "reference_doctype": self.doctype, + "reference_name": self.name + } + + if not frappe.flags.in_test: + frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args) + else: + frappe.sendmail(**email_args) + + def generate_and_send_invoice(self): + invoice = self.generate_invoice(False) + self.send_acknowlement() def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ From 3a67a78ece8ad5a102e0d9623e3666c607532df4 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 14:54:06 +0530 Subject: [PATCH 10/19] feat: add custom buttons for invoice and email --- .../non_profit/doctype/membership/membership.js | 16 ++++++++++++++++ .../non_profit/doctype/membership/membership.py | 1 - 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 554549a0bd..8408a6a1f0 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -8,6 +8,22 @@ frappe.ui.form.on('Membership', { }) }, + refresh: function(frm) { + !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => { + frm.call("generate_invoice", { + save: true + }).then(() => { + frm.reload_doc(); + }); + }); + + frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); + }); + }, + onload: function(frm) { frm.add_fetch('membership_type', 'amount', 'amount'); } diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index c960daa1d3..8d1e44da5d 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -67,7 +67,6 @@ class Membership(Document): member = frappe.get_doc("Member", self.member) plan = frappe.get_doc("Membership Type", self.membership_type) settings = frappe.get_doc("Membership Settings") - attachments = [] if not member.customer: frappe.throw(_("No customer linked to member {}", [member.name])) From 733bde31c9e3de279a77de361c4392fa649777ec Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Fri, 31 Jul 2020 15:06:51 +0530 Subject: [PATCH 11/19] fix: doctype name in patch --- erpnext/patches/v13_0/drop_razorpay_payload_column.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/drop_razorpay_payload_column.py b/erpnext/patches/v13_0/drop_razorpay_payload_column.py index 62f0373008..8980fd0039 100644 --- a/erpnext/patches/v13_0/drop_razorpay_payload_column.py +++ b/erpnext/patches/v13_0/drop_razorpay_payload_column.py @@ -2,6 +2,6 @@ from __future__ import unicode_literals import frappe def execute(): - if frappe.db.exists("DocType", "Membership Settings"): - if 'webhook_payload' in frappe.db.get_table_columns("Membership Settings"): - frappe.db.sql("alter table `tabMembership Settings` drop column webhook_payload") \ No newline at end of file + if frappe.db.exists("DocType", "Membership"): + if 'webhook_payload' in frappe.db.get_table_columns("Membership"): + frappe.db.sql("alter table `tabMembership` drop column webhook_payload") \ No newline at end of file From 6fbe9b54041b1325284774b60e9e475f8b2208a3 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 13:58:21 +0530 Subject: [PATCH 12/19] fix: remove payload --- erpnext/non_profit/doctype/membership/membership.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 8d1e44da5d..bfc2661a8f 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -200,7 +200,6 @@ def trigger_razorpay_subscription(*args, **kwargs): "currency": "INR", "paid": 1, "payment_id": payment.id, - "webhook_payload": data_json, "from_date": datetime.fromtimestamp(subscription.current_start), "to_date": datetime.fromtimestamp(subscription.current_end), "amount": payment.amount / 100 # Convert to rupees from paise From 87ddec069e2da7d624554696eeeba7f9d7db3e56 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sat, 1 Aug 2020 14:01:41 +0530 Subject: [PATCH 13/19] refactor: try block across the function --- erpnext/non_profit/doctype/membership/membership.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index bfc2661a8f..a3a1b5b275 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -157,9 +157,7 @@ def verify_signature(data): controller.verify_signature(data, signature, key) - -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): +def make_membership_entry(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) @@ -218,6 +216,14 @@ def trigger_razorpay_subscription(*args, **kwargs): return { status: 'Success' } +@frappe.whitelist(allow_guest=True) +def trigger_razorpay_subscription(*args, **kwargs): + try: + return make_membership_entry(*args, **kwargs) + except Exception as e: + log = frappe.log_error(e, "Webhook Failed") + return { status: 'Failed' } + def notify_failure(log): try: From d7139bbd437f8dc4f58e64ae36e6733539b66952 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 12:03:01 +0530 Subject: [PATCH 14/19] fix: type error --- erpnext/non_profit/doctype/membership/membership.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index a3a1b5b275..dc243d8c75 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -182,10 +182,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) notify_failure(error_log) - return { status: 'Failed' } + return { 'status': 'Failed' } if not member: - return { status: 'Failed' } + return { 'status': 'Failed' } try: if data.event == "subscription.activated": member.customer_id = payment.customer_id @@ -212,9 +212,9 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { status: 'Failed' } + return { 'status': 'Failed' } - return { status: 'Success' } + return { 'status': 'Success' } @frappe.whitelist(allow_guest=True) def trigger_razorpay_subscription(*args, **kwargs): @@ -222,7 +222,7 @@ def trigger_razorpay_subscription(*args, **kwargs): return make_membership_entry(*args, **kwargs) except Exception as e: log = frappe.log_error(e, "Webhook Failed") - return { status: 'Failed' } + return { 'status': 'Failed' } def notify_failure(log): From 98bcedd09c510c52fe823342e849bc707d9b20e5 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Sun, 2 Aug 2020 14:50:27 +0530 Subject: [PATCH 15/19] feat: create member if not already exists --- erpnext/non_profit/doctype/member/member.py | 2 +- .../doctype/membership/membership.py | 85 +++++++++++-------- .../membership_type/membership_type.py | 2 +- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index c52082ca23..6c2da07bb7 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -78,7 +78,7 @@ def create_member(user_details): member.update({ "member_name": user_details.fullname, "email_id": user_details.email, - "pan_number": user_details.pan, + "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, "customer": create_customer(user_details) }) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index dc243d8c75..fc89396fa8 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -10,6 +10,7 @@ from datetime import datetime from frappe.model.document import Document from frappe.email import sendmail_to_system_managers from frappe.utils import add_days, add_years, nowdate, getdate, add_months, get_link_to_form +from erpnext.non_profit.doctype.member.member import create_member from frappe import _ import erpnext @@ -142,6 +143,7 @@ def get_member_based_on_subscription(subscription_id, email): 'subscription_id': subscription_id, 'email_id': email }, order_by="creation desc") + try: return frappe.get_doc("Member", members[0]['name']) except: @@ -157,14 +159,15 @@ def verify_signature(data): controller.verify_signature(data, signature, key) -def make_membership_entry(*args, **kwargs): +@frappe.whitelist(allow_guest=True) +def trigger_razorpay_subscription(*args, **kwargs): data = frappe.request.get_data(as_text=True) try: verify_signature(data) except Exception as e: - signature = frappe.request.headers.get('X-Razorpay-Signature') - log = "{0} \n\n {1} \n\n {2} \n\n {3}".format(e, frappe.get_traceback(), signature, data) - frappe.log_error(e, "Webhook Verification Error") + log = frappe.log_error(e, "Webhook Verification Error") + notify_failure(log) + return { 'status': 'Failed', 'reason': e} if isinstance(data, six.string_types): data = json.loads(data) @@ -177,34 +180,42 @@ def make_membership_entry(*args, **kwargs): payment = frappe._dict(payment) try: - data_json = json.dumps(data, indent=4, sort_keys=True) + if not data.event == "subscription.charged": + return + member = get_member_based_on_subscription(subscription.id, payment.email) - except Exception as e: - error_log = frappe.log_error(frappe.get_traceback() + '\n' + data_json , _("Membership Webhook Failed")) - notify_failure(error_log) - return { 'status': 'Failed' } + if not member: + member = create_member(frappe._dict({ + 'fullname': payment.email, + 'email': payment.email, + 'plan_id': get_plan_from_razorpay_id(subscription.plan_id) + })) - if not member: - return { 'status': 'Failed' } - try: - if data.event == "subscription.activated": + member.subscription_id = subscription.id member.customer_id = payment.customer_id - elif data.event == "subscription.charged": - membership = frappe.new_doc("Membership") - membership.update({ - "member": member.name, - "membership_status": "Current", - "membership_type": member.membership_type, - "currency": "INR", - "paid": 1, - "payment_id": payment.id, - "from_date": datetime.fromtimestamp(subscription.current_start), - "to_date": datetime.fromtimestamp(subscription.current_end), - "amount": payment.amount / 100 # Convert to rupees from paise - }) - membership.insert(ignore_permissions=True) + if subscription.notes and type(subscription.notes) == dict: + notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items()) + member.add_comment("Comment", notes) + elif subscription.notes and type(subscription.notes) == str: + member.add_comment("Comment", subscription.notes) - # Update these values anyway + + # Update Membership + membership = frappe.new_doc("Membership") + membership.update({ + "member": member.name, + "membership_status": "Current", + "membership_type": member.membership_type, + "currency": "INR", + "paid": 1, + "payment_id": payment.id, + "from_date": datetime.fromtimestamp(subscription.current_start), + "to_date": datetime.fromtimestamp(subscription.current_end), + "amount": payment.amount / 100 # Convert to rupees from paise + }) + membership.insert(ignore_permissions=True) + + # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_activated = 1 @@ -212,18 +223,10 @@ def make_membership_entry(*args, **kwargs): except Exception as e: log = frappe.log_error(e, "Error creating membership entry") notify_failure(log) - return { 'status': 'Failed' } + return { 'status': 'Failed', 'reason': e} return { 'status': 'Success' } -@frappe.whitelist(allow_guest=True) -def trigger_razorpay_subscription(*args, **kwargs): - try: - return make_membership_entry(*args, **kwargs) - except Exception as e: - log = frappe.log_error(e, "Webhook Failed") - return { 'status': 'Failed' } - def notify_failure(log): try: @@ -237,3 +240,11 @@ Administrator""".format(get_link_to_form("Error Log", log.name)) sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content) except: pass + +def get_plan_from_razorpay_id(plan_id): + plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc") + + try: + return plan[0]['name'] + except: + return None \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index ed6b549600..7fa98a9481 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -6,4 +6,4 @@ from __future__ import unicode_literals from frappe.model.document import Document class MembershipType(Document): - pass + pass \ No newline at end of file From e4c58c37c84c65f3f6eaac8ad4d2287e760df8a0 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 3 Aug 2020 14:56:01 +0530 Subject: [PATCH 16/19] feat: allow member creation via API --- erpnext/non_profit/doctype/member/member.py | 42 ++++++++++++++++--- .../membership_type/membership_type.json | 6 +-- .../membership_type/membership_type.py | 6 ++- 3 files changed, 44 insertions(+), 10 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 6c2da07bb7..797736a3db 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -9,6 +9,7 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint from frappe.integrations.utils import get_payment_gateway_controller +from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type class Member(Document): def onload(self): @@ -74,19 +75,23 @@ def get_or_create_member(user_details): return create_member(user_details) def create_member(user_details): + user_details = frappe._dict(user_details) member = frappe.new_doc("Member") member.update({ "member_name": user_details.fullname, "email_id": user_details.email, "pan_number": user_details.pan or None, "membership_type": user_details.plan_id, - "customer": create_customer(user_details) + "subscription_id": user_details.subscription_id or None }) member.insert(ignore_permissions=True) + member.customer = create_customer(user_details, member.name) + member.save(ignore_permissions=True) + return member -def create_customer(user_details): +def create_customer(user_details, member=None): customer = frappe.new_doc("Customer") customer.customer_name = user_details.fullname customer.customer_type = "Individual" @@ -107,7 +112,13 @@ def create_customer(user_details): "link_name": customer.name }) - contact.save() + if member: + contact.append("links", { + "link_doctype": "Member", + "link_name": member + }) + + contact.save(ignore_permissions=True) except frappe.DuplicateEntryError: return customer.name @@ -139,12 +150,31 @@ def create_member_subscription_order(user_details): user_details = frappe._dict(user_details) member = get_or_create_member(user_details) - if not member: - member = create_member(user_details) subscription = member.setup_subscription() member.subscription_id = subscription.get('subscription_id') member.save(ignore_permissions=True) - return subscription \ No newline at end of file + return subscription + +@frappe.whitelist(allow_guest=True) +def register_member(fullname, email, rzpay_plan_id, subscription_id, pan=None, mobile=None): + plan = get_membership_type(rzpay_plan_id) + if not plan: + raise frappe.DoesNotExistError + + member = frappe.db.exists("Member", {'email': email, 'subscription_id': subscription_id }) + if member: + return member + else: + member = create_member(dict( + fullname=fullname, + email=email, + plan_id=plan, + subscription_id=subscription_id, + pan=pan, + mobile=mobile + )) + + return member.name \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.json b/erpnext/non_profit/doctype/membership_type/membership_type.json index a163568bb9..6ce1ecde12 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.json +++ b/erpnext/non_profit/doctype/membership_type/membership_type.json @@ -39,12 +39,12 @@ "fieldname": "linked_item", "fieldtype": "Link", "label": "Linked Item", - "options": "Item" + "options": "Item", + "unique": 1 } ], - "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-28 10:57:50.821375", + "modified": "2020-08-05 15:21:43.595745", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Type", diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py index 7fa98a9481..b95b04316f 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.py +++ b/erpnext/non_profit/doctype/membership_type/membership_type.py @@ -4,6 +4,10 @@ from __future__ import unicode_literals from frappe.model.document import Document +import frappe class MembershipType(Document): - pass \ No newline at end of file + pass + +def get_membership_type(razorpay_id): + return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id}) \ No newline at end of file From b4d3666e69f8a469d358ecb2cd082c45bce9f065 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:18 +0530 Subject: [PATCH 17/19] feat: show send acknowledgement button if enabled in settings --- erpnext/non_profit/doctype/membership/membership.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 8408a6a1f0..ee8a8c0a7b 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -17,11 +17,13 @@ frappe.ui.form.on('Membership', { }); }); - frm.add_custom_button("Send Acknowledgement", () => { - frm.call("send_acknowlement").then(() => { - frm.reload_doc(); + frappe.db.get_single_value("Membership Settings", "send_email").then(val => { + if (val) frm.add_custom_button("Send Acknowledgement", () => { + frm.call("send_acknowlement").then(() => { + frm.reload_doc(); + }); }); - }); + }) }, onload: function(frm) { From 6b68eaad1ec1eb770e3cb2ae0b73fe4f43d1b219 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Wed, 5 Aug 2020 17:33:33 +0530 Subject: [PATCH 18/19] feat: use email template for membership acknowledgement --- .../doctype/membership/membership.py | 9 +++++--- .../membership_settings.json | 23 +++++++------------ 2 files changed, 14 insertions(+), 18 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index fc89396fa8..f058004ff9 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -99,11 +99,14 @@ class Membership(Document): if self.invoice and settings.send_invoice: attachments.append(frappe.attach_print("Sales Invoice", self.invoice, print_format=settings.inv_print_format)) + email_template = frappe.get_doc("Email Template", settings.email_template) + context = { "doc": self, "member": member} + email_args = { "recipients": [email], - "message": settings.message, - "subject": _('Here is your invoice'), - "attachments": [frappe.attach_print("Sales Invoice", invoice.name, print_format=settings.inv_print_format)], + "message": frappe.render_template(email_template.get("response"), context), + "subject": frappe.render_template(email_template.get("subject"), context), + "attachments": attachments, "reference_doctype": self.doctype, "reference_name": self.name } diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json index 2452a763ce..5b6bab5b0a 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json @@ -19,8 +19,7 @@ "send_invoice", "membership_print_format", "inv_print_format", - "section_break_15", - "message" + "email_template" ], "fields": [ { @@ -96,7 +95,7 @@ "default": "0", "fieldname": "send_email", "fieldtype": "Check", - "label": "Send Acknowledge Email" + "label": "Send Membership Acknowledgement" }, { "depends_on": "eval: doc.send_invoice", @@ -111,26 +110,20 @@ "fieldname": "membership_print_format", "fieldtype": "Link", "label": "Membership Print Format", - "mandatory_depends_on": "eval:doc.send_email", "options": "Print Format" }, { "depends_on": "eval:doc.send_email", - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "hide_border": 1 - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "message", - "fieldtype": "Text Editor", - "label": "Message" + "fieldname": "email_template", + "fieldtype": "Link", + "label": "Email Template", + "mandatory_depends_on": "eval:doc.send_email", + "options": "Email Template" } ], - "index_web_pages_for_search": 1, "issingle": 1, "links": [], - "modified": "2020-07-31 14:30:15.701767", + "modified": "2020-08-05 17:26:37.287395", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership Settings", From 3daa9de998e7acbbd440643258b131b863a5b363 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 6 Aug 2020 10:06:23 +0530 Subject: [PATCH 19/19] feat: add email option to field email_id --- erpnext/non_profit/doctype/member/member.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index bb73a843ee..77cdb94b3d 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -133,7 +133,8 @@ { "fieldname": "email_id", "fieldtype": "Data", - "label": "Email Address" + "label": "Email Address", + "options": "Email" }, { "fieldname": "subscription_id", @@ -176,7 +177,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-04-07 14:20:33.215700", + "modified": "2020-08-06 10:06:01.153564", "modified_by": "Administrator", "module": "Non Profit", "name": "Member",