Merge branch 'develop' into improve_taxes_setup

This commit is contained in:
Raffael Meyer 2021-03-11 13:17:44 +01:00 committed by GitHub
commit fb68aba30d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
69 changed files with 3050 additions and 333 deletions

View File

@ -12,7 +12,7 @@ class ModeofPayment(Document):
self.validate_accounts() self.validate_accounts()
self.validate_repeating_companies() self.validate_repeating_companies()
self.validate_pos_mode_of_payment() self.validate_pos_mode_of_payment()
def validate_repeating_companies(self): def validate_repeating_companies(self):
"""Error when Same Company is entered multiple times in accounts""" """Error when Same Company is entered multiple times in accounts"""
accounts_list = [] accounts_list = []
@ -31,10 +31,10 @@ class ModeofPayment(Document):
def validate_pos_mode_of_payment(self): def validate_pos_mode_of_payment(self):
if not self.enabled: if not self.enabled:
pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip
WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name)) WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name))
pos_profiles = list(map(lambda x: x[0], pos_profiles)) pos_profiles = list(map(lambda x: x[0], pos_profiles))
if pos_profiles: if pos_profiles:
message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \ message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \
Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode."

View File

@ -92,14 +92,16 @@ frappe.ui.form.on('Payment Entry', {
}); });
frm.set_query("reference_doctype", "references", function() { 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"]; 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"]; 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"]; var doctypes = ["Expense Claim", "Journal Entry"];
} else if (frm.doc.party_type=="Student") { } else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"]; var doctypes = ["Fees"];
} else if (frm.doc.party_type == "Donor") {
var doctypes = ["Donation"];
} else { } else {
var doctypes = ["Journal Entry"]; var doctypes = ["Journal Entry"];
} }
@ -128,7 +130,7 @@ frappe.ui.form.on('Payment Entry', {
const child = locals[cdt][cdn]; const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company}; const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', 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)) { if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party; 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); let party_types = Object.keys(frappe.boot.party_account_types);
if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){ if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){
frm.set_value("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() { 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=="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=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (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(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount) 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=="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=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (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) { if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = 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])); frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false; 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) { if (row) {

View File

@ -72,6 +72,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation()
self.update_payment_schedule() self.update_payment_schedule()
self.set_status() self.set_status()
@ -82,6 +83,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation(cancel=1)
self.delink_advance_entry_references() self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1) self.update_payment_schedule(cancel=1)
self.set_payment_req_status() self.set_payment_req_status()
@ -242,9 +244,11 @@ class PaymentEntry(AccountsController):
elif self.party_type == "Supplier": elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
elif self.party_type == "Employee": elif self.party_type == "Employee":
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity")
elif self.party_type == "Shareholder": elif self.party_type == "Shareholder":
valid_reference_doctypes = ("Journal Entry") valid_reference_doctypes = ("Journal Entry")
elif self.party_type == "Donor":
valid_reference_doctypes = ("Donation")
for d in self.get("references"): for d in self.get("references"):
if not d.allocated_amount: if not d.allocated_amount:
@ -608,7 +612,7 @@ class PaymentEntry(AccountsController):
if self.payment_type in ("Receive", "Pay") and self.party: if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount \ if d.allocated_amount \
and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance"): and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance", "Gratuity"):
frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
def update_expense_claim(self): def update_expense_claim(self):
@ -618,6 +622,13 @@ class PaymentEntry(AccountsController):
doc = frappe.get_doc("Expense Claim", d.reference_name) doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, self.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): def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name self.reference_no = reference_doc.name
self.reference_date = nowdate() self.reference_date = nowdate()
@ -917,6 +928,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
total_amount = ref_doc.get("grand_total") total_amount = ref_doc.get("grand_total")
exchange_rate = 1 exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount") outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Donation":
total_amount = ref_doc.get("amount")
exchange_rate = 1
elif reference_doctype == "Dunning": elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount") total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1 exchange_rate = 1
@ -936,6 +950,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
exchange_rate = ref_doc.get("exchange_rate") exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency: if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate) total_amount = flt(total_amount) * flt(exchange_rate)
elif ref_doc.doctype == "Gratuity":
total_amount = ref_doc.amount
if not total_amount: if not total_amount:
if party_account_currency == company_currency: if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total total_amount = ref_doc.base_grand_total
@ -959,6 +975,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency: if party_account_currency == company_currency:
exchange_rate = 1 exchange_rate = 1
elif reference_doctype == "Gratuity":
outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount)
else: else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else: else:
@ -1164,10 +1182,12 @@ def set_party_type(dt):
party_type = "Customer" party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"): elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier" party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"): elif dt in ("Expense Claim", "Employee Advance", "Gratuity"):
party_type = "Employee" party_type = "Employee"
elif dt in ("Fees"): elif dt == "Fees":
party_type = "Student" party_type = "Student"
elif dt == "Donation":
party_type = "Donor"
return party_type return party_type
def set_party_account(dt, dn, doc, party_type): def set_party_account(dt, dn, doc, party_type):
@ -1181,6 +1201,8 @@ def set_party_account(dt, dn, doc, party_type):
party_account = doc.advance_account party_account = doc.advance_account
elif dt == "Expense Claim": elif dt == "Expense Claim":
party_account = doc.payable_account party_account = doc.payable_account
elif dt == "Gratuity":
party_account = doc.payable_account
else: else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account return party_account
@ -1193,7 +1215,7 @@ def set_party_account_currency(dt, party_account, doc):
return party_account_currency return party_account_currency
def set_payment_type(dt, doc): 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): or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive" payment_type = "Receive"
else: else:
@ -1226,6 +1248,12 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
elif dt == "Dunning": elif dt == "Dunning":
grand_total = doc.grand_total grand_total = doc.grand_total
outstanding_amount = doc.grand_total outstanding_amount = doc.grand_total
elif dt == "Donation":
grand_total = doc.amount
outstanding_amount = doc.amount
elif dt == "Gratuity":
grand_total = doc.amount
outstanding_amount = flt(doc.amount) - flt(doc.paid_amount)
else: else:
if party_account_currency == doc.company_currency: if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)

View File

@ -968,7 +968,7 @@ class PurchaseInvoice(BuyingController):
# base_rounding_adjustment may become zero due to small precision # base_rounding_adjustment may become zero due to small precision
# eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2
# then base_rounding_adjustment becomes zero and error is thrown in GL Entry # then base_rounding_adjustment becomes zero and error is thrown in GL Entry
if self.rounding_adjustment and self.base_rounding_adjustment: if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment:
round_off_account, round_off_cost_center = \ round_off_account, round_off_cost_center = \
get_round_off_account_and_cost_center(self.company) get_round_off_account_and_cost_center(self.company)

View File

