Merge pull request #23038 from scmmishra/iff-invoicing
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
index bb73a84..77cdb94 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",
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index c52082c..797736a 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -9,6 +9,7 @@
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 @@
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,
+ "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 @@
"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 @@
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/membership.js b/erpnext/non_profit/doctype/membership/membership.js
index 554549a..ee8a8c0 100644
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ b/erpnext/non_profit/doctype/membership/membership.js
@@ -8,6 +8,24 @@
})
},
+ refresh: function(frm) {
+ !frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
+ frm.call("generate_invoice", {
+ save: true
+ }).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) {
frm.add_fetch('membership_type', 'amount', 'amount');
}
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
index 238f4c3..95bb3a5 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/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index 729e111..f058004 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -10,6 +10,7 @@
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
@@ -57,11 +58,95 @@
self.load_from_db()
self.db_set('paid', 1)
+ 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")
+
+ if not member.customer:
+ frappe.throw(_("No customer linked to member {}", [member.name]))
+
+ if not settings.debit_account:
+ frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings"))
+
+ if not settings.company:
+ frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
+
+ invoice = make_invoice(self, member, plan, settings)
+ self.invoice = invoice.name
+
+ if save:
+ self.save()
+
+ return invoice
+
+ def send_acknowlement(self):
+ settings = frappe.get_doc("Membership Settings")
+ if not settings.send_email:
+ frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> 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_template = frappe.get_doc("Email Template", settings.email_template)
+ context = { "doc": self, "member": member}
+
+ email_args = {
+ "recipients": [email],
+ "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
+ }
+
+ 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({
+ '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()
+
+ return invoice
+
def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={
'subscription_id': subscription_id,
'email_id': email
}, order_by="creation desc")
+
try:
return frappe.get_doc("Member", members[0]['name'])
except:
@@ -77,16 +162,15 @@
controller.verify_signature(data, signature, key)
-
@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)
@@ -99,35 +183,42 @@
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,
- "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
- })
- 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
@@ -135,9 +226,9 @@
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' }
+ return { 'status': 'Success' }
def notify_failure(log):
@@ -152,3 +243,11 @@
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_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
index 8c0e3a4..1d89402 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
@@ -10,7 +10,39 @@
})
});
}
+
+ frm.set_query('inv_print_format', function(doc) {
+ return {
+ filters: {
+ "doc_type": "Sales Invoice"
+ }
+ };
+ });
+
+ frm.set_query('membership_print_format', function(doc) {
+ return {
+ filters: {
+ "doc_type": "Membership"
+ }
+ };
+ });
+
+ 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. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
+
frm.trigger("add_generate_button");
+ frm.trigger("add_copy_buttonn");
},
add_generate_button: function(frm) {
@@ -27,4 +59,12 @@
});
});
},
+
+ 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`);
+ });
+ }
+ }
});
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
index 52b9d01..5b6bab5 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
@@ -9,7 +9,17 @@
"razorpay_settings_section",
"billing_cycle",
"billing_frequency",
- "webhook_secret"
+ "webhook_secret",
+ "column_break_6",
+ "enable_auto_invoicing",
+ "company",
+ "debit_account",
+ "column_break_9",
+ "send_email",
+ "send_invoice",
+ "membership_print_format",
+ "inv_print_format",
+ "email_template"
],
"fields": [
{
@@ -41,11 +51,79 @@
"fieldtype": "Password",
"label": "Webhook Secret",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Section Break",
+ "label": "Invoicing"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_auto_invoicing",
+ "fieldtype": "Check",
+ "label": "Enable Auto Invoicing",
+ "mandatory_depends_on": "eval:doc.send_invoice"
+ },
+ {
+ "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"
+ },
+ {
+ "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 Invoice with Email"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_email",
+ "fieldtype": "Check",
+ "label": "Send Membership Acknowledgement"
+ },
+ {
+ "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",
+ "options": "Print Format"
+ },
+ {
+ "depends_on": "eval:doc.send_email",
+ "fieldname": "email_template",
+ "fieldtype": "Link",
+ "label": "Email Template",
+ "mandatory_depends_on": "eval:doc.send_email",
+ "options": "Email Template"
}
],
"issingle": 1,
"links": [],
- "modified": "2020-05-22 12:38:27.103759",
+ "modified": "2020-08-05 17:26:37.287395",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",
@@ -60,6 +138,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,
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
index 226981d..43311a2 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 @@
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 319078f..6ce1ecd 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",
+ "unique": 1
}
],
"links": [],
- "modified": "2020-03-30 12:54:07.850857",
+ "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 ed6b549..b95b043 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
+
+def get_membership_type(razorpay_id):
+ return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a8648f5..4b9c566 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -722,4 +722,5 @@
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
erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment
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 0000000..8980fd0
--- /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"):
+ 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