From 0b29f87fa26ef68eaaba546a238129f32eae00a4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 11 Mar 2021 16:02:23 +0530 Subject: [PATCH] feat(Non Profit): 80G Certificates and Donations (#24848) * feat(Non Profit): 80G Certificates and Donations * fix(Membership): Generate Invoice for membership webhook only if automation is enabled (#24849) --- .../doctype/payment_entry/payment_entry.js | 26 +- .../doctype/payment_entry/payment_entry.py | 23 +- .../__init__.py | 0 .../non_profit/doctype/donation/donation.js | 26 ++ .../non_profit/doctype/donation/donation.json | 156 +++++++++ .../non_profit/doctype/donation/donation.py | 215 +++++++++++++ .../doctype/donation/donation_dashboard.py | 16 + .../doctype/donation/test_donation.py | 76 +++++ erpnext/non_profit/doctype/donor/donor.json | 9 +- erpnext/non_profit/doctype/donor/donor.py | 5 + erpnext/non_profit/doctype/member/member.js | 2 +- erpnext/non_profit/doctype/member/member.py | 11 +- .../doctype/membership/membership.js | 4 +- .../doctype/membership/membership.json | 10 +- .../doctype/membership/membership.py | 96 ++++-- .../doctype/membership/test_membership.py | 63 ++-- .../membership_settings.json | 192 ----------- .../membership_settings.py | 33 -- .../membership_type/membership_type.js | 4 +- .../doctype/non_profit_settings/__init__.py | 0 .../non_profit_settings.js} | 73 +++-- .../non_profit_settings.json | 273 ++++++++++++++++ .../non_profit_settings.py | 36 +++ .../test_non_profit_settings.py} | 2 +- .../workspace/non_profit/non_profit.json | 251 +++++++++++++++ erpnext/patches.txt | 2 + ...bership_settings_to_non_profit_settings.py | 22 ++ ...fields_for_80g_certificate_and_donation.py | 16 + .../tax_exemption_80g_certificate/__init__.py | 0 .../tax_exemption_80g_certificate.js | 67 ++++ .../tax_exemption_80g_certificate.json | 297 ++++++++++++++++++ .../tax_exemption_80g_certificate.py | 89 ++++++ .../test_tax_exemption_80g_certificate.py | 101 ++++++ .../__init__.py | 0 .../tax_exemption_80g_certificate_detail.json | 66 ++++ .../tax_exemption_80g_certificate_detail.py | 10 + erpnext/regional/india/setup.py | 26 +- .../80g_certificate_for_donation.json | 26 ++ .../80g_certificate_for_donation/__init__.py | 0 .../80g_certificate_for_membership.json | 26 ++ .../__init__.py | 0 .../operations/install_fixtures.py | 1 + 42 files changed, 2021 insertions(+), 330 deletions(-) rename erpnext/non_profit/doctype/{membership_settings => donation}/__init__.py (100%) create mode 100644 erpnext/non_profit/doctype/donation/donation.js create mode 100644 erpnext/non_profit/doctype/donation/donation.json create mode 100644 erpnext/non_profit/doctype/donation/donation.py create mode 100644 erpnext/non_profit/doctype/donation/donation_dashboard.py create mode 100644 erpnext/non_profit/doctype/donation/test_donation.py delete mode 100644 erpnext/non_profit/doctype/membership_settings/membership_settings.json delete mode 100644 erpnext/non_profit/doctype/membership_settings/membership_settings.py create mode 100644 erpnext/non_profit/doctype/non_profit_settings/__init__.py rename erpnext/non_profit/doctype/{membership_settings/membership_settings.js => non_profit_settings/non_profit_settings.js} (50%) create mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json create mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py rename erpnext/non_profit/doctype/{membership_settings/test_membership_settings.py => non_profit_settings/test_non_profit_settings.py} (79%) create mode 100644 erpnext/non_profit/workspace/non_profit/non_profit.json create mode 100644 erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py create mode 100644 erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py create mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json create mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/__init__.py create mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json create mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/__init__.py diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index f5c488d0f9..6412772073 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -92,14 +92,16 @@ frappe.ui.form.on('Payment Entry', { }); frm.set_query("reference_doctype", "references", function() { - if (frm.doc.party_type=="Customer") { + if (frm.doc.party_type == "Customer") { var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; - } else if (frm.doc.party_type=="Supplier") { + } else if (frm.doc.party_type == "Supplier") { var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; - } else if (frm.doc.party_type=="Employee") { + } else if (frm.doc.party_type == "Employee") { var doctypes = ["Expense Claim", "Journal Entry"]; - } else if (frm.doc.party_type=="Student") { + } else if (frm.doc.party_type == "Student") { var doctypes = ["Fees"]; + } else if (frm.doc.party_type == "Donor") { + var doctypes = ["Donation"]; } else { var doctypes = ["Journal Entry"]; } @@ -128,7 +130,7 @@ frappe.ui.form.on('Payment Entry', { const child = locals[cdt][cdn]; const filters = {"docstatus": 1, "company": doc.company}; const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', - 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; + 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation']; if (in_list(party_type_doctypes, child.reference_doctype)) { filters[doc.party_type.toLowerCase()] = doc.party; @@ -281,7 +283,7 @@ frappe.ui.form.on('Payment Entry', { let party_types = Object.keys(frappe.boot.party_account_types); if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){ frm.set_value("party_type", ""); - frappe.throw(__("Party can only be one of "+ party_types.join(", "))); + frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")])); } frm.set_query("party", function() { @@ -705,7 +707,8 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") ) { if(total_positive_outstanding > total_negative_outstanding) if (!frm.doc.paid_amount) @@ -748,7 +751,8 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") ) { if(total_positive_outstanding_including_order > paid_amount) { var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; @@ -905,6 +909,12 @@ frappe.ui.form.on('Payment Entry', { frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx])); return false; } + + if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") { + frappe.model.set_value(row.doctype, row.name, "reference_doctype", null); + frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx])); + return false; + } } if (row) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 31a4c8a387..203d06a41f 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -72,6 +72,7 @@ class PaymentEntry(AccountsController): self.update_outstanding_amounts() self.update_advance_paid() self.update_expense_claim() + self.update_donation() self.update_payment_schedule() self.set_status() @@ -82,6 +83,7 @@ class PaymentEntry(AccountsController): self.update_outstanding_amounts() self.update_advance_paid() self.update_expense_claim() + self.update_donation(cancel=1) self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) self.set_payment_req_status() @@ -245,6 +247,8 @@ class PaymentEntry(AccountsController): valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") elif self.party_type == "Shareholder": valid_reference_doctypes = ("Journal Entry") + elif self.party_type == "Donor": + valid_reference_doctypes = ("Donation") for d in self.get("references"): if not d.allocated_amount: @@ -614,6 +618,13 @@ class PaymentEntry(AccountsController): doc = frappe.get_doc("Expense Claim", d.reference_name) update_reimbursed_amount(doc, self.name) + def update_donation(self, cancel=0): + if self.payment_type == "Receive" and self.party_type == "Donor" and self.party: + for d in self.get("references"): + if d.reference_doctype=="Donation" and d.reference_name: + is_paid = 0 if cancel else 1 + frappe.db.set_value("Donation", d.reference_name, "paid", is_paid) + def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name self.reference_date = nowdate() @@ -913,6 +924,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre total_amount = ref_doc.get("grand_total") exchange_rate = 1 outstanding_amount = ref_doc.get("outstanding_amount") + elif reference_doctype == "Donation": + total_amount = ref_doc.get("amount") + exchange_rate = 1 elif reference_doctype == "Dunning": total_amount = ref_doc.get("dunning_amount") exchange_rate = 1 @@ -1162,8 +1176,10 @@ def set_party_type(dt): party_type = "Supplier" elif dt in ("Expense Claim", "Employee Advance"): party_type = "Employee" - elif dt in ("Fees"): + elif dt == "Fees": party_type = "Student" + elif dt == "Donation": + party_type = "Donor" return party_type def set_party_account(dt, dn, doc, party_type): @@ -1189,7 +1205,7 @@ def set_party_account_currency(dt, party_account, doc): return party_account_currency def set_payment_type(dt, doc): - if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ + if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1222,6 +1238,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total + elif dt == "Donation": + grand_total = doc.amount + outstanding_amount = doc.amount else: if party_account_currency == doc.company_currency: grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) diff --git a/erpnext/non_profit/doctype/membership_settings/__init__.py b/erpnext/non_profit/doctype/donation/__init__.py similarity index 100% rename from erpnext/non_profit/doctype/membership_settings/__init__.py rename to erpnext/non_profit/doctype/donation/__init__.py diff --git a/erpnext/non_profit/doctype/donation/donation.js b/erpnext/non_profit/doctype/donation/donation.js new file mode 100644 index 0000000000..10e8220144 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation.js @@ -0,0 +1,26 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Donation', { + refresh: function(frm) { + if (frm.doc.docstatus === 1 && !frm.doc.paid) { + frm.add_custom_button(__('Create Payment Entry'), function() { + frm.events.make_payment_entry(frm); + }); + } + }, + + make_payment_entry: function(frm) { + return frappe.call({ + method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry', + args: { + 'dt': frm.doc.doctype, + 'dn': frm.doc.name + }, + callback: function(r) { + var doc = frappe.model.sync(r.message); + frappe.set_route('Form', doc[0].doctype, doc[0].name); + } + }); + }, +}); diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json new file mode 100644 index 0000000000..6759569d54 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation.json @@ -0,0 +1,156 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2021-02-17 10:28:52.645731", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "donor", + "donor_name", + "email", + "column_break_4", + "company", + "date", + "payment_details_section", + "paid", + "amount", + "mode_of_payment", + "razorpay_payment_id", + "amended_from" + ], + "fields": [ + { + "fieldname": "donor", + "fieldtype": "Link", + "label": "Donor", + "options": "Donor", + "reqd": 1 + }, + { + "fetch_from": "donor.donor_name", + "fieldname": "donor_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Donor Name", + "read_only": 1 + }, + { + "fetch_from": "donor.email", + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Email", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "payment_details_section", + "fieldtype": "Section Break", + "label": "Payment Details" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "reqd": 1 + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, + { + "fieldname": "razorpay_payment_id", + "fieldtype": "Data", + "label": "Razorpay Payment ID", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "NPO-DTN-.YYYY.-" + }, + { + "default": "0", + "fieldname": "paid", + "fieldtype": "Check", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Paid" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Donation", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-03-11 10:53:11.269005", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Donation", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Non Profit Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "search_fields": "donor_name, email", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "donor_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py new file mode 100644 index 0000000000..e947588482 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import six +import json +from frappe.model.document import Document +from frappe import _ +from frappe.utils import getdate, flt, get_link_to_form +from frappe.email import sendmail_to_system_managers +from erpnext.non_profit.doctype.membership.membership import verify_signature + +class Donation(Document): + def validate(self): + if not self.donor or not frappe.db.exists('Donor', self.donor): + # for web forms + user_type = frappe.db.get_value('User', frappe.session.user, 'user_type') + if user_type == 'Website User': + self.create_donor_for_website_user() + else: + frappe.throw(_('Please select a Member')) + + def create_donor_for_website_user(self): + donor_name = frappe.get_value('Donor', dict(email=frappe.session.user)) + + if not donor_name: + user = frappe.get_doc('User', frappe.session.user) + donor = frappe.get_doc(dict( + doctype='Donor', + donor_type=self.get('donor_type'), + email=frappe.session.user, + member_name=user.get_fullname() + )).insert(ignore_permissions=True) + donor_name = donor.name + + if self.get('__islocal'): + self.donor = donor_name + + def on_payment_authorized(self, *args, **kwargs): + self.load_from_db() + self.create_payment_entry() + + def create_payment_entry(self): + settings = frappe.get_doc('Non Profit Settings') + if not settings.automate_donation_payment_entries: + return + + if not settings.donation_payment_account: + frappe.throw(_('You need to set Payment Account for Donation in {0}').format( + get_link_to_form('Non Profit Settings', 'Non Profit Settings'))) + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + frappe.flags.ignore_account_permission = True + pe = get_payment_entry(dt=self.doctype, dn=self.name) + frappe.flags.ignore_account_permission = False + pe.paid_from = settings.donation_debit_account + pe.paid_to = settings.donation_payment_account + pe.reference_no = self.name + pe.reference_date = getdate() + pe.flags.ignore_mandatory = True + pe.insert() + pe.submit() + + +@frappe.whitelist(allow_guest=True) +def capture_razorpay_donations(*args, **kwargs): + """ + Creates Donation from Razorpay Webhook Request Data on payment.captured event + Creates Donor from email if not found + """ + data = frappe.request.get_data(as_text=True) + + try: + verify_signature(data, endpoint='Donation') + except Exception as e: + log = frappe.log_error(e, 'Donation Webhook Verification Error') + notify_failure(log) + return { 'status': 'Failed', 'reason': e } + + if isinstance(data, six.string_types): + data = json.loads(data) + data = frappe._dict(data) + + payment = data.payload.get('payment', {}).get('entity', {}) + payment = frappe._dict(payment) + + try: + if not data.event == 'payment.captured': + return + + donor = get_donor(payment.email) + if not donor: + donor = create_donor(payment) + + donation = create_donation(donor, payment) + donation.run_method('create_payment_entry') + + except Exception as e: + message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id) + log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name)) + notify_failure(log) + return { 'status': 'Failed', 'reason': e } + + return { 'status': 'Success' } + + +def create_donation(donor, payment): + if not frappe.db.exists('Mode of Payment', payment.method): + create_mode_of_payment(payment.method) + + company = get_company_for_donations() + donation = frappe.get_doc({ + 'doctype': 'Donation', + 'company': company, + 'donor': donor.name, + 'donor_name': donor.donor_name, + 'email': donor.email, + 'date': getdate(), + 'amount': flt(payment.amount), + 'mode_of_payment': payment.method, + 'razorpay_payment_id': payment.id + }).insert(ignore_mandatory=True) + + donation.submit() + return donation + + +def get_donor(email): + donors = frappe.get_all('Donor', + filters={'email': email}, + order_by='creation desc') + + try: + return frappe.get_doc('Donor', donors[0]['name']) + except Exception: + return None + + +@frappe.whitelist() +def create_donor(payment): + donor_details = frappe._dict(payment) + donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type') + + donor = frappe.new_doc('Donor') + donor.update({ + 'donor_name': donor_details.email, + 'donor_type': donor_type, + 'email': donor_details.email, + 'contact': donor_details.contact + }) + + if donor_details.get('notes'): + donor = get_additional_notes(donor, donor_details) + + donor.insert(ignore_mandatory=True) + return donor + + +def get_company_for_donations(): + company = frappe.db.get_single_value('Non Profit Settings', 'donation_company') + if not company: + from erpnext.healthcare.setup import get_company + company = get_company() + return company + + +def get_additional_notes(donor, donor_details): + if type(donor_details.notes) == dict: + for k, v in donor_details.notes.items(): + notes = '\n'.join('{}: {}'.format(k, v)) + + # extract donor name from notes + if 'name' in k.lower(): + donor.update({ + 'donor_name': donor_details.notes.get(k) + }) + + # extract pan from notes + if 'pan' in k.lower(): + donor.update({ + 'pan_number': donor_details.notes.get(k) + }) + + donor.add_comment('Comment', notes) + + elif type(donor_details.notes) == str: + donor.add_comment('Comment', donor_details.notes) + + return donor + + +def create_mode_of_payment(method): + frappe.get_doc({ + 'doctype': 'Mode of Payment', + 'mode_of_payment': method + }).insert(ignore_mandatory=True) + + +def notify_failure(log): + try: + content = ''' + Dear System Manager, + Razorpay webhook for creating donation failed due to some reason. + Please check the error log linked below + Error Log: {0} + Regards, Administrator + '''.format(get_link_to_form('Error Log', log.name)) + + sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content) + except Exception: + pass + diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py new file mode 100644 index 0000000000..7e25c8d217 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'donation', + 'non_standard_fieldnames': { + 'Payment Entry': 'reference_name' + }, + 'transactions': [ + { + 'label': _('Payment'), + 'items': ['Payment Entry'] + } + ] + } \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py new file mode 100644 index 0000000000..c6a534dac3 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/test_donation.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from erpnext.non_profit.doctype.donation.donation import create_donation + +class TestDonation(unittest.TestCase): + def setUp(self): + create_donor_type() + settings = frappe.get_doc('Non Profit Settings') + settings.company = '_Test Company' + settings.donation_company = '_Test Company' + settings.default_donor_type = '_Test Donor' + settings.automate_donation_payment_entries = 1 + settings.donation_debit_account = 'Debtors - _TC' + settings.donation_payment_account = 'Cash - _TC' + settings.creation_user = 'Administrator' + settings.flags.ignore_permissions = True + settings.save() + + def test_payment_entry_for_donations(self): + donor = create_donor() + create_mode_of_payment() + payment = frappe._dict({ + 'amount': 100, + 'method': 'Debit Card', + 'id': 'pay_MeXAmsgeKOhq7O' + }) + donation = create_donation(donor, payment) + + self.assertTrue(donation.name) + + # Naive test to check if at all payment entry is generated + # This method is actually triggered from Payment Gateway + # In any case if details were missing, this would throw an error + donation.on_payment_authorized() + donation.reload() + + self.assertEquals(donation.paid, 1) + self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name})) + + +def create_donor_type(): + if not frappe.db.exists('Donor Type', '_Test Donor'): + frappe.get_doc({ + 'doctype': 'Donor Type', + 'donor_type': '_Test Donor' + }).insert() + + +def create_donor(): + donor = frappe.db.exists('Donor', 'donor@test.com') + if donor: + return frappe.get_doc('Donor', 'donor@test.com') + else: + return frappe.get_doc({ + 'doctype': 'Donor', + 'donor_name': '_Test Donor', + 'donor_type': '_Test Donor', + 'email': 'donor@test.com' + }).insert() + + +def create_mode_of_payment(): + if not frappe.db.exists('Mode of Payment', 'Debit Card'): + frappe.get_doc({ + 'doctype': 'Mode of Payment', + 'mode_of_payment': 'Debit Card', + 'accounts': [{ + 'company': '_Test Company', + 'default_account': 'Cash - _TC' + }] + }).insert() \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json index 96392658f1..72f24ef922 100644 --- a/erpnext/non_profit/doctype/donor/donor.json +++ b/erpnext/non_profit/doctype/donor/donor.json @@ -76,8 +76,13 @@ } ], "image_field": "image", - "links": [], - "modified": "2020-09-16 23:46:04.083274", + "links": [ + { + "link_doctype": "Donation", + "link_fieldname": "donor" + } + ], + "modified": "2021-02-17 16:36:33.470731", "modified_by": "Administrator", "module": "Non Profit", "name": "Donor", diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py index 9121d0cdfc..fb70e59575 100644 --- a/erpnext/non_profit/doctype/donor/donor.py +++ b/erpnext/non_profit/doctype/donor/donor.py @@ -11,3 +11,8 @@ class Donor(Document): """Load address and contacts in `__onload`""" load_address_and_contact(self) + def validate(self): + from frappe.utils import validate_email_address + if self.email: + validate_email_address(self.email.strip(), True) + diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js index 199dcfc04f..6b8f1b1deb 100644 --- a/erpnext/non_profit/doctype/member/member.js +++ b/erpnext/non_profit/doctype/member/member.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Member', { setup: function(frm) { - frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { + frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => { if (val && (frm.doc.subscription_id || frm.doc.customer_id)) { frm.set_df_property('razorpay_details_section', 'hidden', false); } diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 04b99f93f2..3ba2ee71c6 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from frappe.integrations.utils import get_payment_gateway_controller from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type @@ -26,9 +26,10 @@ class Member(Document): validate_email_address(email.strip(), True) def setup_subscription(self): - membership_settings = frappe.get_doc("Membership Settings") - if not membership_settings.enable_razorpay: - frappe.throw("Please enable Razorpay to setup subscription") + non_profit_settings = frappe.get_doc('Non Profit Settings') + if not non_profit_settings.enable_razorpay_for_memberships: + frappe.throw('Please check Enable Razorpay for Memberships in {0} to setup subscription').format( + get_link_to_form('Non Profit Settings', 'Non Profit Settings')) controller = get_payment_gateway_controller("Razorpay") settings = controller.get_settings({}) @@ -40,7 +41,7 @@ class Member(Document): subscription_details = { "plan_id": plan_id, - "billing_frequency": cint(membership_settings.billing_frequency), + "billing_frequency": cint(non_profit_settings.billing_frequency), "customer_notify": 1 } diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 573ac3319a..31872048a0 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Membership', { setup: function(frm) { - frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { + frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => { if (val) frm.set_df_property("razorpay_details_section", "hidden", false); }) }, @@ -26,7 +26,7 @@ frappe.ui.form.on('Membership', { }); }); - frappe.db.get_single_value("Membership Settings", "send_email").then(val => { + frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => { if (val) frm.add_custom_button("Send Acknowledgement", () => { frm.call("send_acknowlement").then(() => { frm.reload_doc(); diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 6da053f9fc..11d32f9c2b 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -10,6 +10,7 @@ "member_name", "membership_type", "column_break_3", + "company", "membership_status", "membership_validity_section", "from_date", @@ -132,11 +133,18 @@ "fieldtype": "Data", "label": "Member Name", "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-21 16:31:20.032656", + "modified": "2021-02-19 14:33:44.925122", "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 c113b80d56..191281f4ce 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import json import frappe import six +import os from datetime import datetime from frappe.model.document import Document from frappe.email import sendmail_to_system_managers @@ -58,7 +59,7 @@ class Membership(Document): else: self.from_date = nowdate() - if frappe.db.get_single_value("Membership Settings", "billing_cycle") == "Yearly": + if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly": self.to_date = add_years(self.from_date, 1) else: self.to_date = add_months(self.from_date, 1) @@ -68,9 +69,9 @@ class Membership(Document): return self.load_from_db() self.db_set("paid", 1) - settings = frappe.get_doc("Membership Settings") - if settings.enable_invoicing and settings.create_for_web_forms: - self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True) + settings = frappe.get_doc("Non Profit Settings") + if settings.allow_invoicing and settings.automate_membership_invoicing: + self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) def generate_invoice(self, save=True, with_payment_entry=False): @@ -85,7 +86,7 @@ class Membership(Document): frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) plan = frappe.get_doc("Membership Type", self.membership_type) - settings = frappe.get_doc("Membership Settings") + settings = frappe.get_doc("Non Profit Settings") self.validate_membership_type_and_settings(plan, settings) invoice = make_invoice(self, member, plan, settings) @@ -102,7 +103,7 @@ class Membership(Document): def validate_membership_type_and_settings(self, plan, settings): settings_link = get_link_to_form("Membership Type", self.membership_type) - if not settings.debit_account: + if not settings.membership_debit_account: frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) if not settings.company: @@ -113,25 +114,26 @@ class Membership(Document): get_link_to_form("Membership Type", self.membership_type))) def make_payment_entry(self, settings, invoice): - if not settings.payment_account: - frappe.throw(_("You need to set Payment Account in {0}").format( - get_link_to_form("Membership Type", self.membership_type))) + if not settings.membership_payment_account: + frappe.throw(_("You need to set Payment Account for Membership in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings"))) from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry frappe.flags.ignore_account_permission = True pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) frappe.flags.ignore_account_permission=False - pe.paid_to = settings.payment_account + pe.paid_to = settings.membership_payment_account pe.reference_no = self.name pe.reference_date = getdate() - pe.save(ignore_permissions=True) + pe.flags.ignore_mandatory = True + pe.save() pe.submit() def send_acknowlement(self): - settings = frappe.get_doc("Membership Settings") + settings = frappe.get_doc("Non Profit Settings") if not settings.send_email: frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( - get_link_to_form("Membership Settings", "Membership Settings"))) + get_link_to_form("Non Profit Settings", "Non Profit Settings"))) member = frappe.get_doc("Member", self.member) if not member.email_id: @@ -170,7 +172,7 @@ def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ "doctype": "Sales Invoice", "customer": member.customer, - "debit_to": settings.debit_account, + "debit_to": settings.membership_debit_account, "currency": membership.currency, "company": settings.company, "is_pos": 0, @@ -183,7 +185,7 @@ def make_invoice(membership, member, plan, settings): ] }) invoice.set_missing_values() - invoice.insert(ignore_permissions=True) + invoice.insert() invoice.submit() frappe.msgprint(_("Sales Invoice created successfully")) @@ -203,17 +205,18 @@ def get_member_based_on_subscription(subscription_id, email): return None -def verify_signature(data): - if frappe.flags.in_test: +def verify_signature(data, endpoint="Membership"): + if frappe.flags.in_test or os.environ.get("CI"): return True signature = frappe.request.headers.get("X-Razorpay-Signature") - settings = frappe.get_doc("Membership Settings") - key = settings.get_webhook_secret() + settings = frappe.get_doc("Non Profit Settings") + key = settings.get_webhook_secret(endpoint) controller = frappe.get_doc("Razorpay Settings") controller.verify_signature(data, signature, key) + frappe.set_user(settings.creation_user) @frappe.whitelist(allow_guest=True) @@ -222,7 +225,7 @@ def trigger_razorpay_subscription(*args, **kwargs): try: verify_signature(data) except Exception as e: - log = frappe.log_error(e, "Webhook Verification Error") + log = frappe.log_error(e, "Membership Webhook Verification Error") notify_failure(log) return { "status": "Failed", "reason": e} @@ -250,16 +253,15 @@ def trigger_razorpay_subscription(*args, **kwargs): member.subscription_id = subscription.id member.customer_id = payment.customer_id - 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) + if subscription.get("notes"): + member = get_additional_notes(member, subscription) + company = get_company_for_memberships() # Update Membership membership = frappe.new_doc("Membership") membership.update({ + "company": company, "member": member.name, "membership_status": "Current", "membership_type": member.membership_type, @@ -270,13 +272,20 @@ def trigger_razorpay_subscription(*args, **kwargs): "to_date": datetime.fromtimestamp(subscription.current_end), "amount": payment.amount / 100 # Convert to rupees from paise }) - membership.insert(ignore_permissions=True) + membership.flags.ignore_mandatory = True + membership.insert() # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_activated = 1 - member.save(ignore_permissions=True) + member.flags.ignore_mandatory = True + member.save() + + settings = frappe.get_doc("Non Profit Settings") + if settings.allow_invoicing and settings.automate_membership_invoicing: + membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) + except Exception as e: message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) @@ -286,6 +295,39 @@ def trigger_razorpay_subscription(*args, **kwargs): return { "status": "Success" } +def get_company_for_memberships(): + company = frappe.db.get_single_value("Non Profit Settings", "company") + if not company: + from erpnext.healthcare.setup import get_company + company = get_company() + return company + + +def get_additional_notes(member, subscription): + if type(subscription.notes) == dict: + for k, v in subscription.notes.items(): + notes = "\n".join("{}: {}".format(k, v)) + + # extract member name from notes + if "name" in k.lower(): + member.update({ + "member_name": subscription.notes.get(k) + }) + + # extract pan number from notes + if "pan" in k.lower(): + member.update({ + "pan_number": subscription.notes.get(k) + }) + + member.add_comment("Comment", notes) + + elif type(subscription.notes) == str: + member.add_comment("Comment", subscription.notes) + + return member + + def notify_failure(log): try: content = """ diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index ff7e6c473c..31da792e53 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -10,33 +10,7 @@ from frappe.utils import nowdate, add_months class TestMembership(unittest.TestCase): def setUp(self): - # Get default company - company = frappe.get_doc("Company", erpnext.get_default_company()) - - # update membership settings - settings = frappe.get_doc("Membership Settings") - # Enable razorpay - settings.enable_razorpay = 1 - settings.billing_cycle = "Monthly" - settings.billing_frequency = 24 - # Enable invoicing - settings.enable_invoicing = 1 - settings.make_payment_entry = 1 - settings.company = company.name - settings.payment_account = company.default_cash_account - settings.debit_account = company.default_receivable_account - settings.save() - - # make test plan - if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): - plan = frappe.new_doc("Membership Type") - plan.membership_type = "_rzpy_test_milythm" - plan.amount = 100 - plan.razorpay_plan_id = "_rzpy_test_milythm" - plan.linked_item = create_item("_Test Item for Non Profit Membership").name - plan.insert() - else: - plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") + plan = setup_membership() # make test member self.member_doc = create_member(frappe._dict({ @@ -78,7 +52,7 @@ class TestMembership(unittest.TestCase): }) def set_config(key, value): - frappe.db.set_value("Membership Settings", None, key, value) + frappe.db.set_value("Non Profit Settings", None, key, value) def make_membership(member, payload={}): data = { @@ -109,3 +83,36 @@ def create_item(item_code): else: item = frappe.get_doc("Item", item_code) return item + +def setup_membership(): + # Get default company + company = frappe.get_doc("Company", erpnext.get_default_company()) + + # update non profit settings + settings = frappe.get_doc("Non Profit Settings") + # Enable razorpay + settings.enable_razorpay_for_memberships = 1 + settings.billing_cycle = "Monthly" + settings.billing_frequency = 24 + # Enable invoicing + settings.allow_invoicing = 1 + settings.automate_membership_payment_entries = 1 + settings.company = company.name + settings.donation_company = company.name + settings.membership_payment_account = company.default_cash_account + settings.membership_debit_account = company.default_receivable_account + settings.flags.ignore_mandatory = True + settings.save() + + # make test plan + if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): + plan = frappe.new_doc("Membership Type") + plan.membership_type = "_rzpy_test_milythm" + plan.amount = 100 + plan.razorpay_plan_id = "_rzpy_test_milythm" + plan.linked_item = create_item("_Test Item for Non Profit Membership").name + plan.insert() + else: + plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") + + return plan \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json deleted file mode 100644 index 3887b0a2be..0000000000 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "actions": [], - "creation": "2020-03-29 12:57:03.005120", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enable_razorpay", - "razorpay_settings_section", - "billing_cycle", - "billing_frequency", - "webhook_secret", - "column_break_6", - "enable_invoicing", - "create_for_web_forms", - "make_payment_entry", - "company", - "debit_account", - "payment_account", - "column_break_9", - "send_email", - "send_invoice", - "membership_print_format", - "inv_print_format", - "email_template" - ], - "fields": [ - { - "fieldname": "billing_cycle", - "fieldtype": "Select", - "label": "Billing Cycle", - "options": "Monthly\nYearly" - }, - { - "default": "0", - "fieldname": "enable_razorpay", - "fieldtype": "Check", - "label": "Enable RazorPay For Memberships" - }, - { - "depends_on": "eval:doc.enable_razorpay", - "fieldname": "razorpay_settings_section", - "fieldtype": "Section Break", - "label": "RazorPay Settings" - }, - { - "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.", - "fieldname": "billing_frequency", - "fieldtype": "Int", - "label": "Billing Frequency" - }, - { - "fieldname": "webhook_secret", - "fieldtype": "Password", - "label": "Webhook Secret", - "read_only": 1 - }, - { - "fieldname": "column_break_6", - "fieldtype": "Section Break", - "label": "Invoicing" - }, - { - "depends_on": "eval:doc.enable_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_invoicing", - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "mandatory_depends_on": "eval:doc.enable_auto_invoicing", - "options": "Company" - }, - { - "default": "0", - "depends_on": "eval:doc.enable_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" - }, - { - "default": "0", - "fieldname": "enable_invoicing", - "fieldtype": "Check", - "label": "Enable Invoicing", - "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" - }, - { - "default": "0", - "depends_on": "eval:doc.enable_invoicing", - "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.", - "fieldname": "make_payment_entry", - "fieldtype": "Check", - "label": "Make Payment Entry" - }, - { - "depends_on": "eval:doc.make_payment_entry", - "fieldname": "payment_account", - "fieldtype": "Link", - "label": "Payment To", - "mandatory_depends_on": "eval:doc.make_payment_entry", - "options": "Account" - }, - { - "default": "0", - "depends_on": "eval:doc.enable_invoicing", - "description": "Automatically create an invoice when payment is authorized from a web form entry", - "fieldname": "create_for_web_forms", - "fieldtype": "Check", - "label": "Auto Create Invoice for Web Forms" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2021-01-21 19:57:53.213286", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Membership Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "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, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py deleted file mode 100644 index f3b2eee6f9..0000000000 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.integrations.utils import get_payment_gateway_controller -from frappe.model.document import Document - -class MembershipSettings(Document): - def generate_webhook_key(self): - key = frappe.generate_hash(length=20) - self.webhook_secret = key - self.save() - - frappe.msgprint( - _("Here is your webhook secret, this will be shown to you only once.") + "

" + key, - _("Webhook Secret") - ); - - def revoke_key(self): - self.webhook_secret = None; - self.save() - - def get_webhook_secret(self): - return self.get_password(fieldname="webhook_secret", raise_exception=False) - -@frappe.whitelist() -def get_plans_for_membership(*args, **kwargs): - controller = get_payment_gateway_controller("Razorpay") - plans = controller.get_plans() - return [plan.get("item") for plan in plans.get("items")] \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js index 91a5cb74ba..2f2427629c 100644 --- a/erpnext/non_profit/doctype/membership_type/membership_type.js +++ b/erpnext/non_profit/doctype/membership_type/membership_type.js @@ -3,11 +3,11 @@ frappe.ui.form.on('Membership Type', { refresh: function (frm) { - frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => { + frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => { if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); }); - frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => { + frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => { if (val) frm.set_df_property('linked_item', 'hidden', false); }); diff --git a/erpnext/non_profit/doctype/non_profit_settings/__init__.py b/erpnext/non_profit/doctype/non_profit_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js similarity index 50% rename from erpnext/non_profit/doctype/membership_settings/membership_settings.js rename to erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js index c95aab2a7a..cff92b42ab 100644 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js @@ -1,16 +1,8 @@ // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt -frappe.ui.form.on("Membership Settings", { +frappe.ui.form.on("Non Profit Settings", { refresh: function(frm) { - if (frm.doc.webhook_secret) { - frm.add_custom_button(__("Revoke "), () => { - frm.call("revoke_key").then(() => { - frm.refresh(); - }) - }); - } - frm.set_query("inv_print_format", function() { return { filters: { @@ -37,7 +29,7 @@ frappe.ui.form.on("Membership Settings", { }; }); - frm.set_query("payment_account", function () { + frm.set_query("membership_payment_account", function () { var account_types = ["Bank", "Cash"]; return { filters: { @@ -51,31 +43,70 @@ 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"); + frm.trigger("setup_buttons_for_membership"); + frm.trigger("setup_buttons_for_donation"); }, - add_generate_button: function(frm) { + setup_buttons_for_membership: function(frm) { let label; - if (frm.doc.webhook_secret) { + if (frm.doc.membership_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`); + }, __("Memberships")); + + frm.add_custom_button(__("Revoke Key"), () => { + frm.call("revoke_key", { + key: "membership_webhook_secret" + }).then(() => { + frm.refresh(); + }); + }, __("Memberships")); + label = __("Regenerate Webhook Secret"); + } else { label = __("Generate Webhook Secret"); } + frm.add_custom_button(label, () => { - frm.call("generate_webhook_key").then(() => { + frm.call("generate_webhook_secret", { + field: "membership_webhook_secret" + }).then(() => { frm.refresh(); }); - }); + }, __("Memberships")); }, - add_copy_buttonn: function(frm) { - if (frm.doc.webhook_secret) { + setup_buttons_for_donation: function(frm) { + let label; + + if (frm.doc.donation_webhook_secret) { + label = __("Regenerate 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`); - }); + frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.donation.donation.capture_razorpay_donations`); + }, __("Donations")); + + frm.add_custom_button(__("Revoke Key"), () => { + frm.call("revoke_key", { + key: "donation_webhook_secret" + }).then(() => { + frm.refresh(); + }); + }, __("Donations")); + + } else { + label = __("Generate Webhook Secret"); } + + frm.add_custom_button(label, () => { + frm.call("generate_webhook_secret", { + field: "donation_webhook_secret" + }).then(() => { + frm.refresh(); + }); + }, __("Donations")); } }); diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json new file mode 100644 index 0000000000..25ff0c1bb0 --- /dev/null +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json @@ -0,0 +1,273 @@ +{ + "actions": [], + "creation": "2020-03-29 12:57:03.005120", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable_razorpay_for_memberships", + "razorpay_settings_section", + "billing_cycle", + "billing_frequency", + "membership_webhook_secret", + "column_break_6", + "allow_invoicing", + "automate_membership_invoicing", + "automate_membership_payment_entries", + "company", + "membership_debit_account", + "membership_payment_account", + "column_break_9", + "send_email", + "send_invoice", + "membership_print_format", + "inv_print_format", + "email_template", + "donation_settings_section", + "donation_company", + "default_donor_type", + "donation_webhook_secret", + "column_break_22", + "automate_donation_payment_entries", + "donation_debit_account", + "donation_payment_account", + "section_break_27", + "creation_user" + ], + "fields": [ + { + "fieldname": "billing_cycle", + "fieldtype": "Select", + "label": "Billing Cycle", + "options": "Monthly\nYearly" + }, + { + "depends_on": "eval:doc.enable_razorpay_for_memberships", + "fieldname": "razorpay_settings_section", + "fieldtype": "Section Break", + "label": "RazorPay Settings for Memberships" + }, + { + "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.", + "fieldname": "billing_frequency", + "fieldtype": "Int", + "label": "Billing Frequency" + }, + { + "fieldname": "column_break_6", + "fieldtype": "Section Break", + "label": "Membership Invoicing" + }, + { + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, + { + "description": "This company will be set for the Memberships created via webhook.", + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "default": "0", + "depends_on": "eval:doc.allow_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" + }, + { + "default": "0", + "fieldname": "allow_invoicing", + "fieldtype": "Check", + "label": "Allow Invoicing for Memberships", + "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" + }, + { + "default": "0", + "depends_on": "eval:doc.allow_invoicing", + "description": "Automatically create an invoice when payment is authorized from a web form entry", + "fieldname": "automate_membership_invoicing", + "fieldtype": "Check", + "label": "Automate Invoicing for Web Forms" + }, + { + "default": "0", + "depends_on": "eval:doc.allow_invoicing", + "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.", + "fieldname": "automate_membership_payment_entries", + "fieldtype": "Check", + "label": "Automate Payment Entry Creation" + }, + { + "default": "0", + "fieldname": "enable_razorpay_for_memberships", + "fieldtype": "Check", + "label": "Enable RazorPay For Memberships" + }, + { + "depends_on": "eval:doc.automate_membership_payment_entries", + "description": "Account for accepting membership payments", + "fieldname": "membership_payment_account", + "fieldtype": "Link", + "label": "Membership Payment To", + "mandatory_depends_on": "eval:doc.automate_membership_payment_entries", + "options": "Account" + }, + { + "fieldname": "membership_webhook_secret", + "fieldtype": "Password", + "label": "Membership Webhook Secret", + "read_only": 1 + }, + { + "fieldname": "donation_webhook_secret", + "fieldtype": "Password", + "label": "Donation Webhook Secret", + "read_only": 1 + }, + { + "depends_on": "automate_donation_payment_entries", + "description": "Account for accepting donation payments", + "fieldname": "donation_payment_account", + "fieldtype": "Link", + "label": "Donation Payment To", + "mandatory_depends_on": "automate_donation_payment_entries", + "options": "Account" + }, + { + "default": "0", + "description": "Auto creates Payment Entry for Donations created from web forms.", + "fieldname": "automate_donation_payment_entries", + "fieldtype": "Check", + "label": "Automate Donation Payment Entries" + }, + { + "depends_on": "eval:doc.allow_invoicing", + "fieldname": "membership_debit_account", + "fieldtype": "Link", + "label": "Debit Account", + "mandatory_depends_on": "eval:doc.allow_invoicing", + "options": "Account" + }, + { + "depends_on": "automate_donation_payment_entries", + "fieldname": "donation_debit_account", + "fieldtype": "Link", + "label": "Debit Account", + "mandatory_depends_on": "automate_donation_payment_entries", + "options": "Account" + }, + { + "description": "This company will be set for the Donations created via webhook.", + "fieldname": "donation_company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "donation_settings_section", + "fieldtype": "Section Break", + "label": "Donation Settings" + }, + { + "fieldname": "column_break_22", + "fieldtype": "Column Break" + }, + { + "description": "This Donor Type will be set for the Donor created via Donation web form entry.", + "fieldname": "default_donor_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Default Donor Type", + "options": "Donor Type", + "reqd": 1 + }, + { + "fieldname": "section_break_27", + "fieldtype": "Section Break" + }, + { + "description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.", + "fieldname": "creation_user", + "fieldtype": "Link", + "label": "Creation User", + "options": "User", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2021-03-11 10:43:38.124240", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Non Profit Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "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, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py new file mode 100644 index 0000000000..108554c6a0 --- /dev/null +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.integrations.utils import get_payment_gateway_controller +from frappe.model.document import Document + +class NonProfitSettings(Document): + def generate_webhook_secret(self, field="membership_webhook_secret"): + key = frappe.generate_hash(length=20) + self.set(field, key) + self.save() + + secret_for = "Membership" if field == "membership_webhook_secret" else "Donation" + + frappe.msgprint( + _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "

" + key, + _("Webhook Secret") + ) + + def revoke_key(self, key): + self.set(key, None) + self.save() + + def get_webhook_secret(self, endpoint="Membership"): + fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret" + return self.get_password(fieldname=fieldname, raise_exception=False) + +@frappe.whitelist() +def get_plans_for_membership(*args, **kwargs): + controller = get_payment_gateway_controller("Razorpay") + plans = controller.get_plans() + return [plan.get("item") for plan in plans.get("items")] \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py similarity index 79% rename from erpnext/non_profit/doctype/membership_settings/test_membership_settings.py rename to erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py index 2ad7984583..3f0ede32e5 100644 --- a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py +++ b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py @@ -6,5 +6,5 @@ from __future__ import unicode_literals # import frappe import unittest -class TestMembershipSettings(unittest.TestCase): +class TestNonProfitSettings(unittest.TestCase): pass diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json new file mode 100644 index 0000000000..2557d77d88 --- /dev/null +++ b/erpnext/non_profit/workspace/non_profit/non_profit.json @@ -0,0 +1,251 @@ +{ + "category": "Domains", + "charts": [], + "creation": "2020-03-02 17:23:47.811421", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Workspace", + "extends_another_page": 0, + "hide_custom": 0, + "icon": "non-profit", + "idx": 0, + "is_default": 0, + "is_standard": 1, + "label": "Non Profit", + "links": [ + { + "hidden": 0, + "is_query_report": 0, + "label": "Loan Management", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Type", + "link_to": "Loan Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan Application", + "link_to": "Loan Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Loan", + "link_to": "Loan", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Grant Application", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Grant Application", + "link_to": "Grant Application", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Membership", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Member", + "link_to": "Member", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Membership", + "link_to": "Membership", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Membership Type", + "link_to": "Membership Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Membership Settings", + "link_to": "Non Profit Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Volunteer", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Volunteer", + "link_to": "Volunteer", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Volunteer Type", + "link_to": "Volunteer Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Chapter", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Chapter", + "link_to": "Chapter", + "link_type": "DocType", + "onboard": 1, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Donation", + "onboard": 0, + "type": "Card Break" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Donor", + "link_to": "Donor", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "dependencies": "", + "hidden": 0, + "is_query_report": 0, + "label": "Donor Type", + "link_to": "Donor Type", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Donation", + "link_to": "Donation", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Tax Exemption Certification (India)", + "link_type": "DocType", + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Tax Exemption 80G Certificate", + "link_to": "Tax Exemption 80G Certificate", + "link_type": "DocType", + "onboard": 0, + "type": "Link" + } + ], + "modified": "2021-03-11 11:38:09.140655", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Non Profit", + "owner": "Administrator", + "pin_to_bottom": 0, + "pin_to_top": 0, + "restrict_to_domain": "Non Profit", + "shortcuts": [ + { + "label": "Member", + "link_to": "Member", + "type": "DocType" + }, + { + "label": "Non Profit Settings", + "link_to": "Non Profit Settings", + "type": "DocType" + }, + { + "label": "Membership", + "link_to": "Membership", + "type": "DocType" + }, + { + "label": "Chapter", + "link_to": "Chapter", + "type": "DocType" + }, + { + "label": "Chapter Member", + "link_to": "Chapter Member", + "type": "DocType" + } + ] +} \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 80e2f1c01a..20ea5097bf 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -753,3 +753,5 @@ erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021 erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v12_0.add_state_code_for_ladakh erpnext.patches.v13_0.update_vehicle_no_reqd_condition +erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation +erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings \ No newline at end of file diff --git a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py new file mode 100644 index 0000000000..3fa09a7baa --- /dev/null +++ b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py @@ -0,0 +1,22 @@ +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if frappe.db.table_exists("Membership Settings"): + frappe.rename_doc("DocType", "Membership Settings", "Non Profit Settings") + frappe.reload_doctype("Non Profit Settings", force=True) + + if frappe.db.table_exists("Non Profit Settings"): + rename_fields_map = { + "enable_invoicing": "allow_invoicing", + "create_for_web_forms": "automate_membership_invoicing", + "make_payment_entry": "automate_membership_payment_entries", + "enable_razorpay": "enable_razorpay_for_memberships", + "debit_account": "membership_debit_account", + "payment_account": "membership_payment_account", + "webhook_secret": "membership_webhook_secret" + } + + for old_name, new_name in rename_fields_map.items(): + rename_field("Non Profit Settings", old_name, new_name) \ No newline at end of file diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py new file mode 100644 index 0000000000..aea53f8add --- /dev/null +++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py @@ -0,0 +1,16 @@ +import frappe +from erpnext.regional.india.setup import make_custom_fields + +def execute(): + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + make_custom_fields() + + if not frappe.db.exists('Party Type', 'Donor'): + frappe.get_doc({ + 'doctype': 'Party Type', + 'party_type': 'Donor', + 'account_type': 'Receivable' + }).insert(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js new file mode 100644 index 0000000000..54cde9c0cf --- /dev/null +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js @@ -0,0 +1,67 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Tax Exemption 80G Certificate', { + refresh: function(frm) { + if (frm.doc.donor) { + frm.set_query('donation', function() { + return { + filters: { + docstatus: 1, + donor: frm.doc.donor + } + }; + }); + } + }, + + recipient: function(frm) { + if (frm.doc.recipient === 'Donor') { + frm.set_value({ + 'member': '', + 'member_name': '', + 'member_email': '', + 'member_pan_number': '', + 'fiscal_year': '', + 'total': 0, + 'payments': [] + }); + } else { + frm.set_value({ + 'donor': '', + 'donor_name': '', + 'donor_email': '', + 'donor_pan_number': '', + 'donation': '', + 'date_of_donation': '', + 'amount': 0, + 'mode_of_payment': '', + 'razorpay_payment_id': '' + }); + } + }, + + get_payments: function(frm) { + frm.call({ + doc: frm.doc, + method: 'get_payments', + freeze: true + }); + }, + + company: function(frm) { + if ((frm.doc.member || frm.doc.donor) && frm.doc.company) { + frm.call({ + doc: frm.doc, + method: 'set_company_address', + freeze: true + }); + } + }, + + donation: function(frm) { + if (frm.doc.recipient === 'Donor' && !frm.doc.donor) { + frappe.msgprint(__('Please select donor first')); + } + } +}); diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json new file mode 100644 index 0000000000..9eee722f42 --- /dev/null +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json @@ -0,0 +1,297 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2021-02-15 12:37:21.577042", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "recipient", + "member", + "member_name", + "member_email", + "member_pan_number", + "donor", + "donor_name", + "donor_email", + "donor_pan_number", + "column_break_4", + "date", + "fiscal_year", + "section_break_11", + "company", + "company_address", + "company_address_display", + "column_break_14", + "company_pan_number", + "company_80g_number", + "company_80g_wef", + "title", + "section_break_6", + "get_payments", + "payments", + "total", + "donation_details_section", + "donation", + "date_of_donation", + "amount", + "column_break_27", + "mode_of_payment", + "razorpay_payment_id" + ], + "fields": [ + { + "fieldname": "recipient", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Certificate Recipient", + "options": "Member\nDonor", + "reqd": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Member\";", + "fieldname": "member", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Member", + "mandatory_depends_on": "eval:doc.recipient === \"Member\";", + "options": "Member" + }, + { + "depends_on": "eval:doc.recipient === \"Member\";", + "fetch_from": "member.member_name", + "fieldname": "member_name", + "fieldtype": "Data", + "label": "Member Name", + "read_only": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Donor\";", + "fieldname": "donor", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Donor", + "mandatory_depends_on": "eval:doc.recipient === \"Donor\";", + "options": "Donor" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "reqd": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Member\";", + "fieldname": "section_break_6", + "fieldtype": "Section Break" + }, + { + "fieldname": "payments", + "fieldtype": "Table", + "label": "Payments", + "options": "Tax Exemption 80G Certificate Detail" + }, + { + "fieldname": "total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Total", + "read_only": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Member\";", + "fieldname": "fiscal_year", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Fiscal Year", + "options": "Fiscal Year" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "get_payments", + "fieldtype": "Button", + "label": "Get Memberships" + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "NPO-80G-.YYYY.-" + }, + { + "fieldname": "section_break_11", + "fieldtype": "Section Break", + "label": "Company Details" + }, + { + "fieldname": "company_address", + "fieldtype": "Link", + "label": "Company Address", + "options": "Address" + }, + { + "fieldname": "column_break_14", + "fieldtype": "Column Break" + }, + { + "fetch_from": "company.pan_details", + "fieldname": "company_pan_number", + "fieldtype": "Data", + "label": "PAN Number", + "read_only": 1 + }, + { + "fieldname": "company_address_display", + "fieldtype": "Small Text", + "hidden": 1, + "label": "Company Address Display", + "print_hide": 1, + "read_only": 1 + }, + { + "fetch_from": "company.company_80g_number", + "fieldname": "company_80g_number", + "fieldtype": "Data", + "label": "80G Number", + "read_only": 1 + }, + { + "fetch_from": "company.with_effect_from", + "fieldname": "company_80g_wef", + "fieldtype": "Date", + "label": "80G With Effect From", + "read_only": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Donor\";", + "fieldname": "donation_details_section", + "fieldtype": "Section Break", + "label": "Donation Details" + }, + { + "fieldname": "donation", + "fieldtype": "Link", + "label": "Donation", + "mandatory_depends_on": "eval:doc.recipient === \"Donor\";", + "options": "Donation" + }, + { + "fetch_from": "donation.amount", + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "read_only": 1 + }, + { + "fetch_from": "donation.mode_of_payment", + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment", + "read_only": 1 + }, + { + "fetch_from": "donation.razorpay_payment_id", + "fieldname": "razorpay_payment_id", + "fieldtype": "Data", + "label": "RazorPay Payment ID", + "read_only": 1 + }, + { + "fetch_from": "donation.date", + "fieldname": "date_of_donation", + "fieldtype": "Date", + "label": "Date of Donation", + "read_only": 1 + }, + { + "fieldname": "column_break_27", + "fieldtype": "Column Break" + }, + { + "depends_on": "eval:doc.recipient === \"Donor\";", + "fetch_from": "donor.donor_name", + "fieldname": "donor_name", + "fieldtype": "Data", + "label": "Donor Name", + "read_only": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Donor\";", + "fetch_from": "donor.email", + "fieldname": "donor_email", + "fieldtype": "Data", + "label": "Email", + "read_only": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Member\";", + "fetch_from": "member.email_id", + "fieldname": "member_email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "read_only": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Member\";", + "fetch_from": "member.pan_number", + "fieldname": "member_pan_number", + "fieldtype": "Data", + "label": "PAN Details", + "read_only": 1 + }, + { + "depends_on": "eval:doc.recipient === \"Donor\";", + "fetch_from": "donor.pan_number", + "fieldname": "donor_pan_number", + "fieldtype": "Data", + "label": "PAN Details", + "read_only": 1 + }, + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "print_hide": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2021-02-22 00:03:34.215633", + "modified_by": "Administrator", + "module": "Regional", + "name": "Tax Exemption 80G Certificate", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "search_fields": "member, member_name", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py new file mode 100644 index 0000000000..d734a18c3a --- /dev/null +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +from frappe.utils import getdate, flt, get_link_to_form +from erpnext.accounts.utils import get_fiscal_year +from frappe.contacts.doctype.address.address import get_company_address + +class TaxExemption80GCertificate(Document): + def validate(self): + self.validate_date() + self.validate_duplicates() + self.validate_company_details() + self.set_company_address() + self.set_title() + + def validate_date(self): + if self.recipient == 'Member': + if getdate(self.date): + fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) + + if not (fiscal_year.year_start_date <= getdate(self.date) \ + <= fiscal_year.year_end_date): + frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year))) + + def validate_duplicates(self): + if self.recipient == 'Donor': + certificate = frappe.db.exists(self.doctype, {'donation': self.donation}) + if certificate: + frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format( + get_link_to_form(self.doctype, certificate), frappe.bold(self.donation) + ), title=_('Duplicate Certificate')) + + def validate_company_details(self): + fields = ['company_80g_number', 'with_effect_from', 'pan_details'] + company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True) + if not company_details.company_80g_number: + frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'), + get_link_to_form('Company', self.company))) + + if not company_details.pan_details: + frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'), + get_link_to_form('Company', self.company))) + + def set_company_address(self): + address = get_company_address(self.company) + self.company_address = address.company_address + self.company_address_display = address.company_address_display + + def set_title(self): + if self.recipient == "Member": + self.title = self.member_name + else: + self.title = self.donor_name + + def get_payments(self): + if not self.member: + frappe.throw(_('Please select a Member first.')) + + fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True) + + memberships = frappe.db.get_all('Membership', { + 'member': self.member, + 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], + 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], + 'membership_status': ('!=', 'Cancelled') + }, ['from_date', 'amount', 'name', 'invoice', 'payment_id']) + + if not memberships: + frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member)) + + total = 0 + self.payments = [] + + for doc in memberships: + self.append('payments', { + 'date': doc.from_date, + 'amount': doc.amount, + 'invoice_id': doc.invoice, + 'razorpay_payment_id': doc.payment_id, + 'membership': doc.name + }) + total += flt(doc.amount) + + self.total = total diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py new file mode 100644 index 0000000000..346ebbf679 --- /dev/null +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from frappe.utils import getdate +from erpnext.accounts.utils import get_fiscal_year +from erpnext.non_profit.doctype.donation.test_donation import create_donor, create_mode_of_payment, create_donor_type +from erpnext.non_profit.doctype.donation.donation import create_donation +from erpnext.non_profit.doctype.membership.test_membership import setup_membership, make_membership +from erpnext.non_profit.doctype.member.member import create_member + +class TestTaxExemption80GCertificate(unittest.TestCase): + def setUp(self): + frappe.db.sql('delete from `tabTax Exemption 80G Certificate`') + frappe.db.sql('delete from `tabMembership`') + create_donor_type() + settings = frappe.get_doc('Non Profit Settings') + settings.company = '_Test Company' + settings.donation_company = '_Test Company' + settings.default_donor_type = '_Test Donor' + settings.creation_user = 'Administrator' + settings.save() + + company = frappe.get_doc('Company', '_Test Company') + company.pan_details = 'BBBTI3374C' + company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087' + company.with_effect_from = getdate() + company.save() + + def test_duplicate_donation_certificate(self): + donor = create_donor() + create_mode_of_payment() + payment = frappe._dict({ + 'amount': 100, + 'method': 'Debit Card', + 'id': 'pay_MeXAmsgeKOhq7O' + }) + donation = create_donation(donor, payment) + + args = frappe._dict({ + 'recipient': 'Donor', + 'donor': donor.name, + 'donation': donation.name + }) + certificate = create_80g_certificate(args) + certificate.insert() + + # check company details + self.assertEquals(certificate.company_pan_number, 'BBBTI3374C') + self.assertEquals(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087') + + # check donation details + self.assertEquals(certificate.amount, donation.amount) + + duplicate_certificate = create_80g_certificate(args) + # duplicate validation + self.assertRaises(frappe.ValidationError, duplicate_certificate.insert) + + def test_membership_80g_certificate(self): + plan = setup_membership() + + # make test member + member_doc = create_member(frappe._dict({ + 'fullname': "_Test_Member", + 'email': "_test_member_erpnext@example.com", + 'plan_id': plan.name + })) + member_doc.make_customer_and_link() + member = member_doc.name + + membership = make_membership(member, { "from_date": getdate() }) + invoice = membership.generate_invoice(save=True) + + args = frappe._dict({ + 'recipient': 'Member', + 'member': member, + 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name') + }) + certificate = create_80g_certificate(args) + certificate.get_payments() + certificate.insert() + + self.assertEquals(len(certificate.payments), 1) + self.assertEquals(certificate.payments[0].amount, membership.amount) + self.assertEquals(certificate.payments[0].invoice_id, invoice.name) + + +def create_80g_certificate(args): + certificate = frappe.get_doc({ + 'doctype': 'Tax Exemption 80G Certificate', + 'recipient': args.recipient, + 'date': getdate(), + 'company': '_Test Company' + }) + + certificate.update(args) + + return certificate \ No newline at end of file diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json new file mode 100644 index 0000000000..dfa817dd27 --- /dev/null +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json @@ -0,0 +1,66 @@ +{ + "actions": [], + "creation": "2021-02-15 12:43:52.754124", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "date", + "amount", + "invoice_id", + "column_break_4", + "razorpay_payment_id", + "membership" + ], + "fields": [ + { + "fieldname": "date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Amount", + "reqd": 1 + }, + { + "fieldname": "invoice_id", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Invoice ID", + "options": "Sales Invoice", + "reqd": 1 + }, + { + "fieldname": "razorpay_payment_id", + "fieldtype": "Data", + "label": "Razorpay Payment ID" + }, + { + "fieldname": "membership", + "fieldtype": "Link", + "label": "Membership", + "options": "Membership" + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-02-15 16:35:10.777587", + "modified_by": "Administrator", + "module": "Regional", + "name": "Tax Exemption 80G Certificate Detail", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py new file mode 100644 index 0000000000..bdad798d98 --- /dev/null +++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class TaxExemption80GCertificateDetail(Document): + pass diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 526198424f..40247f7e3d 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -398,9 +398,9 @@ def make_custom_fields(update=True): si_einvoice_fields = [ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), - + dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), - + dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, @@ -498,6 +498,14 @@ def make_custom_fields(update=True): fieldtype='Link', options='Salary Component', insert_after='basic_component'), dict(fieldname='arrear_component', label='Arrear Component', fieldtype='Link', options='Salary Component', insert_after='hra_component'), + dict(fieldname='non_profit_section', label='Non Profit Settings', + fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), + dict(fieldname='company_80g_number', label='80G Number', + fieldtype='Data', insert_after='non_profit_section'), + dict(fieldname='with_effect_from', label='80G With Effect From', + fieldtype='Date', insert_after='company_80g_number'), + dict(fieldname='pan_details', label='PAN Number', + fieldtype='Data', insert_after='with_effect_from') ], 'Employee Tax Exemption Declaration':[ dict(fieldname='hra_section', label='HRA Exemption', @@ -580,7 +588,15 @@ def make_custom_fields(update=True): 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } ], - "Member": [ + 'Member': [ + { + 'fieldname': 'pan_number', + 'label': 'PAN Details', + 'fieldtype': 'Data', + 'insert_after': 'email_id' + } + ], + 'Donor': [ { 'fieldname': 'pan_number', 'label': 'PAN Details', @@ -642,7 +658,7 @@ def set_tax_withholding_category(company): pass docs = get_tds_details(accounts, fiscal_year) - + for d in docs: try: doc = frappe.get_doc(d) @@ -660,7 +676,7 @@ def set_tax_withholding_category(company): fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] if not fy_exist: doc.append("rates", d.get('rates')[0]) - + doc.flags.ignore_permissions = True doc.flags.ignore_mandatory = True doc.save() diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json new file mode 100644 index 0000000000..a8da0bd209 --- /dev/null +++ b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json @@ -0,0 +1,26 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-02-22 00:17:33.878581", + "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Tax Exemption 80G Certificate", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "{% if letter_head and not no_letterhead -%}\n
{{ letter_head }}
\n{%- endif %}\n\n
\n

{{ doc.company }} 80G Donor Certificate

\n
\n

\n\n
\n

{{ _(\"Certificate No. : \") }} {{ doc.name }}

\n

\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n

\n

\n \n
\n\n This is to confirm that the {{ doc.company }} received an amount of {{doc.get_formatted(\"amount\")}}\n from {{ doc.donor_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n

\n \n

\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

\n\n", + "idx": 0, + "line_breaks": 0, + "modified": "2021-02-22 00:20:08.516600", + "modified_by": "Administrator", + "module": "Regional", + "name": "80G Certificate for Donation", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py b/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json new file mode 100644 index 0000000000..f1b15aab29 --- /dev/null +++ b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json @@ -0,0 +1,26 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-02-15 16:53:55.026611", + "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Tax Exemption 80G Certificate", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "{% if letter_head and not no_letterhead -%}\n
{{ letter_head }}
\n{%- endif %}\n\n
\n

{{ doc.company }} Members 80G Donor Certificate

\n

Financial Cycle {{ doc.fiscal_year }}

\n
\n

\n\n
\n

{{ _(\"Certificate No. : \") }} {{ doc.name }}

\n

\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n

\n

\n \n
\n This is to confirm that the {{ doc.company }} received a total amount of {{doc.get_formatted(\"total\")}}\n from {{ doc.member_name }}\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n as per the payment details given below:\n \n

\n \n \t\n \t\t\n \t\t\t\n \t\t\t\n \t\t\t\n \t\t\n \t\n \t\n \t\t{%- for payment in doc.payments -%}\n \t\t\n \t\t\t\n \t\t\t\n \t\t\t\n \t\t\n \t\t{%- endfor -%}\n \t\n
{{ _(\"Date\") }}{{ _(\"Amount\") }}{{ _(\"Invoice ID\") }}
{{ payment.date }} {{ payment.get_formatted(\"amount\") }}{{ payment.invoice_id }}
\n \n
\n \n

\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

\n\n", + "idx": 0, + "line_breaks": 0, + "modified": "2021-02-21 23:29:00.778973", + "modified_by": "Administrator", + "module": "Regional", + "name": "80G Certificate for Membership", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 72ed00293e..5053c6a512 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -195,6 +195,7 @@ def install(country=None): {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, + {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"}, {'doctype': "Opportunity Type", "name": "Hub"}, {'doctype': "Opportunity Type", "name": _("Sales")},