@ -240,8 +240,7 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters): def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values(): for entries in gl_entries_by_account.values():
for entry in entries: for entry in entries:
key = entry.account_number or entry.account_name d = accounts_by_name.get(entry.account_name)
d = accounts_by_name.get(key)
if d: if d:
for company in companies: for company in companies:
# check if posting date is within the period # check if posting date is within the period
@ -256,7 +255,8 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts""" """accumulate children's values in parent accounts"""
for d in reversed(accounts): for d in reversed(accounts):
if d.parent_account: if d.parent_account:
account = d.parent_account.split(' - ')[0].strip() account = d.parent_account_name
if not accounts_by_name.get(account): if not accounts_by_name.get(account):
continue continue
@ -267,16 +267,34 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account]["opening_balance"] = \ accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters): def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, filters)
if not accounts: if not accounts:
return None, None return None, None
accounts = update_parent_account_names(accounts)
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name return accounts, accounts_by_name
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.
parent_name is `name` of parent account which could have other prefix
of account_number and suffix of company abbr. This function adds key called
`parent_account_name` which does not have such prefix/suffix.
"""
name_to_account_map = { d.name : d.account_name for d in accounts }
for account in accounts:
if account.parent_account:
account["parent_account_name"] = name_to_account_map[account.parent_account]
return accounts
def get_companies(filters): def get_companies(filters):
companies = {} companies = {}
all_companies = get_subsidiary_companies(filters.get('company')) all_companies = get_subsidiary_companies(filters.get('company'))
@ -381,9 +399,9 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries: for entry in gl_entries:
key = entry.account_number or entry.account_name account_name = entry.account_name
validate_entries(key, entry, accounts_by_name, accounts) validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry) gl_entries_by_account.setdefault(account_name, []).append(entry)
return gl_entries_by_account return gl_entries_by_account
@ -452,8 +470,7 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {} parent_children_map = {}
accounts_by_name = {} accounts_by_name = {}
for d in accounts: for d in accounts:
key = d.account_number or d.account_name accounts_by_name[d.account_name] = d
accounts_by_name[key] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d) parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = [] filtered_accounts = []

View File

@ -813,7 +813,7 @@
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2021-01-01 16:54:33.477439", "modified": "2021-01-02 16:54:33.477439",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@ -48,6 +48,7 @@ class TestEmployee(unittest.TestCase):
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
def make_employee(user, company=None, **kwargs): def make_employee(user, company=None, **kwargs):
""
if not frappe.db.get_value("User", user): if not frappe.db.get_value("User", user):
frappe.get_doc({ frappe.get_doc({
"doctype": "User", "doctype": "User",

View File

@ -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);
}
});
},
});

View File

@ -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
}

View File

@ -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 <b>Payment Account</b> 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

View File

@ -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']
}
]
}

View File

@ -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()

View File

@ -76,8 +76,13 @@
} }
], ],
"image_field": "image", "image_field": "image",
"links": [], "links": [
"modified": "2020-09-16 23:46:04.083274", {
"link_doctype": "Donation",
"link_fieldname": "donor"
}
],
"modified": "2021-02-17 16:36:33.470731",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Donor", "name": "Donor",

View File

@ -11,3 +11,8 @@ class Donor(Document):
"""Load address and contacts in `__onload`""" """Load address and contacts in `__onload`"""
load_address_and_contact(self) 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)

View File

@ -3,7 +3,7 @@
frappe.ui.form.on('Member', { frappe.ui.form.on('Member', {
setup: function(frm) { 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)) { if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
frm.set_df_property('razorpay_details_section', 'hidden', false); frm.set_df_property('razorpay_details_section', 'hidden', false);
} }

View File

@ -7,7 +7,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact 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 frappe.integrations.utils import get_payment_gateway_controller
from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type 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) validate_email_address(email.strip(), True)
def setup_subscription(self): def setup_subscription(self):
membership_settings = frappe.get_doc("Membership Settings") non_profit_settings = frappe.get_doc('Non Profit Settings')
if not membership_settings.enable_razorpay: if not non_profit_settings.enable_razorpay_for_memberships:
frappe.throw("Please enable Razorpay to setup subscription") 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") controller = get_payment_gateway_controller("Razorpay")
settings = controller.get_settings({}) settings = controller.get_settings({})
@ -40,7 +41,7 @@ class Member(Document):
subscription_details = { subscription_details = {
"plan_id": plan_id, "plan_id": plan_id,
"billing_frequency": cint(membership_settings.billing_frequency), "billing_frequency": cint(non_profit_settings.billing_frequency),
"customer_notify": 1 "customer_notify": 1
} }

View File

@ -3,7 +3,7 @@
frappe.ui.form.on('Membership', { frappe.ui.form.on('Membership', {
setup: function(frm) { 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); 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", () => { if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => { frm.call("send_acknowlement").then(() => {
frm.reload_doc(); frm.reload_doc();

View File

@ -10,6 +10,7 @@
"member_name", "member_name",
"membership_type", "membership_type",
"column_break_3", "column_break_3",
"company",
"membership_status", "membership_status",
"membership_validity_section", "membership_validity_section",
"from_date", "from_date",
@ -132,11 +133,18 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Member Name", "label": "Member Name",
"read_only": 1 "read_only": 1
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-01-21 16:31:20.032656", "modified": "2021-02-19 14:33:44.925122",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Membership", "name": "Membership",

View File

@ -6,6 +6,7 @@ from __future__ import unicode_literals
import json import json
import frappe import frappe
import six import six
import os
from datetime import datetime from datetime import datetime
from frappe.model.document import Document from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers from frappe.email import sendmail_to_system_managers
@ -58,7 +59,7 @@ class Membership(Document):
else: else:
self.from_date = nowdate() 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) self.to_date = add_years(self.from_date, 1)
else: else:
self.to_date = add_months(self.from_date, 1) self.to_date = add_months(self.from_date, 1)
@ -68,9 +69,9 @@ class Membership(Document):
return return
self.load_from_db() self.load_from_db()
self.db_set("paid", 1) self.db_set("paid", 1)
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Non Profit Settings")
if settings.enable_invoicing and settings.create_for_web_forms: if settings.allow_invoicing and settings.automate_membership_invoicing:
self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True) self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
def generate_invoice(self, save=True, with_payment_entry=False): 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))) frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type) 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) self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, 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): def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type) 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 <b>Debit Account</b> in {0}").format(settings_link)) frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
if not settings.company: if not settings.company:
@ -113,25 +114,26 @@ class Membership(Document):
get_link_to_form("Membership Type", self.membership_type))) get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice): def make_payment_entry(self, settings, invoice):
if not settings.payment_account: if not settings.membership_payment_account:
frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format( frappe.throw(_("You need to set <b>Payment Account</b> for Membership in {0}").format(
get_link_to_form("Membership Type", self.membership_type))) get_link_to_form("Non Profit Settings", "Non Profit Settings")))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False 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_no = self.name
pe.reference_date = getdate() pe.reference_date = getdate()
pe.save(ignore_permissions=True) pe.flags.ignore_mandatory = True
pe.save()
pe.submit() pe.submit()
def send_acknowlement(self): def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Non Profit Settings")
if not settings.send_email: if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format( frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> 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) member = frappe.get_doc("Member", self.member)
if not member.email_id: if not member.email_id:
@ -170,7 +172,7 @@ def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({ invoice = frappe.get_doc({
"doctype": "Sales Invoice", "doctype": "Sales Invoice",
"customer": member.customer, "customer": member.customer,
"debit_to": settings.debit_account, "debit_to": settings.membership_debit_account,
"currency": membership.currency, "currency": membership.currency,
"company": settings.company, "company": settings.company,
"is_pos": 0, "is_pos": 0,
@ -183,7 +185,7 @@ def make_invoice(membership, member, plan, settings):
] ]
}) })
invoice.set_missing_values() invoice.set_missing_values()
invoice.insert(ignore_permissions=True) invoice.insert()
invoice.submit() invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully")) frappe.msgprint(_("Sales Invoice created successfully"))
@ -203,17 +205,18 @@ def get_member_based_on_subscription(subscription_id, email):
return None return None
def verify_signature(data): def verify_signature(data, endpoint="Membership"):
if frappe.flags.in_test: if frappe.flags.in_test or os.environ.get("CI"):
return True return True
signature = frappe.request.headers.get("X-Razorpay-Signature") signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Membership Settings") settings = frappe.get_doc("Non Profit Settings")
key = settings.get_webhook_secret() key = settings.get_webhook_secret(endpoint)
controller = frappe.get_doc("Razorpay Settings") controller = frappe.get_doc("Razorpay Settings")
controller.verify_signature(data, signature, key) controller.verify_signature(data, signature, key)
frappe.set_user(settings.creation_user)
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
@ -222,7 +225,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
try: try:
verify_signature(data) verify_signature(data)
except Exception as e: except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error") log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log) notify_failure(log)
return { "status": "Failed", "reason": e} return { "status": "Failed", "reason": e}
@ -250,16 +253,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member.subscription_id = subscription.id member.subscription_id = subscription.id
member.customer_id = payment.customer_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 # Update Membership
membership = frappe.new_doc("Membership") membership = frappe.new_doc("Membership")
membership.update({ membership.update({
"company": company,
"member": member.name, "member": member.name,
"membership_status": "Current", "membership_status": "Current",
"membership_type": member.membership_type, "membership_type": member.membership_type,
@ -270,13 +272,20 @@ def trigger_razorpay_subscription(*args, **kwargs):
"to_date": datetime.fromtimestamp(subscription.current_end), "to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise "amount": payment.amount / 100 # Convert to rupees from paise
}) })
membership.insert(ignore_permissions=True) membership.flags.ignore_mandatory = True
membership.insert()
# Update membership values # Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1 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: except Exception as e:
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) 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)) 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" } 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): def notify_failure(log):
try: try:
content = """ content = """

View File

@ -10,33 +10,7 @@ from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase): class TestMembership(unittest.TestCase):
def setUp(self): def setUp(self):
# Get default company plan = setup_membership()
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")
# make test member # make test member
self.member_doc = create_member(frappe._dict({ self.member_doc = create_member(frappe._dict({
@ -78,7 +52,7 @@ class TestMembership(unittest.TestCase):
}) })
def set_config(key, value): 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={}): def make_membership(member, payload={}):
data = { data = {
@ -109,3 +83,36 @@ def create_item(item_code):
else: else:
item = frappe.get_doc("Item", item_code) item = frappe.get_doc("Item", item_code)
return item 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

View File

@ -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
}

View File

@ -3,11 +3,11 @@
frappe.ui.form.on('Membership Type', { frappe.ui.form.on('Membership Type', {
refresh: function (frm) { 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); 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); if (val) frm.set_df_property('linked_item', 'hidden', false);
}); });

View File

@ -1,16 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Membership Settings", { frappe.ui.form.on("Non Profit Settings", {
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.webhook_secret) {
frm.add_custom_button(__("Revoke <Key></Key>"), () => {
frm.call("revoke_key").then(() => {
frm.refresh();
})
});
}
frm.set_query("inv_print_format", function() { frm.set_query("inv_print_format", function() {
return { return {
filters: { 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"]; var account_types = ["Bank", "Cash"];
return { return {
filters: { 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"; let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true); frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
frm.trigger("setup_buttons_for_membership");
frm.trigger("add_generate_button"); frm.trigger("setup_buttons_for_donation");
frm.trigger("add_copy_buttonn");
}, },
add_generate_button: function(frm) { setup_buttons_for_membership: function(frm) {
let label; 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"); label = __("Regenerate Webhook Secret");
} else { } else {
label = __("Generate Webhook Secret"); label = __("Generate Webhook Secret");
} }
frm.add_custom_button(label, () => { frm.add_custom_button(label, () => {
frm.call("generate_webhook_key").then(() => { frm.call("generate_webhook_secret", {
field: "membership_webhook_secret"
}).then(() => {
frm.refresh(); frm.refresh();
}); });
}); }, __("Memberships"));
}, },
add_copy_buttonn: function(frm) { setup_buttons_for_donation: function(frm) {
if (frm.doc.webhook_secret) { let label;
if (frm.doc.donation_webhook_secret) {
label = __("Regenerate Webhook Secret");
frm.add_custom_button(__("Copy Webhook URL"), () => { 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"));
} }
}); });

View File

@ -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
}

View File

@ -8,23 +8,26 @@ from frappe import _
from frappe.integrations.utils import get_payment_gateway_controller from frappe.integrations.utils import get_payment_gateway_controller
from frappe.model.document import Document from frappe.model.document import Document
class MembershipSettings(Document): class NonProfitSettings(Document):
def generate_webhook_key(self): def generate_webhook_secret(self, field="membership_webhook_secret"):
key = frappe.generate_hash(length=20) key = frappe.generate_hash(length=20)
self.webhook_secret = key self.set(field, key)
self.save() self.save()
secret_for = "Membership" if field == "membership_webhook_secret" else "Donation"
frappe.msgprint( frappe.msgprint(
_("Here is your webhook secret, this will be shown to you only once.") + "<br><br>" + key, _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "<br><br>" + key,
_("Webhook Secret") _("Webhook Secret")
); )
def revoke_key(self): def revoke_key(self, key):
self.webhook_secret = None; self.set(key, None)
self.save() self.save()
def get_webhook_secret(self): def get_webhook_secret(self, endpoint="Membership"):
return self.get_password(fieldname="webhook_secret", raise_exception=False) fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret"
return self.get_password(fieldname=fieldname, raise_exception=False)
@frappe.whitelist() @frappe.whitelist()
def get_plans_for_membership(*args, **kwargs): def get_plans_for_membership(*args, **kwargs):

View File

@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe # import frappe
import unittest import unittest
class TestMembershipSettings(unittest.TestCase): class TestNonProfitSettings(unittest.TestCase):
pass pass

View File

@ -10,6 +10,7 @@
"hide_custom": 0, "hide_custom": 0,
"icon": "non-profit", "icon": "non-profit",
"idx": 0, "idx": 0,
"is_default": 0,
"is_standard": 1, "is_standard": 1,
"label": "Non Profit", "label": "Non Profit",
"links": [ "links": [
@ -109,7 +110,7 @@
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Membership Settings", "label": "Membership Settings",
"link_to": "Membership Settings", "link_to": "Non Profit Settings",
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
@ -161,7 +162,7 @@
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Donor", "label": "Donation",
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
@ -184,9 +185,35 @@
"link_type": "DocType", "link_type": "DocType",
"onboard": 0, "onboard": 0,
"type": "Link" "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": "2020-12-01 13:38:38.351409", "modified": "2021-03-11 11:38:09.140655",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Non Profit", "module": "Non Profit",
"name": "Non Profit", "name": "Non Profit",
@ -201,8 +228,8 @@
"type": "DocType" "type": "DocType"
}, },
{ {
"label": "Membership Settings", "label": "Non Profit Settings",
"link_to": "Membership Settings", "link_to": "Non Profit Settings",
"type": "DocType" "type": "DocType"
}, },
{ {

View File

@ -756,3 +756,6 @@ erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition 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
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae

View File

@ -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)

View File

@ -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)

View File

@ -0,0 +1,16 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc('payroll', 'doctype', 'gratuity_rule')
frappe.reload_doc('payroll', 'doctype', 'gratuity_rule_slab')
frappe.reload_doc('payroll', 'doctype', 'gratuity_applicable_component')
if frappe.db.exists("Company", {"country": "India"}):
from erpnext.regional.india.setup import create_gratuity_rule
create_gratuity_rule()
if frappe.db.exists("Company", {"country": "United Arab Emirates"}):
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
create_gratuity_rule()

View File

@ -0,0 +1,72 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Gratuity', {
setup: function (frm) {
frm.set_query('salary_component', function () {
return {
filters: {
type: "Earning"
}
};
});
frm.set_query("expense_account", function () {
return {
filters: {
"root_type": "Expense",
"is_group": 0,
"company": frm.doc.company
}
};
});
frm.set_query("payable_account", function () {
return {
filters: {
"root_type": "Liability",
"is_group": 0,
"company": frm.doc.company
}
};
});
},
refresh: function (frm) {
if (frm.doc.docstatus === 1 && frm.doc.pay_via_salary_slip === 0 && frm.doc.status === "Unpaid") {
frm.add_custom_button(__("Create Payment Entry"), function () {
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 doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
});
}
},
employee: function (frm) {
frm.events.calculate_work_experience_and_amount(frm);
},
gratuity_rule: function (frm) {
frm.events.calculate_work_experience_and_amount(frm);
},
calculate_work_experience_and_amount: function (frm) {
if (frm.doc.employee && frm.doc.gratuity_rule) {
frappe.call({
method: "erpnext.payroll.doctype.gratuity.gratuity.calculate_work_experience_and_amount",
args: {
employee: frm.doc.employee,
gratuity_rule: frm.doc.gratuity_rule
}
}).then((r) => {
frm.set_value("current_work_experience", r.message['current_work_experience']);
frm.set_value("amount", r.message['amount']);
});
}
}
});

View File

@ -0,0 +1,232 @@
{
"actions": [],
"autoname": "HR-GRA-PAY-.#####",
"creation": "2020-08-05 20:52:13.024683",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"department",
"designation",
"column_break_3",
"posting_date",
"status",
"company",
"gratuity_rule",
"section_break_5",
"pay_via_salary_slip",
"payroll_date",
"salary_component",
"payable_account",
"expense_account",
"mode_of_payment",
"cost_center",
"column_break_15",
"current_work_experience",
"amount",
"paid_amount",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1,
"search_index": 1
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"read_only": 1,
"reqd": 1
},
{
"default": "1",
"fieldname": "pay_via_salary_slip",
"fieldtype": "Check",
"label": "Pay via Salary Slip"
},
{
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting date",
"reqd": 1
},
{
"depends_on": "eval: doc.pay_via_salary_slip == 1",
"fieldname": "salary_component",
"fieldtype": "Link",
"label": "Salary Component",
"mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1",
"options": "Salary Component"
},
{
"default": "0",
"fieldname": "current_work_experience",
"fieldtype": "Int",
"label": "Current Work Experience",
"read_only": 1
},
{
"default": "0",
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Total Amount",
"read_only": 1,
"reqd": 1
},
{
"default": "Draft",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Draft\nUnpaid\nPaid",
"read_only": 1,
"reqd": 1
},
{
"depends_on": "eval: doc.pay_via_salary_slip == 0",
"fieldname": "expense_account",
"fieldtype": "Link",
"label": "Expense Account",
"mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
"options": "Account"
},
{
"depends_on": "eval: doc.pay_via_salary_slip == 0",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
"options": "Mode of Payment"
},
{
"fieldname": "gratuity_rule",
"fieldtype": "Link",
"label": "Gratuity Rule",
"options": "Gratuity Rule",
"reqd": 1
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Payment Configuration"
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fetch_from": "employee.designation",
"fieldname": "designation",
"fieldtype": "Data",
"label": "Designation",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Gratuity",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "column_break_15",
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.pay_via_salary_slip == 1",
"fieldname": "payroll_date",
"fieldtype": "Date",
"label": "Payroll Date",
"mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1"
},
{
"default": "0",
"depends_on": "eval:doc.pay_via_salary_slip == 0",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount",
"read_only": 1
},
{
"depends_on": "eval: doc.pay_via_salary_slip == 0",
"fieldname": "payable_account",
"fieldtype": "Link",
"label": "Payable Account",
"mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
"options": "Account"
},
{
"depends_on": "eval: doc.pay_via_salary_slip == 0",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
"options": "Cost Center"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-02 18:21:11.971488",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Gratuity",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -0,0 +1,249 @@
# -*- 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 _, bold
from frappe.utils import flt, get_datetime, get_link_to_form
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController
from math import floor
class Gratuity(AccountsController):
def validate(self):
data = calculate_work_experience_and_amount(self.employee, self.gratuity_rule)
self.current_work_experience = data["current_work_experience"]
self.amount = data["amount"]
if self.docstatus == 1:
self.status = "Unpaid"
def on_submit(self):
if self.pay_via_salary_slip:
self.create_additional_salary()
else:
self.create_gl_entries()
def on_cancel(self):
self.ignore_linked_doctypes = ['GL Entry']
self.create_gl_entries(cancel=True)
def create_gl_entries(self, cancel=False):
gl_entries = self.get_gl_entries()
make_gl_entries(gl_entries, cancel)
def get_gl_entries(self):
gl_entry = []
# payable entry
if self.amount:
gl_entry.append(
self.get_gl_dict({
"account": self.payable_account,
"credit": self.amount,
"credit_in_account_currency": self.amount,
"against": self.expense_account,
"party_type": "Employee",
"party": self.employee,
"against_voucher_type": self.doctype,
"against_voucher": self.name,
"cost_center": self.cost_center
}, item=self)
)
# expense entries
gl_entry.append(
self.get_gl_dict({
"account": self.expense_account,
"debit": self.amount,
"debit_in_account_currency": self.amount,
"against": self.payable_account,
"cost_center": self.cost_center
}, item=self)
)
else:
frappe.throw(_("Total Amount can not be zero"))
return gl_entry
def create_additional_salary(self):
if self.pay_via_salary_slip:
additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = self.employee
additional_salary.salary_component = self.salary_component
additional_salary.overwrite_salary_structure_amount = 0
additional_salary.amount = self.amount
additional_salary.payroll_date = self.payroll_date
additional_salary.company = self.company
additional_salary.ref_doctype = self.doctype
additional_salary.ref_docname = self.name
additional_salary.submit()
def set_total_advance_paid(self):
paid_amount = frappe.db.sql("""
select ifnull(sum(debit_in_account_currency), 0) as paid_amount
from `tabGL Entry`
where against_voucher_type = 'Gratuity'
and against_voucher = %s
and party_type = 'Employee'
and party = %s
""", (self.name, self.employee), as_dict=1)[0].paid_amount
if flt(paid_amount) > self.amount:
frappe.throw(_("Row {0}# Paid Amount cannot be greater than Total amount"))
self.db_set("paid_amount", paid_amount)
if self.amount == self.paid_amount:
self.db_set("status", "Paid")
@frappe.whitelist()
def calculate_work_experience_and_amount(employee, gratuity_rule):
current_work_experience = calculate_work_experience(employee, gratuity_rule) or 0
gratuity_amount = calculate_gratuity_amount(employee, gratuity_rule, current_work_experience) or 0
return {'current_work_experience': current_work_experience, "amount": gratuity_amount}
def calculate_work_experience(employee, gratuity_rule):
total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value("Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"])
date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
if not relieving_date:
frappe.throw(_("Please set Relieving Date for employee: {0}").format(bold(get_link_to_form("Employee", employee))))
method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function")
employee_total_workings_days = calculate_employee_total_workings_days(employee, date_of_joining, relieving_date)
current_work_experience = employee_total_workings_days/total_working_days_per_year or 1
current_work_experience = get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee)
return current_work_experience
def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date ):
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on") or "Leave"
if payroll_based_on == "Leave":
total_lwp = get_non_working_days(employee, relieving_date, "On Leave")
employee_total_workings_days -= total_lwp
elif payroll_based_on == "Attendance":
total_absents = get_non_working_days(employee, relieving_date, "Absent")
employee_total_workings_days -= total_absents
return employee_total_workings_days
def get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee):
if method == "Round off Work Experience":
current_work_experience = round(current_work_experience)
else:
current_work_experience = floor(current_work_experience)
if current_work_experience < minimum_year_for_gratuity:
frappe.throw(_("Employee: {0} have to complete minimum {1} years for gratuity").format(bold(employee), minimum_year_for_gratuity))
return current_work_experience
def get_non_working_days(employee, relieving_date, status):
filters={
"docstatus": 1,
"status": status,
"employee": employee,
"attendance_date": ("<=", get_datetime(relieving_date))
}
if status == "On Leave":
lwp_leave_types = frappe.get_list("Leave Type", filters = {"is_lwp":1})
lwp_leave_types = [leave_type.name for leave_type in lwp_leave_types]
filters["leave_type"] = ("IN", lwp_leave_types)
record = frappe.get_all("Attendance", filters=filters, fields = ["COUNT(name) as total_lwp"])
return record[0].total_lwp if len(record) else 0
def calculate_gratuity_amount(employee, gratuity_rule, experience):
applicable_earnings_component = get_applicable_components(gratuity_rule)
total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule)
calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on")
gratuity_amount = 0
slabs = get_gratuity_rule_slabs(gratuity_rule)
slab_found = False
year_left = experience
for slab in slabs:
if calculate_gratuity_amount_based_on == "Current Slab":
slab_found, gratuity_amount = calculate_amount_based_on_current_slab(slab.from_year, slab.to_year,
experience, total_applicable_components_amount, slab.fraction_of_applicable_earnings)
if slab_found:
break
elif calculate_gratuity_amount_based_on == "Sum of all previous slabs":
if slab.to_year == 0 and slab.from_year == 0:
gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings
slab_found = True
break
if experience > slab.to_year and experience > slab.from_year and slab.to_year !=0:
gratuity_amount += (slab.to_year - slab.from_year) * total_applicable_components_amount * slab.fraction_of_applicable_earnings
year_left -= (slab.to_year - slab.from_year)
slab_found = True
elif slab.from_year <= experience and (experience < slab.to_year or slab.to_year == 0):
gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings
slab_found = True
if not slab_found:
frappe.throw(_("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format(bold(gratuity_rule)))
return gratuity_amount
def get_applicable_components(gratuity_rule):
applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"])
if len(applicable_earnings_component) == 0:
frappe.throw(_("No Applicable Earnings Component found for Gratuity Rule: {0}").format(bold(get_link_to_form("Gratuity Rule",gratuity_rule))))
applicable_earnings_component = [component.salary_component for component in applicable_earnings_component]
return applicable_earnings_component
def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule):
sal_slip = get_last_salary_slip(employee)
if not sal_slip:
frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee)))
component_and_amounts = frappe.get_list("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
"parentfield": "earnings",
'salary_component': ('in', applicable_earnings_component)
},
fields=["amount"])
total_applicable_components_amount = 0
if not len(component_and_amounts):
frappe.throw(_("No Applicable Component is present in last month salary slip"))
for data in component_and_amounts:
total_applicable_components_amount += data.amount
return total_applicable_components_amount
def calculate_amount_based_on_current_slab(from_year, to_year, experience, total_applicable_components_amount, fraction_of_applicable_earnings):
slab_found = False; gratuity_amount = 0
if experience >= from_year and (to_year == 0 or experience < to_year):
gratuity_amount = total_applicable_components_amount * experience * fraction_of_applicable_earnings
if fraction_of_applicable_earnings:
slab_found = True
return slab_found, gratuity_amount
def get_gratuity_rule_slabs(gratuity_rule):
return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx")
def get_salary_structure(employee):
return frappe.get_list("Salary Structure Assignment", filters = {
"employee": employee, 'docstatus': 1
},
fields=["from_date", "salary_structure"],
order_by = "from_date desc")[0].salary_structure
def get_last_salary_slip(employee):
return frappe.get_list("Salary Slip", filters = {
"employee": employee, 'docstatus': 1
},
order_by = "start_date desc")[0].name

View File

@ -0,0 +1,20 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'reference_name',
'non_standard_fieldnames': {
'Additional Salary': 'ref_docname',
},
'transactions': [
{
'label': _('Payment'),
'items': ['Payment Entry']
},
{
'label': _('Additional Salary'),
'items': ['Additional Salary']
}
]
}

View File

@ -0,0 +1,192 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, make_earning_salary_component, \
make_deduction_salary_component
from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
from frappe.utils import getdate, add_days, get_datetime, flt
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
class TestGratuity(unittest.TestCase):
def setUp(self):
make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company'])
frappe.db.sql("DELETE FROM `tabGratuity`")
frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
employee, sal_slip = create_employee_and_get_last_salary_slip()
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
gratuity = create_gratuity(pay_via_salary_slip = 1, employee=employee, rule=rule.name)
#work experience calculation
date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
experience = employee_total_workings_days/rule.total_working_days_per_year
gratuity.reload()
from math import floor
self.assertEqual(floor(experience), gratuity.current_work_experience)
#amount Calculation
component_amount = frappe.get_list("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
"parentfield": "earnings",
'salary_component': "Basic Salary"
},
fields=["amount"])
''' 5 - 0 fraction is 1 '''
gratuity_amount = component_amount[0].amount * experience
gratuity.reload()
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
#additional salary creation (Pay via salary slip)
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
def test_check_gratuity_amount_based_on_all_previous_slabs(self):
employee, sal_slip = create_employee_and_get_last_salary_slip()
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
set_mode_of_payment_account()
gratuity = create_gratuity(expense_account = 'Payment Account - _TC', mode_of_payment='Cash', employee=employee)
#work experience calculation
date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date'])
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days
experience = employee_total_workings_days/rule.total_working_days_per_year
gratuity.reload()
from math import floor
self.assertEqual(floor(experience), gratuity.current_work_experience)
#amount Calculation
component_amount = frappe.get_list("Salary Detail",
filters={
"docstatus": 1,
'parent': sal_slip,
"parentfield": "earnings",
'salary_component': "Basic Salary"
},
fields=["amount"])
''' range | Fraction
0-1 | 0
1-5 | 0.7
5-0 | 1
'''
gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
gratuity.reload()
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
self.assertEqual(gratuity.status, "Unpaid")
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
pay_entry = get_payment_entry("Gratuity", gratuity.name)
pay_entry.reference_no = "123467"
pay_entry.reference_date = getdate()
pay_entry.save()
pay_entry.submit()
gratuity.reload()
self.assertEqual(gratuity.status, "Paid")
self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2))
def tearDown(self):
frappe.db.sql("DELETE FROM `tabGratuity`")
frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
def get_gratuity_rule(name):
rule = frappe.db.exists("Gratuity Rule", name)
if not rule:
create_gratuity_rule()
rule = frappe.get_doc("Gratuity Rule", name)
rule.applicable_earnings_component = []
rule.append("applicable_earnings_component", {
"salary_component": "Basic Salary"
})
rule.save()
rule.reload()
return rule
def create_gratuity(**args):
if args:
args = frappe._dict(args)
gratuity = frappe.new_doc("Gratuity")
gratuity.employee = args.employee
gratuity.posting_date = getdate()
gratuity.gratuity_rule = args.rule or "Rule Under Limited Contract (UAE)"
gratuity.pay_via_salary_slip = args.pay_via_salary_slip or 0
if gratuity.pay_via_salary_slip:
gratuity.payroll_date = getdate()
gratuity.salary_component = "Performance Bonus"
else:
gratuity.expense_account = args.expense_account or 'Payment Account - _TC'
gratuity.payable_account = args.payable_account or get_payable_account("_Test Company")
gratuity.mode_of_payment = args.mode_of_payment or 'Cash'
gratuity.save()
gratuity.submit()
return gratuity
def set_mode_of_payment_account():
if not frappe.db.exists("Account", "Payment Account - _TC"):
mode_of_payment = create_account()
mode_of_payment = frappe.get_doc("Mode of Payment", "Cash")
mode_of_payment.accounts = []
mode_of_payment.append("accounts", {
"company": "_Test Company",
"default_account": "_Test Bank - _TC"
})
mode_of_payment.save()
def create_account():
return frappe.get_doc({
"doctype": "Account",
"company": "_Test Company",
"account_name": "Payment Account",
"root_type": "Asset",
"report_type": "Balance Sheet",
"currency": "INR",
"parent_account": "Bank Accounts - _TC",
"account_type": "Bank",
}).insert(ignore_permissions=True)
def create_employee_and_get_last_salary_slip():
employee = make_employee("test_employee@salary.com", company='_Test Company')
frappe.db.set_value("Employee", employee, "relieving_date", getdate())
frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), - (6*365)))
if not frappe.db.exists("Salary Slip", {"employee":employee}):
salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly")
salary_slip.submit()
salary_slip = salary_slip.name
else:
salary_slip = get_last_salary_slip(employee)
if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"):
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
make_holiday_list()
frappe.db.set_value("Company", '_Test Company', "default_holiday_list", "Salary Slip Test Holiday List")
return employee, salary_slip

View File

@ -0,0 +1,32 @@
{
"actions": [],
"creation": "2020-08-05 19:00:28.097265",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"salary_component"
],
"fields": [
{
"fieldname": "salary_component",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Salary Component ",
"options": "Salary Component",
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-08-05 20:17:13.855035",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Gratuity Applicable Component",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- 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.model.document import Document
class GratuityApplicableComponent(Document):
pass

View File

@ -0,0 +1,40 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Gratuity Rule', {
// refresh: function(frm) {
// }
});
frappe.ui.form.on('Gratuity Rule Slab', {
/*
Slabs should be in order like
from | to | fraction
0 | 4 | 0.5
4 | 6 | 0.7
So, on row addition setting current_row.from = previous row.to.
On to_year insert we have to check that it is not less than from_year
Wrong order may lead to Wrong Calculation
*/
gratuity_rule_slabs_add(frm, cdt, cdn) {
let row = locals[cdt][cdn];
let array_idx = row.idx - 1;
if (array_idx > 0) {
row.from_year = cur_frm.doc.gratuity_rule_slabs[array_idx - 1].to_year;
frm.refresh();
}
},
to_year(frm, cdt, cdn) {
let row = locals[cdt][cdn];
if (row.to_year <= row.from_year && row.to_year === 0) {
frappe.throw(__("To(Year) year can not be less than From(year) "));
}
}
});

View File

@ -0,0 +1,114 @@
{
"actions": [],
"autoname": "Prompt",
"creation": "2020-08-05 19:00:36.103500",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"applicable_earnings_component",
"work_experience_calculation_function",
"total_working_days_per_year",
"column_break_3",
"disable",
"calculate_gratuity_amount_based_on",
"minimum_year_for_gratuity",
"gratuity_rules_section",
"gratuity_rule_slabs"
],
"fields": [
{
"default": "0",
"fieldname": "disable",
"fieldtype": "Check",
"label": "Disable"
},
{
"fieldname": "calculate_gratuity_amount_based_on",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Calculate Gratuity Amount Based On",
"options": "Current Slab\nSum of all previous slabs",
"reqd": 1
},
{
"description": "Salary components should be part of the Salary Structure.",
"fieldname": "applicable_earnings_component",
"fieldtype": "Table MultiSelect",
"label": "Applicable Earnings Component",
"options": "Gratuity Applicable Component",
"reqd": 1
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "gratuity_rules_section",
"fieldtype": "Section Break",
"label": "Gratuity Rules"
},
{
"description": "Leave <b>From</b> and <b>To</b> 0 for no upper and lower limit.",
"fieldname": "gratuity_rule_slabs",
"fieldtype": "Table",
"label": "Current Work Experience",
"options": "Gratuity Rule Slab",
"reqd": 1
},
{
"default": "Round off Work Experience",
"fieldname": "work_experience_calculation_function",
"fieldtype": "Select",
"label": "Work Experience Calculation method",
"options": "Round off Work Experience\nTake Exact Completed Years"
},
{
"default": "365",
"fieldname": "total_working_days_per_year",
"fieldtype": "Int",
"label": "Total working Days Per Year"
},
{
"fieldname": "minimum_year_for_gratuity",
"fieldtype": "Int",
"label": "Minimum Year for Gratuity"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2020-12-03 17:08:27.891535",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Gratuity Rule",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,33 @@
# -*- 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.model.document import Document
from frappe import _
class GratuityRule(Document):
def validate(self):
for current_slab in self.gratuity_rule_slabs:
if (current_slab.from_year > current_slab.to_year) and current_slab.to_year != 0:
frappe(_("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx))
if current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1:
frappe.throw(_("You can not define multiple slabs if you have a slab with no lower and upper limits."))
def get_gratuity_rule(name, slabs, **args):
args = frappe._dict(args)
rule = frappe.new_doc("Gratuity Rule")
rule.name = name
rule.calculate_gratuity_amount_based_on = args.calculate_gratuity_amount_based_on or "Current Slab"
rule.work_experience_calculation_method = args.work_experience_calculation_method or "Take Exact Completed Years"
rule.minimum_year_for_gratuity = 1
for slab in slabs:
slab = frappe._dict(slab)
rule.append("gratuity_rule_slabs", slab)
return rule

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'gratuity_rule',
'transactions': [
{
'label': _('Gratuity'),
'items': ['Gratuity']
}
]
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
# import frappe
import unittest
class TestGratuityRule(unittest.TestCase):
pass

View File

@ -0,0 +1,50 @@
{
"actions": [],
"creation": "2020-08-05 19:12:49.423500",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"from_year",
"to_year",
"fraction_of_applicable_earnings"
],
"fields": [
{
"fieldname": "fraction_of_applicable_earnings",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Fraction of Applicable Earnings ",
"reqd": 1
},
{
"default": "0",
"fieldname": "from_year",
"fieldtype": "Int",
"in_list_view": 1,
"label": "From(Year)",
"read_only": 1,
"reqd": 1
},
{
"default": "0",
"fieldname": "to_year",
"fieldtype": "Int",
"in_list_view": 1,
"label": "To(Year)",
"reqd": 1
}
],
"istable": 1,
"links": [],
"modified": "2020-08-17 14:09:56.781712",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Gratuity Rule Slab",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,10 @@
# -*- 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.model.document import Document
class GratuityRuleSlab(Document):
pass

View File

@ -80,9 +80,26 @@ class SalarySlip(TransactionBase):
if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip() self.email_salary_slip()
self.update_payment_status_for_gratuity()
def update_payment_status_for_gratuity(self):
add_salary = frappe.db.get_all("Additional Salary",
filters = {
"payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
"employee": self.employee,
"ref_doctype": "Gratuity",
"docstatus": 1,
}, fields = ["ref_docname", "name"], limit=1)
if len(add_salary):
status = "Paid" if self.docstatus == 1 else "Unpaid"
if add_salary[0].name in [data.additional_salary for data in self.earnings]:
frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
def on_cancel(self): def on_cancel(self):
self.set_status() self.set_status()
self.update_status() self.update_status()
self.update_payment_status_for_gratuity()
self.cancel_loan_repayment_entry() self.cancel_loan_repayment_entry()
def on_trash(self): def on_trash(self):
@ -574,6 +591,7 @@ class SalarySlip(TransactionBase):
for d in self.get(key): for d in self.get(key):
if d.salary_component == struct_row.salary_component: if d.salary_component == struct_row.salary_component:
component_row = d component_row = d
if not component_row or (struct_row.get("is_additional_component") and not overwrite): if not component_row or (struct_row.get("is_additional_component") and not overwrite):
if amount: if amount:
self.append(key, { self.append(key, {

View File

@ -21,6 +21,7 @@ from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_ta
class TestSalarySlip(unittest.TestCase): class TestSalarySlip(unittest.TestCase):
def setUp(self): def setUp(self):
setup_test() setup_test()
def tearDown(self): def tearDown(self):
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@ -1885,7 +1885,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.throw(__("Please enter Item Code to get batch no")); frappe.throw(__("Please enter Item Code to get batch no"));
} else if (doc.doctype == "Purchase Receipt" || } else if (doc.doctype == "Purchase Receipt" ||
(doc.doctype == "Purchase Invoice" && doc.update_stock)) { (doc.doctype == "Purchase Invoice" && doc.update_stock)) {
return { return {
filters: {'item': item.item_code} filters: {'item': item.item_code}
} }
@ -1911,9 +1910,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
set_query_for_item_tax_template: function(doc, cdt, cdn) { set_query_for_item_tax_template: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn); var item = frappe.get_doc(cdt, cdn);
if(!item.item_code) { if(!item.item_code) {
frappe.throw(__("Please enter Item Code to get item taxes")); return doc.company ? {filters: {company: doc.company}} : {};
} else { } else {
let filters = { let filters = {
'item_code': item.item_code, 'item_code': item.item_code,
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
@ -2124,4 +2122,4 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => {
} }
} }
}); });
}; };

View File

@ -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'));
}
}
});

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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
}

View File

@ -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

View File

@ -21,6 +21,7 @@ def setup_company_independent_fixtures():
add_permissions() add_permissions()
add_custom_roles_for_reports() add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
create_gratuity_rule()
add_print_formats() add_print_formats()
def add_hsn_sac_codes(): def add_hsn_sac_codes():
@ -399,9 +400,9 @@ def make_custom_fields(update=True):
si_einvoice_fields = [ si_einvoice_fields = [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, 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'), 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_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='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, dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
@ -499,6 +500,14 @@ def make_custom_fields(update=True):
fieldtype='Link', options='Salary Component', insert_after='basic_component'), fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='arrear_component', label='Arrear Component', dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_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':[ 'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption', dict(fieldname='hra_section', label='HRA Exemption',
@ -581,7 +590,15 @@ def make_custom_fields(update=True):
'options': '\nWith Payment of Tax\nWithout Payment of Tax' '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', 'fieldname': 'pan_number',
'label': 'PAN Details', 'label': 'PAN Details',
@ -643,7 +660,7 @@ def set_tax_withholding_category(company):
pass pass
docs = get_tds_details(accounts, fiscal_year) docs = get_tds_details(accounts, fiscal_year)
for d in docs: for d in docs:
try: try:
doc = frappe.get_doc(d) doc = frappe.get_doc(d)
@ -661,7 +678,7 @@ def set_tax_withholding_category(company):
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year] fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
if not fy_exist: if not fy_exist:
doc.append("rates", d.get('rates')[0]) doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.save() doc.save()
@ -823,4 +840,24 @@ def get_tds_details(accounts, fiscal_year):
doctype="Tax Withholding Category", accounts=accounts, doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}]) "single_threshold": 2500, "cumulative_threshold": 0}])
] ]
def create_gratuity_rule():
# Standard Indain Gratuity Rule
if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
rule = frappe.new_doc("Gratuity Rule")
rule.name = "Indian Standard Gratuity Rule"
rule.calculate_gratuity_amount_based_on = "Current Slab"
rule.work_experience_calculation_method = "Round Off Work Experience"
rule.minimum_year_for_gratuity = 5
fraction = 15/26
rule.append("gratuity_rule_slabs", {
"from_year": 0,
"to_year":0,
"fraction_of_applicable_earnings": fraction
})
rule.flags.ignore_mandatory = True
rule.save()

View File

@ -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 <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} 80G Donor Certificate</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n\n This is to confirm that the {{ doc.company }} received an amount of <b>{{doc.get_formatted(\"amount\")}}</b>\n from <b>{{ doc.donor_name }}</b>\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 <br><br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
"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"
}

View File

@ -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 <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} Members 80G Donor Certificate</h3>\n <h3 class=\"text-center\">Financial Cycle {{ doc.fiscal_year }}</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n This is to confirm that the {{ doc.company }} received a total amount of <b>{{doc.get_formatted(\"total\")}}</b>\n from <b>{{ doc.member_name }}</b>\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 <br><br>\n <table class=\"table table-bordered table-condensed\">\n \t<thead>\n \t\t<tr>\n \t\t\t<th >{{ _(\"Date\") }}</th>\n \t\t\t<th class=\"text-right\">{{ _(\"Amount\") }}</th>\n \t\t\t<th class=\"text-right\">{{ _(\"Invoice ID\") }}</th>\n \t\t</tr>\n \t</thead>\n \t<tbody>\n \t\t{%- for payment in doc.payments -%}\n \t\t<tr>\n \t\t\t<td> {{ payment.date }} </td>\n \t\t\t<td class=\"text-right\">{{ payment.get_formatted(\"amount\") }}</td>\n \t\t\t<td class=\"text-right\">{{ payment.invoice_id }}</td>\n \t\t</tr>\n \t\t{%- endfor -%}\n \t</tbody>\n </table>\n \n <br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
"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"
}

View File

@ -6,13 +6,14 @@ from __future__ import unicode_literals
import frappe, os, json import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property from frappe.permissions import add_permission, update_permission_property
from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule
def setup(company=None, patch=True): def setup(company=None, patch=True):
make_custom_fields() make_custom_fields()
add_print_formats() add_print_formats()
add_custom_roles_for_reports() add_custom_roles_for_reports()
add_permissions() add_permissions()
create_gratuity_rule()
def make_custom_fields(): def make_custom_fields():
is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated',
@ -153,3 +154,93 @@ def add_permissions():
add_permission(doctype, role, 0) add_permission(doctype, role, 0)
update_permission_property(doctype, role, 0, 'write', 1) update_permission_property(doctype, role, 0, 'write', 1)
update_permission_property(doctype, role, 0, 'create', 1) update_permission_property(doctype, role, 0, 'create', 1)
def create_gratuity_rule():
rule_1 = rule_2 = rule_3 = None
# Rule Under Limited Contract
slabs = get_slab_for_limited_contract()
if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"):
rule_1 = get_gratuity_rule("Rule Under Limited Contract (UAE)", slabs, calculate_gratuity_amount_based_on="Sum of all previous slabs")
# Rule Under Unlimited Contract on termination
slabs = get_slab_for_unlimited_contract_on_termination()
if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)"):
rule_2 = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)", slabs)
# Rule Under Unlimited Contract on resignation
slabs = get_slab_for_unlimited_contract_on_resignation()
if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"):
rule_3 = get_gratuity_rule("Rule Under Unlimited Contract on resignation (UAE)", slabs)
#for applicable salary component user need to set this by its own
if rule_1:
rule_1.flags.ignore_mandatory = True
rule_1.save()
if rule_2:
rule_2.flags.ignore_mandatory = True
rule_2.save()
if rule_3:
rule_3.flags.ignore_mandatory = True
rule_3.save()
def get_slab_for_limited_contract():
return [{
"from_year": 0,
"to_year":1,
"fraction_of_applicable_earnings": 0
},
{
"from_year": 1,
"to_year":5,
"fraction_of_applicable_earnings": 21/30
},
{
"from_year": 5,
"to_year":0,
"fraction_of_applicable_earnings": 1
}]
def get_slab_for_unlimited_contract_on_termination():
return [{
"from_year": 0,
"to_year":1,
"fraction_of_applicable_earnings": 0
},
{
"from_year": 1,
"to_year":5,
"fraction_of_applicable_earnings": 21/30
},
{
"from_year": 5,
"to_year":0,
"fraction_of_applicable_earnings": 1
}]
def get_slab_for_unlimited_contract_on_resignation():
fraction_1 = 1/3 * 21/30
fraction_2 = 2/3 * 21/30
fraction_3 = 21/30
return [{
"from_year": 0,
"to_year":1,
"fraction_of_applicable_earnings": 0
},
{
"from_year": 1,
"to_year":3,
"fraction_of_applicable_earnings": fraction_1
},
{
"from_year": 3,
"to_year":5,
"fraction_of_applicable_earnings": fraction_2
},
{
"from_year": 5,
"to_year":0,
"fraction_of_applicable_earnings": fraction_3
}]

View File

@ -195,6 +195,7 @@ def install(country=None):
{'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"},
{'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, {'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": "Hub"},
{'doctype': "Opportunity Type", "name": _("Sales")}, {'doctype': "Opportunity Type", "name": _("Sales")},