diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
index 89465eedf0e..ee501f664b6 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
@@ -63,17 +63,21 @@
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
"account_number": "1371"
},
- "Abziehbare VSt. 7%": {
- "account_number": "1571"
- },
- "Abziehbare VSt. 19%": {
- "account_number": "1576"
- },
- "Abziehbare VStr. nach \u00a713b UStG 19%": {
- "account_number": "1577"
- },
- "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
- "account_number": "3120"
+ "Abziehbare Vorsteuer": {
+ "account_type": "Tax",
+ "is_group": 1,
+ "Abziehbare Vorsteuer 7%": {
+ "account_number": "1571"
+ },
+ "Abziehbare Vorsteuer 19%": {
+ "account_number": "1576"
+ },
+ "Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
+ "account_number": "1577"
+ },
+ "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
+ "account_number": "3120"
+ }
}
},
"III. Wertpapiere": {
@@ -196,6 +200,7 @@
},
"Umsatzsteuer": {
"is_group": 1,
+ "account_type": "Tax",
"Umsatzsteuer 7%": {
"account_number": "1771"
},
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json
index 7fa67081341..57e8bdd9dc7 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json
@@ -292,18 +292,21 @@
"Umsatzsteuerforderungen fr\u00fchere Jahre": {}
},
"Sonstige Verm\u00f6gensgegenst\u00e4nde oder sonstige Verbindlichkeiten": {
- "Abziehbare Vorsteuer": {},
- "Abziehbare Vorsteuer 16%": {},
- "Abziehbare Vorsteuer 19%": {},
- "Abziehbare Vorsteuer 7%": {},
- "Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {},
- "Abziehbare Vorsteuer nach \u00a7 13b UStG ": {},
- "Abziehbare Vorsteuer nach \u00a7 13b UStG 16%": {},
- "Abziehbare Vorsteuer nach \u00a7 13b UStG 19%": {},
+ "Abziehbare Vorsteuer": {
+ "account_type": "Tax",
+ "is_group": 1,
+ "Abziehbare Vorsteuer 16%": {},
+ "Abziehbare Vorsteuer 19%": {},
+ "Abziehbare Vorsteuer 7%": {},
+ "Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {},
+ "Abziehbare Vorsteuer nach \u00a7 13b UStG ": {},
+ "Abziehbare Vorsteuer nach \u00a7 13b UStG 16%": {},
+ "Abziehbare Vorsteuer nach \u00a7 13b UStG 19%": {}
+ },
"Aufl\u00f6sung Vorsteuer aus Vorjahr \u00a7 4/3 EStG": {},
"Aufzuteilende Vorsteuer": {},
"Aufzuteilende Vorsteuer 16%": {},
@@ -673,23 +676,26 @@
"Sonstige Verrechnungskonten (Interimskonto)": {
"account_type": "Stock Received But Not Billed"
},
- "Umsatzsteuer": {},
- "Umsatzsteuer 16%": {},
- "Umsatzsteuer 19%": {},
- "Umsatzsteuer 7%": {},
- "Umsatzsteuer Vorjahr": {},
- "Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {},
- "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {},
- "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {},
- "Umsatzsteuer fr\u00fchere Jahre": {},
- "Umsatzsteuer laufendes Jahr": {},
- "Umsatzsteuer nach \u00a713b UStG": {},
- "Umsatzsteuer nach \u00a713b UStG 16%": {},
- "Umsatzsteuer nach \u00a713b UStG 19%": {},
+ "Umsatzsteuer": {
+ "account_type": "Tax",
+ "is_group": 1,
+ "Umsatzsteuer 16%": {},
+ "Umsatzsteuer 19%": {},
+ "Umsatzsteuer 7%": {},
+ "Umsatzsteuer Vorjahr": {},
+ "Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {},
+ "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {},
+ "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {},
+ "Umsatzsteuer fr\u00fchere Jahre": {},
+ "Umsatzsteuer laufendes Jahr": {},
+ "Umsatzsteuer nach \u00a713b UStG": {},
+ "Umsatzsteuer nach \u00a713b UStG 16%": {},
+ "Umsatzsteuer nach \u00a713b UStG 19%": {}
+ },
"Umsatzsteuer- Vorauszahlungen": {},
"Umsatzsteuer- Vorauszahlungen 1/11": {},
"Verbindlichkeiten aus Lohn- und Kirchensteuer": {}
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
index 849df18c6f9..2bf55cfcd04 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
@@ -659,6 +659,7 @@
},
"Abziehbare Vorsteuer (Gruppe)": {
"is_group": 1,
+ "account_type": "Tax",
"Abziehbare Vorsteuer": {
"account_number": "1400"
},
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
index 8915f79b926..77c9e95b759 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
@@ -1,7 +1,7 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
- "autoname": "field:title",
"creation": "2018-11-22 22:45:00.370913",
"doctype": "DocType",
"document_type": "Setup",
@@ -20,8 +20,7 @@
"in_list_view": 1,
"label": "Title",
"no_copy": 1,
- "reqd": 1,
- "unique": 1
+ "reqd": 1
},
{
"fieldname": "taxes",
@@ -33,12 +32,14 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
}
],
- "modified": "2020-09-18 17:26:09.703215",
+ "links": [],
+ "modified": "2021-03-08 19:50:21.416513",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -81,5 +82,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
index e77481d44f5..d9155cbab4a 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
@@ -11,6 +11,11 @@ class ItemTaxTemplate(Document):
def validate(self):
self.validate_tax_accounts()
+ def autoname(self):
+ if self.company and self.title:
+ abbr = frappe.get_cached_value('Company', self.company, 'abbr')
+ self.name = '{0} - {1}'.format(self.title, abbr)
+
def validate_tax_accounts(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""
check_list = []
diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
index d54a47e3c96..32473694c80 100644
--- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
+++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
@@ -12,7 +12,7 @@ class ModeofPayment(Document):
self.validate_accounts()
self.validate_repeating_companies()
self.validate_pos_mode_of_payment()
-
+
def validate_repeating_companies(self):
"""Error when Same Company is entered multiple times in accounts"""
accounts_list = []
@@ -31,10 +31,10 @@ class ModeofPayment(Document):
def validate_pos_mode_of_payment(self):
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))
pos_profiles = list(map(lambda x: x[0], pos_profiles))
-
+
if pos_profiles:
message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \
Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode."
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 76027a301f3..e6449b78316 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -198,6 +198,7 @@ def start_import(invoices):
try:
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
+ doc.flags.ignore_mandatory = True
doc.insert()
doc.submit()
frappe.db.commit()
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index f5c488d0f97..6412772073c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -92,14 +92,16 @@ frappe.ui.form.on('Payment Entry', {
});
frm.set_query("reference_doctype", "references", function() {
- if (frm.doc.party_type=="Customer") {
+ if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
- } else if (frm.doc.party_type=="Supplier") {
+ } else if (frm.doc.party_type == "Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
- } else if (frm.doc.party_type=="Employee") {
+ } else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"];
- } else if (frm.doc.party_type=="Student") {
+ } else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"];
+ } else if (frm.doc.party_type == "Donor") {
+ var doctypes = ["Donation"];
} else {
var doctypes = ["Journal Entry"];
}
@@ -128,7 +130,7 @@ frappe.ui.form.on('Payment Entry', {
const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
- 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning'];
+ 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation'];
if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party;
@@ -281,7 +283,7 @@ frappe.ui.form.on('Payment Entry', {
let party_types = Object.keys(frappe.boot.party_account_types);
if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){
frm.set_value("party_type", "");
- frappe.throw(__("Party can only be one of "+ party_types.join(", ")));
+ frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")]));
}
frm.set_query("party", function() {
@@ -705,7 +707,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) {
if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount)
@@ -748,7 +751,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) {
if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
@@ -905,6 +909,12 @@ frappe.ui.form.on('Payment Entry', {
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false;
}
+
+ if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") {
+ frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
+ frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx]));
+ return false;
+ }
}
if (row) {
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 2e1f201e253..328584a61a0 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -536,7 +536,8 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"depends_on": "party",
@@ -588,7 +589,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:56:20.007336",
+ "modified": "2021-03-08 13:05:16.958866",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -632,4 +633,4 @@
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 31a4c8a3879..8acd92cb6b5 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -72,6 +72,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_expense_claim()
+ self.update_donation()
self.update_payment_schedule()
self.set_status()
@@ -82,6 +83,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_expense_claim()
+ self.update_donation(cancel=1)
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
@@ -242,9 +244,11 @@ class PaymentEntry(AccountsController):
elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
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":
valid_reference_doctypes = ("Journal Entry")
+ elif self.party_type == "Donor":
+ valid_reference_doctypes = ("Donation")
for d in self.get("references"):
if not d.allocated_amount:
@@ -455,6 +459,10 @@ class PaymentEntry(AccountsController):
.format(total_negative_outstanding), InvalidPaymentEntry)
def set_title(self):
+ if frappe.flags.in_import and self.title:
+ # do not set title dynamically if title exists during data import.
+ return
+
if self.payment_type in ("Receive", "Pay"):
self.title = self.party
else:
@@ -604,7 +612,7 @@ class PaymentEntry(AccountsController):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
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()
def update_expense_claim(self):
@@ -614,6 +622,13 @@ class PaymentEntry(AccountsController):
doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, self.name)
+ def update_donation(self, cancel=0):
+ if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:
+ for d in self.get("references"):
+ if d.reference_doctype=="Donation" and d.reference_name:
+ is_paid = 0 if cancel else 1
+ frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
+
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name
self.reference_date = nowdate()
@@ -913,6 +928,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
total_amount = ref_doc.get("grand_total")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
+ elif reference_doctype == "Donation":
+ total_amount = ref_doc.get("amount")
+ exchange_rate = 1
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
@@ -932,6 +950,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
+ elif ref_doc.doctype == "Gratuity":
+ total_amount = ref_doc.amount
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
@@ -955,6 +975,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
+ elif reference_doctype == "Gratuity":
+ outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount)
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else:
@@ -996,7 +1018,7 @@ def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_curre
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc)
-
+
if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
party_account_currency, company_currency, ref_doc)
@@ -1160,10 +1182,12 @@ def set_party_type(dt):
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
- elif dt in ("Expense Claim", "Employee Advance"):
+ elif dt in ("Expense Claim", "Employee Advance", "Gratuity"):
party_type = "Employee"
- elif dt in ("Fees"):
+ elif dt == "Fees":
party_type = "Student"
+ elif dt == "Donation":
+ party_type = "Donor"
return party_type
def set_party_account(dt, dn, doc, party_type):
@@ -1177,6 +1201,8 @@ def set_party_account(dt, dn, doc, party_type):
party_account = doc.advance_account
elif dt == "Expense Claim":
party_account = doc.payable_account
+ elif dt == "Gratuity":
+ party_account = doc.payable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
@@ -1189,7 +1215,7 @@ def set_party_account_currency(dt, party_account, doc):
return party_account_currency
def set_payment_type(dt, doc):
- if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
+ if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
@@ -1222,6 +1248,12 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
+ elif dt == "Donation":
+ grand_total = doc.amount
+ outstanding_amount = doc.amount
+ elif dt == "Gratuity":
+ grand_total = doc.amount
+ outstanding_amount = flt(doc.amount) - flt(doc.paid_amount)
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
@@ -1326,4 +1358,4 @@ def make_payment_order(source_name, target_doc=None):
}, target_doc, set_missing_values)
- return doclist
\ No newline at end of file
+ return doclist
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 15875afe878..eb52fd62759 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -99,10 +99,10 @@ class TestPOSInvoice(unittest.TestCase):
item_row = inv.get("items")[0]
add_items = [
- (54, '_Test Account Excise Duty @ 12'),
- (288, '_Test Account Excise Duty @ 15'),
- (144, '_Test Account Excise Duty @ 20'),
- (430, '_Test Item Tax Template 1')
+ (54, '_Test Account Excise Duty @ 12 - _TC'),
+ (288, '_Test Account Excise Duty @ 15 - _TC'),
+ (144, '_Test Account Excise Duty @ 20 - _TC'),
+ (430, '_Test Item Tax Template 1 - _TC')
]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 451c9368816..18b66375e99 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -58,6 +58,7 @@
"rejected_warehouse",
"col_break_warehouse",
"set_from_warehouse",
+ "supplier_warehouse",
"is_subcontracted",
"items_section",
"update_stock",
@@ -1350,7 +1351,7 @@
"options": "Company"
},
{
- "depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)",
+ "depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
"description": "Sets 'From Warehouse' in each row of the items table.",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
@@ -1360,13 +1361,24 @@
"print_hide": 1,
"print_width": "50px",
"width": "50px"
+ },
+ {
+ "depends_on": "eval:doc.update_stock && doc.is_subcontracted==\"Yes\"",
+ "fieldname": "supplier_warehouse",
+ "fieldtype": "Link",
+ "label": "Supplier Warehouse",
+ "no_copy": 1,
+ "options": "Warehouse",
+ "print_hide": 1,
+ "print_width": "50px",
+ "width": "50px"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-26 20:49:03.305063",
+ "modified": "2021-03-09 21:12:30.422084",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index dacd50a3e24..5c4e32e493e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -968,7 +968,7 @@ class PurchaseInvoice(BuyingController):
# 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
# 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 = \
get_round_off_account_and_cost_center(self.company)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 2c088ce2b20..ded293b88d5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -456,7 +456,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
- return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
+ return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
@@ -1031,7 +1031,7 @@ def make_purchase_invoice_against_cost_center(**args):
pi.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No"
- if args.supplier_warehouse:
+ if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", {
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json
index 7030faf2b73..e7166c5a12d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_records.json
+++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json
@@ -18,7 +18,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"parentfield": "items",
"qty": 10,
"rate": 50,
diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index ee6419db20a..e00a58f8641 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_records.json
+++ b/erpnext/accounts/doctype/sales_invoice/test_records.json
@@ -148,7 +148,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"parentfield": "items",
"price_list_rate": 50,
"qty": 10,
@@ -276,7 +276,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"parentfield": "items",
"price_list_rate": 62.5,
"qty": 10,
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 7cd1828343b..1b9557839fe 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -405,10 +405,10 @@ class TestSalesInvoice(unittest.TestCase):
item_row = si.get("items")[0]
add_items = [
- (54, '_Test Account Excise Duty @ 12'),
- (288, '_Test Account Excise Duty @ 15'),
- (144, '_Test Account Excise Duty @ 20'),
- (430, '_Test Item Tax Template 1')
+ (54, '_Test Account Excise Duty @ 12 - _TC'),
+ (288, '_Test Account Excise Duty @ 15 - _TC'),
+ (144, '_Test Account Excise Duty @ 20 - _TC'),
+ (430, '_Test Item Tax Template 1 - _TC')
]
for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row)
@@ -2077,14 +2077,14 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
item.save()
item.append("taxes", {
- "item_tax_template": "_Test Item Tax Template 1",
+ "item_tax_template": "_Test Item Tax Template 1 - _TC",
"valid_from": add_days(nowdate(), 1)
})
item.save()
sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
- sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1"
+ sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1 - _TC"
self.assertRaises(frappe.ValidationError, sales_invoice.save)
item.taxes = []
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 76f3c50578e..0c4a4224407 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -240,8 +240,7 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values():
for entry in entries:
- key = entry.account_number or entry.account_name
- d = accounts_by_name.get(key)
+ d = accounts_by_name.get(entry.account_name)
if d:
for company in companies:
# 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"""
for d in reversed(accounts):
if d.parent_account:
- account = d.parent_account.split(' - ')[0].strip()
+ account = d.parent_account_name
+
if not accounts_by_name.get(account):
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].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
+
def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters)
if not accounts:
return None, None
+ accounts = update_parent_account_names(accounts)
+
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
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):
companies = {}
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'))
for entry in gl_entries:
- key = entry.account_number or entry.account_name
- validate_entries(key, entry, accounts_by_name, accounts)
- gl_entries_by_account.setdefault(key, []).append(entry)
+ account_name = entry.account_name
+ validate_entries(account_name, entry, accounts_by_name, accounts)
+ gl_entries_by_account.setdefault(account_name, []).append(entry)
return gl_entries_by_account
@@ -452,8 +470,7 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
- key = d.account_number or d.account_name
- accounts_by_name[key] = d
+ accounts_by_name[d.account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = []
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index f735d87a764..b5d7992604f 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -129,6 +129,9 @@ def get_gl_entries(filters, accounting_dimensions):
order_by_statement = "order by posting_date, account, creation"
+ if filters.get("include_dimensions"):
+ order_by_statement = "order by posting_date, creation"
+
if filters.get("group_by") == _("Group by Voucher"):
order_by_statement = "order by posting_date, voucher_type, voucher_no"
@@ -142,7 +145,9 @@ def get_gl_entries(filters, accounting_dimensions):
distributed_cost_center_query = ""
if filters and filters.get('cost_center'):
- select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
+ select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit,
+ credit*(DCC_allocation.percentage_allocation/100) as credit,
+ debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """
@@ -200,7 +205,7 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters):
conditions = []
- if filters.get("account"):
+ if filters.get("account") and not filters.get("include_dimensions"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
conditions.append("""account in (select name from tabAccount
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
@@ -245,17 +250,19 @@ def get_conditions(filters):
if match_conditions:
conditions.append(match_conditions)
- accounting_dimensions = get_accounting_dimensions(as_list=False)
+ if filters.get("include_dimensions"):
+ accounting_dimensions = get_accounting_dimensions(as_list=False)
- if accounting_dimensions:
- for dimension in accounting_dimensions:
- if filters.get(dimension.fieldname):
- if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'):
- filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type,
- filters.get(dimension.fieldname))
- conditions.append("{0} in %({0})s".format(dimension.fieldname))
- else:
- conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
+ if accounting_dimensions:
+ for dimension in accounting_dimensions:
+ if not dimension.disabled:
+ if filters.get(dimension.fieldname):
+ if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'):
+ filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type,
+ filters.get(dimension.fieldname))
+ conditions.append("{0} in %({0})s".format(dimension.fieldname))
+ else:
+ conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else ""
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index c7cfee74cb0..a8280c1b18e 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -55,7 +55,7 @@ def get_result(filters):
except IndexError:
account = []
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
- filters.company, filters.from_date, filters.to_date)
+ filters.company, filters.from_date, filters.to_date, filters.fiscal_year)
if total_invoiced_amount or tds_deducted:
row = [supplier.pan, supplier.name]
@@ -68,7 +68,7 @@ def get_result(filters):
return out
-def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date):
+def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year):
''' calculate total invoice amount and total tds deducted for given supplier '''
entries = frappe.db.sql("""
@@ -94,7 +94,9 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date):
""".format(', '.join(["'%s'" % d for d in vouchers])),
(account, from_date, to_date, company))[0][0])
- debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, company=company)
+ date_range_filter = [fiscal_year, from_date, to_date]
+
+ debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company)
total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index d568ef1ceda..02d48653203 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -231,12 +231,12 @@ class TestPurchaseOrder(unittest.TestCase):
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", {
- "item_tax_template": "Test Update Items Template",
+ "item_tax_template": "Test Update Items Template - _TC",
"valid_from": nowdate()
})
new_item_with_tax.save()
- tax_template = "_Test Account Excise Duty @ 10"
+ tax_template = "_Test Account Excise Duty @ 10 - _TC"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
@@ -287,7 +287,7 @@ class TestPurchaseOrder(unittest.TestCase):
po.cancel()
po.delete()
new_item_with_tax.delete()
- frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index e4698389963..219d5295c38 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -278,7 +278,7 @@ class BuyingController(StockController):
if self.is_subcontracted == "Yes":
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
- frappe.throw(_("Supplier Warehouse mandatory for sub-contracted Purchase Receipt"))
+ frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index e0031c9c699..11ac703311b 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -406,7 +406,8 @@ class StockController(AccountsController):
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
- d.stock_uom_rate = d.rate / d.conversion_factor
+ if d.conversion_factor:
+ d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index 948e7cc1aed..afa0be9b9f3 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -36,6 +36,7 @@ def enroll_student(source_name):
student.save()
program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name
+ program_enrollment.student_category = student.student_category
program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program")
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
diff --git a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
index 9f8f9f4dc00..8180102c582 100644
--- a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
+++ b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
@@ -30,7 +30,7 @@ class ProgramEnrollmentTool(Document):
.format(condition), self.as_dict(), as_dict=1)
elif self.get_students_from == "Program Enrollment":
condition2 = 'and student_batch_name=%(student_batch)s' if self.student_batch else " "
- students = frappe.db.sql('''select student, student_name, student_batch_name from `tabProgram Enrollment`
+ students = frappe.db.sql('''select student, student_name, student_batch_name, student_category from `tabProgram Enrollment`
where program=%(program)s and academic_year=%(academic_year)s {0} {1} and docstatus != 2'''
.format(condition, condition2), self.as_dict(), as_dict=1)
@@ -57,6 +57,7 @@ class ProgramEnrollmentTool(Document):
prog_enrollment = frappe.new_doc("Program Enrollment")
prog_enrollment.student = stud.student
prog_enrollment.student_name = stud.student_name
+ prog_enrollment.student_category = stud.student_category
prog_enrollment.program = self.new_program
prog_enrollment.academic_year = self.new_academic_year
prog_enrollment.academic_term = self.new_academic_term
diff --git a/erpnext/education/doctype/student_applicant/student_applicant.json b/erpnext/education/doctype/student_applicant/student_applicant.json
index 6df9b9a84f9..95f9224a73c 100644
--- a/erpnext/education/doctype/student_applicant/student_applicant.json
+++ b/erpnext/education/doctype/student_applicant/student_applicant.json
@@ -11,6 +11,7 @@
"middle_name",
"last_name",
"program",
+ "student_category",
"lms_only",
"paid",
"column_break_8",
@@ -257,12 +258,18 @@
"options": "Student Applicant",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "student_category",
+ "fieldtype": "Link",
+ "label": "Student Category",
+ "options": "Student Category"
}
],
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2020-10-05 13:59:45.631647",
+ "modified": "2021-03-01 23:00:25.119241",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Applicant",
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
index ff516469eb3..b55d5d6f633 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
@@ -364,7 +364,7 @@ let calculate_age = function(birth) {
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
// List Stock items
diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js
index f1634c12949..bb7976ccfac 100644
--- a/erpnext/healthcare/doctype/lab_test/lab_test.js
+++ b/erpnext/healthcare/doctype/lab_test/lab_test.js
@@ -258,5 +258,5 @@ var calculate_age = function (dob) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js
index 490f2475001..bce42e51d07 100644
--- a/erpnext/healthcare/doctype/patient/patient.js
+++ b/erpnext/healthcare/doctype/patient/patient.js
@@ -46,11 +46,11 @@ frappe.ui.form.on('Patient', {
}
},
onload: function (frm) {
- if(!frm.doc.dob){
+ if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('');
}
- if(frm.doc.dob){
- $(frm.fields_dict['age_html'].wrapper).html('AGE : ' + get_age(frm.doc.dob));
+ if (frm.doc.dob) {
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`);
}
}
});
@@ -65,7 +65,7 @@ frappe.ui.form.on('Patient', 'dob', function(frm) {
}
else {
let age_str = get_age(frm.doc.dob);
- $(frm.fields_dict['age_html'].wrapper).html('AGE : ' + age_str);
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
}
}
else {
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index 63dd8d4793a..8603f974c39 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -108,7 +108,7 @@ class Patient(Document):
if self.dob:
dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), dob)
- age_str = str(age.years) + ' year(s) ' + str(age.months) + ' month(s) ' + str(age.days) + ' day(s)'
+ age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
def invoice_patient_registration(self):
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 0354733dfb7..2976ef13a1d 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -596,5 +596,5 @@ let calculate_age = function(birth) {
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
index e960f0a9c40..aaeaa692e63 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
@@ -358,5 +358,5 @@ let calculate_age = function(birth) {
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
index 03903912358..ddf8285bc6d 100644
--- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js
+++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
@@ -36,5 +36,5 @@ var calculate_age = function(birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 373b94008e7..18a4fe53c4b 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -131,6 +131,10 @@ def mark_bulk_attendance(data):
data = json.loads(data)
data = frappe._dict(data)
company = frappe.get_value('Employee', data.employee, 'company')
+ if not data.unmarked_days:
+ frappe.throw(_("Please select a date."))
+ return
+
for date in data.unmarked_days:
doc_dict = {
'doctype': 'Attendance',
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 6df3dbd7845..0c7eafe9c61 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -12,7 +12,7 @@ frappe.listview_settings['Attendance'] = {
onload: function(list_view) {
let me = this;
const months = moment.months()
- list_view.page.add_inner_button( __("Mark Attendance"), function(){
+ list_view.page.add_inner_button( __("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
fields: [
@@ -22,11 +22,12 @@ frappe.listview_settings['Attendance'] = {
fieldtype: 'Link',
options: 'Employee',
reqd: 1,
- onchange: function(){
+ onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
}
},
{
@@ -35,13 +36,18 @@ frappe.listview_settings['Attendance'] = {
fieldname: "month",
options: months,
reqd: 1,
- onchange: function(){
+ onchange: function() {
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
+ if (options.length > 0) {
+ dialog.set_df_property("unmarked_days", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", options);
+ } else {
+ dialog.no_unmarked_days_left = true;
+ }
});
}
}
@@ -64,21 +70,25 @@ frappe.listview_settings['Attendance'] = {
hidden: 1
},
],
- primary_action(data){
- frappe.confirm(__('Mark attendance as ' + data.status + ' for ' + data.month +'' + ' on selected dates?'), () => {
- frappe.call({
- method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
- args: {
- data : data
- },
- callback: function(r) {
- if(r.message === 1) {
- frappe.show_alert({message:__("Attendance Marked"), indicator:'blue'});
- cur_dialog.hide();
+ primary_action(data) {
+ if (cur_dialog.no_unmarked_days_left) {
+ frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
+ } else {
+ frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
+ frappe.call({
+ method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
+ args: {
+ data: data
+ },
+ callback: function(r) {
+ if (r.message === 1) {
+ frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
+ cur_dialog.hide();
+ }
}
- }
+ });
});
- });
+ }
dialog.hide();
list_view.refresh();
},
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index dc2aaa4a067..5123d6a5a78 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2021-01-01 16:54:33.477439",
+ "modified": "2021-01-02 16:54:33.477439",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index c0e614ac088..7d652a7366a 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -48,6 +48,7 @@ class TestEmployee(unittest.TestCase):
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
def make_employee(user, company=None, **kwargs):
+ ""
if not frappe.db.get_value("User", user):
frappe.get_doc({
"doctype": "User",
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index d57ef5955dc..0c4c1cafb07 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-import frappe, erpnext
-from frappe import _
-from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today
-from frappe.model.document import Document
-from frappe.desk.form import assign_to
+import erpnext
+import frappe
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from frappe import _
+from frappe.desk.form import assign_to
+from frappe.model.document import Document
+from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
+ get_datetime, getdate, nowdate, today, unique)
+
class DuplicateDeclarationError(frappe.ValidationError): pass
+
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
@@ -48,27 +51,38 @@ class EmployeeBoardingController(Document):
continue
task = frappe.get_doc({
- "doctype": "Task",
- "project": self.project,
- "subject": activity.activity_name + " : " + self.employee_name,
- "description": activity.description,
- "department": self.department,
- "company": self.company,
- "task_weight": activity.task_weight
- }).insert(ignore_permissions=True)
+ "doctype": "Task",
+ "project": self.project,
+ "subject": activity.activity_name + " : " + self.employee_name,
+ "description": activity.description,
+ "department": self.department,
+ "company": self.company,
+ "task_weight": activity.task_weight
+ }).insert(ignore_permissions=True)
activity.db_set("task", task.name)
+
users = [activity.user] if activity.user else []
if activity.role:
- user_list = frappe.db.sql_list('''select distinct(parent) from `tabHas Role`
- where parenttype='User' and role=%s''', activity.role)
- users = users + user_list
+ user_list = frappe.db.sql_list('''
+ SELECT
+ DISTINCT(has_role.parent)
+ FROM
+ `tabHas Role` has_role
+ LEFT JOIN `tabUser` user
+ ON has_role.parent = user.name
+ WHERE
+ has_role.parenttype = 'User'
+ AND user.enabled = 1
+ AND has_role.role = %s
+ ''', activity.role)
+ users = unique(users + user_list)
if "Administrator" in users:
users.remove("Administrator")
# assign the task the users
if users:
- self.assign_task_to_users(task, set(users))
+ self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
diff --git a/erpnext/non_profit/doctype/membership_settings/__init__.py b/erpnext/non_profit/doctype/donation/__init__.py
similarity index 100%
rename from erpnext/non_profit/doctype/membership_settings/__init__.py
rename to erpnext/non_profit/doctype/donation/__init__.py
diff --git a/erpnext/non_profit/doctype/donation/donation.js b/erpnext/non_profit/doctype/donation/donation.js
new file mode 100644
index 00000000000..10e82201440
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.js
@@ -0,0 +1,26 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Donation', {
+ refresh: function(frm) {
+ if (frm.doc.docstatus === 1 && !frm.doc.paid) {
+ frm.add_custom_button(__('Create Payment Entry'), function() {
+ frm.events.make_payment_entry(frm);
+ });
+ }
+ },
+
+ make_payment_entry: function(frm) {
+ return frappe.call({
+ method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
+ args: {
+ 'dt': frm.doc.doctype,
+ 'dn': frm.doc.name
+ },
+ callback: function(r) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route('Form', doc[0].doctype, doc[0].name);
+ }
+ });
+ },
+});
diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json
new file mode 100644
index 00000000000..6759569d54d
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.json
@@ -0,0 +1,156 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2021-02-17 10:28:52.645731",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "donor",
+ "donor_name",
+ "email",
+ "column_break_4",
+ "company",
+ "date",
+ "payment_details_section",
+ "paid",
+ "amount",
+ "mode_of_payment",
+ "razorpay_payment_id",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "donor",
+ "fieldtype": "Link",
+ "label": "Donor",
+ "options": "Donor",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "donor.donor_name",
+ "fieldname": "donor_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Donor Name",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donor.email",
+ "fieldname": "email",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "payment_details_section",
+ "fieldtype": "Section Break",
+ "label": "Payment Details"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "reqd": 1
+ },
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "Razorpay Payment ID",
+ "read_only": 1
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "NPO-DTN-.YYYY.-"
+ },
+ {
+ "default": "0",
+ "fieldname": "paid",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Paid"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Donation",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-03-11 10:53:11.269005",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Donation",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "donor_name, email",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "donor_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py
new file mode 100644
index 00000000000..e947588482d
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.py
@@ -0,0 +1,215 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import six
+import json
+from frappe.model.document import Document
+from frappe import _
+from frappe.utils import getdate, flt, get_link_to_form
+from frappe.email import sendmail_to_system_managers
+from erpnext.non_profit.doctype.membership.membership import verify_signature
+
+class Donation(Document):
+ def validate(self):
+ if not self.donor or not frappe.db.exists('Donor', self.donor):
+ # for web forms
+ user_type = frappe.db.get_value('User', frappe.session.user, 'user_type')
+ if user_type == 'Website User':
+ self.create_donor_for_website_user()
+ else:
+ frappe.throw(_('Please select a Member'))
+
+ def create_donor_for_website_user(self):
+ donor_name = frappe.get_value('Donor', dict(email=frappe.session.user))
+
+ if not donor_name:
+ user = frappe.get_doc('User', frappe.session.user)
+ donor = frappe.get_doc(dict(
+ doctype='Donor',
+ donor_type=self.get('donor_type'),
+ email=frappe.session.user,
+ member_name=user.get_fullname()
+ )).insert(ignore_permissions=True)
+ donor_name = donor.name
+
+ if self.get('__islocal'):
+ self.donor = donor_name
+
+ def on_payment_authorized(self, *args, **kwargs):
+ self.load_from_db()
+ self.create_payment_entry()
+
+ def create_payment_entry(self):
+ settings = frappe.get_doc('Non Profit Settings')
+ if not settings.automate_donation_payment_entries:
+ return
+
+ if not settings.donation_payment_account:
+ frappe.throw(_('You need to set Payment Account for Donation in {0}').format(
+ get_link_to_form('Non Profit Settings', 'Non Profit Settings')))
+
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+
+ frappe.flags.ignore_account_permission = True
+ pe = get_payment_entry(dt=self.doctype, dn=self.name)
+ frappe.flags.ignore_account_permission = False
+ pe.paid_from = settings.donation_debit_account
+ pe.paid_to = settings.donation_payment_account
+ pe.reference_no = self.name
+ pe.reference_date = getdate()
+ pe.flags.ignore_mandatory = True
+ pe.insert()
+ pe.submit()
+
+
+@frappe.whitelist(allow_guest=True)
+def capture_razorpay_donations(*args, **kwargs):
+ """
+ Creates Donation from Razorpay Webhook Request Data on payment.captured event
+ Creates Donor from email if not found
+ """
+ data = frappe.request.get_data(as_text=True)
+
+ try:
+ verify_signature(data, endpoint='Donation')
+ except Exception as e:
+ log = frappe.log_error(e, 'Donation Webhook Verification Error')
+ notify_failure(log)
+ return { 'status': 'Failed', 'reason': e }
+
+ if isinstance(data, six.string_types):
+ data = json.loads(data)
+ data = frappe._dict(data)
+
+ payment = data.payload.get('payment', {}).get('entity', {})
+ payment = frappe._dict(payment)
+
+ try:
+ if not data.event == 'payment.captured':
+ return
+
+ donor = get_donor(payment.email)
+ if not donor:
+ donor = create_donor(payment)
+
+ donation = create_donation(donor, payment)
+ donation.run_method('create_payment_entry')
+
+ except Exception as e:
+ message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id)
+ log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name))
+ notify_failure(log)
+ return { 'status': 'Failed', 'reason': e }
+
+ return { 'status': 'Success' }
+
+
+def create_donation(donor, payment):
+ if not frappe.db.exists('Mode of Payment', payment.method):
+ create_mode_of_payment(payment.method)
+
+ company = get_company_for_donations()
+ donation = frappe.get_doc({
+ 'doctype': 'Donation',
+ 'company': company,
+ 'donor': donor.name,
+ 'donor_name': donor.donor_name,
+ 'email': donor.email,
+ 'date': getdate(),
+ 'amount': flt(payment.amount),
+ 'mode_of_payment': payment.method,
+ 'razorpay_payment_id': payment.id
+ }).insert(ignore_mandatory=True)
+
+ donation.submit()
+ return donation
+
+
+def get_donor(email):
+ donors = frappe.get_all('Donor',
+ filters={'email': email},
+ order_by='creation desc')
+
+ try:
+ return frappe.get_doc('Donor', donors[0]['name'])
+ except Exception:
+ return None
+
+
+@frappe.whitelist()
+def create_donor(payment):
+ donor_details = frappe._dict(payment)
+ donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type')
+
+ donor = frappe.new_doc('Donor')
+ donor.update({
+ 'donor_name': donor_details.email,
+ 'donor_type': donor_type,
+ 'email': donor_details.email,
+ 'contact': donor_details.contact
+ })
+
+ if donor_details.get('notes'):
+ donor = get_additional_notes(donor, donor_details)
+
+ donor.insert(ignore_mandatory=True)
+ return donor
+
+
+def get_company_for_donations():
+ company = frappe.db.get_single_value('Non Profit Settings', 'donation_company')
+ if not company:
+ from erpnext.healthcare.setup import get_company
+ company = get_company()
+ return company
+
+
+def get_additional_notes(donor, donor_details):
+ if type(donor_details.notes) == dict:
+ for k, v in donor_details.notes.items():
+ notes = '\n'.join('{}: {}'.format(k, v))
+
+ # extract donor name from notes
+ if 'name' in k.lower():
+ donor.update({
+ 'donor_name': donor_details.notes.get(k)
+ })
+
+ # extract pan from notes
+ if 'pan' in k.lower():
+ donor.update({
+ 'pan_number': donor_details.notes.get(k)
+ })
+
+ donor.add_comment('Comment', notes)
+
+ elif type(donor_details.notes) == str:
+ donor.add_comment('Comment', donor_details.notes)
+
+ return donor
+
+
+def create_mode_of_payment(method):
+ frappe.get_doc({
+ 'doctype': 'Mode of Payment',
+ 'mode_of_payment': method
+ }).insert(ignore_mandatory=True)
+
+
+def notify_failure(log):
+ try:
+ content = '''
+ Dear System Manager,
+ Razorpay webhook for creating donation failed due to some reason.
+ Please check the error log linked below
+ Error Log: {0}
+ Regards, Administrator
+ '''.format(get_link_to_form('Error Log', log.name))
+
+ sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content)
+ except Exception:
+ pass
+
diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py
new file mode 100644
index 00000000000..7e25c8d2173
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'donation',
+ 'non_standard_fieldnames': {
+ 'Payment Entry': 'reference_name'
+ },
+ 'transactions': [
+ {
+ 'label': _('Payment'),
+ 'items': ['Payment Entry']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py
new file mode 100644
index 00000000000..c6a534dac34
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/test_donation.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.non_profit.doctype.donation.donation import create_donation
+
+class TestDonation(unittest.TestCase):
+ def setUp(self):
+ create_donor_type()
+ settings = frappe.get_doc('Non Profit Settings')
+ settings.company = '_Test Company'
+ settings.donation_company = '_Test Company'
+ settings.default_donor_type = '_Test Donor'
+ settings.automate_donation_payment_entries = 1
+ settings.donation_debit_account = 'Debtors - _TC'
+ settings.donation_payment_account = 'Cash - _TC'
+ settings.creation_user = 'Administrator'
+ settings.flags.ignore_permissions = True
+ settings.save()
+
+ def test_payment_entry_for_donations(self):
+ donor = create_donor()
+ create_mode_of_payment()
+ payment = frappe._dict({
+ 'amount': 100,
+ 'method': 'Debit Card',
+ 'id': 'pay_MeXAmsgeKOhq7O'
+ })
+ donation = create_donation(donor, payment)
+
+ self.assertTrue(donation.name)
+
+ # Naive test to check if at all payment entry is generated
+ # This method is actually triggered from Payment Gateway
+ # In any case if details were missing, this would throw an error
+ donation.on_payment_authorized()
+ donation.reload()
+
+ self.assertEquals(donation.paid, 1)
+ self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name}))
+
+
+def create_donor_type():
+ if not frappe.db.exists('Donor Type', '_Test Donor'):
+ frappe.get_doc({
+ 'doctype': 'Donor Type',
+ 'donor_type': '_Test Donor'
+ }).insert()
+
+
+def create_donor():
+ donor = frappe.db.exists('Donor', 'donor@test.com')
+ if donor:
+ return frappe.get_doc('Donor', 'donor@test.com')
+ else:
+ return frappe.get_doc({
+ 'doctype': 'Donor',
+ 'donor_name': '_Test Donor',
+ 'donor_type': '_Test Donor',
+ 'email': 'donor@test.com'
+ }).insert()
+
+
+def create_mode_of_payment():
+ if not frappe.db.exists('Mode of Payment', 'Debit Card'):
+ frappe.get_doc({
+ 'doctype': 'Mode of Payment',
+ 'mode_of_payment': 'Debit Card',
+ 'accounts': [{
+ 'company': '_Test Company',
+ 'default_account': 'Cash - _TC'
+ }]
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json
index 96392658f1a..72f24ef9226 100644
--- a/erpnext/non_profit/doctype/donor/donor.json
+++ b/erpnext/non_profit/doctype/donor/donor.json
@@ -76,8 +76,13 @@
}
],
"image_field": "image",
- "links": [],
- "modified": "2020-09-16 23:46:04.083274",
+ "links": [
+ {
+ "link_doctype": "Donation",
+ "link_fieldname": "donor"
+ }
+ ],
+ "modified": "2021-02-17 16:36:33.470731",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Donor",
diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py
index 9121d0cdfc8..fb70e59575b 100644
--- a/erpnext/non_profit/doctype/donor/donor.py
+++ b/erpnext/non_profit/doctype/donor/donor.py
@@ -11,3 +11,8 @@ class Donor(Document):
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)
+ def validate(self):
+ from frappe.utils import validate_email_address
+ if self.email:
+ validate_email_address(self.email.strip(), True)
+
diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js
index 199dcfc04f5..6b8f1b1deb6 100644
--- a/erpnext/non_profit/doctype/member/member.js
+++ b/erpnext/non_profit/doctype/member/member.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('Member', {
setup: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
frm.set_df_property('razorpay_details_section', 'hidden', false);
}
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 04b99f93f21..3ba2ee71c67 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -7,7 +7,7 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.utils import cint
+from frappe.utils import cint, get_link_to_form
from frappe.integrations.utils import get_payment_gateway_controller
from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type
@@ -26,9 +26,10 @@ class Member(Document):
validate_email_address(email.strip(), True)
def setup_subscription(self):
- membership_settings = frappe.get_doc("Membership Settings")
- if not membership_settings.enable_razorpay:
- frappe.throw("Please enable Razorpay to setup subscription")
+ non_profit_settings = frappe.get_doc('Non Profit Settings')
+ if not non_profit_settings.enable_razorpay_for_memberships:
+ frappe.throw('Please check Enable Razorpay for Memberships in {0} to setup subscription').format(
+ get_link_to_form('Non Profit Settings', 'Non Profit Settings'))
controller = get_payment_gateway_controller("Razorpay")
settings = controller.get_settings({})
@@ -40,7 +41,7 @@ class Member(Document):
subscription_details = {
"plan_id": plan_id,
- "billing_frequency": cint(membership_settings.billing_frequency),
+ "billing_frequency": cint(non_profit_settings.billing_frequency),
"customer_notify": 1
}
diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js
index 573ac3319a4..31872048a06 100644
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ b/erpnext/non_profit/doctype/membership/membership.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('Membership', {
setup: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => {
if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
})
},
@@ -26,7 +26,7 @@ frappe.ui.form.on('Membership', {
});
});
- frappe.db.get_single_value("Membership Settings", "send_email").then(val => {
+ frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => {
if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => {
frm.reload_doc();
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
index 6da053f9fc4..11d32f9c2b4 100644
--- a/erpnext/non_profit/doctype/membership/membership.json
+++ b/erpnext/non_profit/doctype/membership/membership.json
@@ -10,6 +10,7 @@
"member_name",
"membership_type",
"column_break_3",
+ "company",
"membership_status",
"membership_validity_section",
"from_date",
@@ -132,11 +133,18 @@
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-21 16:31:20.032656",
+ "modified": "2021-02-19 14:33:44.925122",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index c113b80d56f..191281f4cea 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -6,6 +6,7 @@ from __future__ import unicode_literals
import json
import frappe
import six
+import os
from datetime import datetime
from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers
@@ -58,7 +59,7 @@ class Membership(Document):
else:
self.from_date = nowdate()
- if frappe.db.get_single_value("Membership Settings", "billing_cycle") == "Yearly":
+ if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly":
self.to_date = add_years(self.from_date, 1)
else:
self.to_date = add_months(self.from_date, 1)
@@ -68,9 +69,9 @@ class Membership(Document):
return
self.load_from_db()
self.db_set("paid", 1)
- settings = frappe.get_doc("Membership Settings")
- if settings.enable_invoicing and settings.create_for_web_forms:
- self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True)
+ settings = frappe.get_doc("Non Profit Settings")
+ if settings.allow_invoicing and settings.automate_membership_invoicing:
+ self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
def generate_invoice(self, save=True, with_payment_entry=False):
@@ -85,7 +86,7 @@ class Membership(Document):
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type)
- settings = frappe.get_doc("Membership Settings")
+ settings = frappe.get_doc("Non Profit Settings")
self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings)
@@ -102,7 +103,7 @@ class Membership(Document):
def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type)
- if not settings.debit_account:
+ if not settings.membership_debit_account:
frappe.throw(_("You need to set Debit Account in {0}").format(settings_link))
if not settings.company:
@@ -113,25 +114,26 @@ class Membership(Document):
get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice):
- if not settings.payment_account:
- frappe.throw(_("You need to set Payment Account in {0}").format(
- get_link_to_form("Membership Type", self.membership_type)))
+ if not settings.membership_payment_account:
+ frappe.throw(_("You need to set Payment Account for Membership in {0}").format(
+ get_link_to_form("Non Profit Settings", "Non Profit Settings")))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False
- pe.paid_to = settings.payment_account
+ pe.paid_to = settings.membership_payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
- pe.save(ignore_permissions=True)
+ pe.flags.ignore_mandatory = True
+ pe.save()
pe.submit()
def send_acknowlement(self):
- settings = frappe.get_doc("Membership Settings")
+ settings = frappe.get_doc("Non Profit Settings")
if not settings.send_email:
frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format(
- get_link_to_form("Membership Settings", "Membership Settings")))
+ get_link_to_form("Non Profit Settings", "Non Profit Settings")))
member = frappe.get_doc("Member", self.member)
if not member.email_id:
@@ -170,7 +172,7 @@ def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
"doctype": "Sales Invoice",
"customer": member.customer,
- "debit_to": settings.debit_account,
+ "debit_to": settings.membership_debit_account,
"currency": membership.currency,
"company": settings.company,
"is_pos": 0,
@@ -183,7 +185,7 @@ def make_invoice(membership, member, plan, settings):
]
})
invoice.set_missing_values()
- invoice.insert(ignore_permissions=True)
+ invoice.insert()
invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully"))
@@ -203,17 +205,18 @@ def get_member_based_on_subscription(subscription_id, email):
return None
-def verify_signature(data):
- if frappe.flags.in_test:
+def verify_signature(data, endpoint="Membership"):
+ if frappe.flags.in_test or os.environ.get("CI"):
return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
- settings = frappe.get_doc("Membership Settings")
- key = settings.get_webhook_secret()
+ settings = frappe.get_doc("Non Profit Settings")
+ key = settings.get_webhook_secret(endpoint)
controller = frappe.get_doc("Razorpay Settings")
controller.verify_signature(data, signature, key)
+ frappe.set_user(settings.creation_user)
@frappe.whitelist(allow_guest=True)
@@ -222,7 +225,7 @@ def trigger_razorpay_subscription(*args, **kwargs):
try:
verify_signature(data)
except Exception as e:
- log = frappe.log_error(e, "Webhook Verification Error")
+ log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log)
return { "status": "Failed", "reason": e}
@@ -250,16 +253,15 @@ def trigger_razorpay_subscription(*args, **kwargs):
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
- if subscription.notes and type(subscription.notes) == dict:
- notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
- member.add_comment("Comment", notes)
- elif subscription.notes and type(subscription.notes) == str:
- member.add_comment("Comment", subscription.notes)
+ if subscription.get("notes"):
+ member = get_additional_notes(member, subscription)
+ company = get_company_for_memberships()
# Update Membership
membership = frappe.new_doc("Membership")
membership.update({
+ "company": company,
"member": member.name,
"membership_status": "Current",
"membership_type": member.membership_type,
@@ -270,13 +272,20 @@ def trigger_razorpay_subscription(*args, **kwargs):
"to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise
})
- membership.insert(ignore_permissions=True)
+ membership.flags.ignore_mandatory = True
+ membership.insert()
# Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1
- member.save(ignore_permissions=True)
+ member.flags.ignore_mandatory = True
+ member.save()
+
+ settings = frappe.get_doc("Non Profit Settings")
+ if settings.allow_invoicing and settings.automate_membership_invoicing:
+ membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
+
except Exception as e:
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
@@ -286,6 +295,39 @@ def trigger_razorpay_subscription(*args, **kwargs):
return { "status": "Success" }
+def get_company_for_memberships():
+ company = frappe.db.get_single_value("Non Profit Settings", "company")
+ if not company:
+ from erpnext.healthcare.setup import get_company
+ company = get_company()
+ return company
+
+
+def get_additional_notes(member, subscription):
+ if type(subscription.notes) == dict:
+ for k, v in subscription.notes.items():
+ notes = "\n".join("{}: {}".format(k, v))
+
+ # extract member name from notes
+ if "name" in k.lower():
+ member.update({
+ "member_name": subscription.notes.get(k)
+ })
+
+ # extract pan number from notes
+ if "pan" in k.lower():
+ member.update({
+ "pan_number": subscription.notes.get(k)
+ })
+
+ member.add_comment("Comment", notes)
+
+ elif type(subscription.notes) == str:
+ member.add_comment("Comment", subscription.notes)
+
+ return member
+
+
def notify_failure(log):
try:
content = """
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index ff7e6c473c5..31da792e534 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -10,33 +10,7 @@ from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase):
def setUp(self):
- # Get default company
- company = frappe.get_doc("Company", erpnext.get_default_company())
-
- # update membership settings
- settings = frappe.get_doc("Membership Settings")
- # Enable razorpay
- settings.enable_razorpay = 1
- settings.billing_cycle = "Monthly"
- settings.billing_frequency = 24
- # Enable invoicing
- settings.enable_invoicing = 1
- settings.make_payment_entry = 1
- settings.company = company.name
- settings.payment_account = company.default_cash_account
- settings.debit_account = company.default_receivable_account
- settings.save()
-
- # make test plan
- if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
- plan = frappe.new_doc("Membership Type")
- plan.membership_type = "_rzpy_test_milythm"
- plan.amount = 100
- plan.razorpay_plan_id = "_rzpy_test_milythm"
- plan.linked_item = create_item("_Test Item for Non Profit Membership").name
- plan.insert()
- else:
- plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
+ plan = setup_membership()
# make test member
self.member_doc = create_member(frappe._dict({
@@ -78,7 +52,7 @@ class TestMembership(unittest.TestCase):
})
def set_config(key, value):
- frappe.db.set_value("Membership Settings", None, key, value)
+ frappe.db.set_value("Non Profit Settings", None, key, value)
def make_membership(member, payload={}):
data = {
@@ -109,3 +83,36 @@ def create_item(item_code):
else:
item = frappe.get_doc("Item", item_code)
return item
+
+def setup_membership():
+ # Get default company
+ company = frappe.get_doc("Company", erpnext.get_default_company())
+
+ # update non profit settings
+ settings = frappe.get_doc("Non Profit Settings")
+ # Enable razorpay
+ settings.enable_razorpay_for_memberships = 1
+ settings.billing_cycle = "Monthly"
+ settings.billing_frequency = 24
+ # Enable invoicing
+ settings.allow_invoicing = 1
+ settings.automate_membership_payment_entries = 1
+ settings.company = company.name
+ settings.donation_company = company.name
+ settings.membership_payment_account = company.default_cash_account
+ settings.membership_debit_account = company.default_receivable_account
+ settings.flags.ignore_mandatory = True
+ settings.save()
+
+ # make test plan
+ if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
+ plan = frappe.new_doc("Membership Type")
+ plan.membership_type = "_rzpy_test_milythm"
+ plan.amount = 100
+ plan.razorpay_plan_id = "_rzpy_test_milythm"
+ plan.linked_item = create_item("_Test Item for Non Profit Membership").name
+ plan.insert()
+ else:
+ plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
+
+ return plan
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
deleted file mode 100644
index 3887b0a2bea..00000000000
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json
+++ /dev/null
@@ -1,192 +0,0 @@
-{
- "actions": [],
- "creation": "2020-03-29 12:57:03.005120",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "enable_razorpay",
- "razorpay_settings_section",
- "billing_cycle",
- "billing_frequency",
- "webhook_secret",
- "column_break_6",
- "enable_invoicing",
- "create_for_web_forms",
- "make_payment_entry",
- "company",
- "debit_account",
- "payment_account",
- "column_break_9",
- "send_email",
- "send_invoice",
- "membership_print_format",
- "inv_print_format",
- "email_template"
- ],
- "fields": [
- {
- "fieldname": "billing_cycle",
- "fieldtype": "Select",
- "label": "Billing Cycle",
- "options": "Monthly\nYearly"
- },
- {
- "default": "0",
- "fieldname": "enable_razorpay",
- "fieldtype": "Check",
- "label": "Enable RazorPay For Memberships"
- },
- {
- "depends_on": "eval:doc.enable_razorpay",
- "fieldname": "razorpay_settings_section",
- "fieldtype": "Section Break",
- "label": "RazorPay Settings"
- },
- {
- "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
- "fieldname": "billing_frequency",
- "fieldtype": "Int",
- "label": "Billing Frequency"
- },
- {
- "fieldname": "webhook_secret",
- "fieldtype": "Password",
- "label": "Webhook Secret",
- "read_only": 1
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Section Break",
- "label": "Invoicing"
- },
- {
- "depends_on": "eval:doc.enable_invoicing",
- "fieldname": "debit_account",
- "fieldtype": "Link",
- "label": "Debit Account",
- "mandatory_depends_on": "eval:doc.enable_auto_invoicing",
- "options": "Account"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:doc.enable_invoicing",
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "mandatory_depends_on": "eval:doc.enable_auto_invoicing",
- "options": "Company"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing && doc.send_email",
- "fieldname": "send_invoice",
- "fieldtype": "Check",
- "label": "Send Invoice with Email"
- },
- {
- "default": "0",
- "fieldname": "send_email",
- "fieldtype": "Check",
- "label": "Send Membership Acknowledgement"
- },
- {
- "depends_on": "eval: doc.send_invoice",
- "fieldname": "inv_print_format",
- "fieldtype": "Link",
- "label": "Invoice Print Format",
- "mandatory_depends_on": "eval: doc.send_invoice",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "membership_print_format",
- "fieldtype": "Link",
- "label": "Membership Print Format",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "email_template",
- "fieldtype": "Link",
- "label": "Email Template",
- "mandatory_depends_on": "eval:doc.send_email",
- "options": "Email Template"
- },
- {
- "default": "0",
- "fieldname": "enable_invoicing",
- "fieldtype": "Check",
- "label": "Enable Invoicing",
- "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing",
- "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
- "fieldname": "make_payment_entry",
- "fieldtype": "Check",
- "label": "Make Payment Entry"
- },
- {
- "depends_on": "eval:doc.make_payment_entry",
- "fieldname": "payment_account",
- "fieldtype": "Link",
- "label": "Payment To",
- "mandatory_depends_on": "eval:doc.make_payment_entry",
- "options": "Account"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing",
- "description": "Automatically create an invoice when payment is authorized from a web form entry",
- "fieldname": "create_for_web_forms",
- "fieldtype": "Check",
- "label": "Auto Create Invoice for Web Forms"
- }
- ],
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2021-01-21 19:57:53.213286",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Membership Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- },
- {
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Member",
- "share": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py
deleted file mode 100644
index f3b2eee6f97..00000000000
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-from frappe.integrations.utils import get_payment_gateway_controller
-from frappe.model.document import Document
-
-class MembershipSettings(Document):
- def generate_webhook_key(self):
- key = frappe.generate_hash(length=20)
- self.webhook_secret = key
- self.save()
-
- frappe.msgprint(
- _("Here is your webhook secret, this will be shown to you only once.") + "
" + key,
- _("Webhook Secret")
- );
-
- def revoke_key(self):
- self.webhook_secret = None;
- self.save()
-
- def get_webhook_secret(self):
- return self.get_password(fieldname="webhook_secret", raise_exception=False)
-
-@frappe.whitelist()
-def get_plans_for_membership(*args, **kwargs):
- controller = get_payment_gateway_controller("Razorpay")
- plans = controller.get_plans()
- return [plan.get("item") for plan in plans.get("items")]
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
index 91a5cb74ba1..2f2427629c3 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.js
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.js
@@ -3,11 +3,11 @@
frappe.ui.form.on('Membership Type', {
refresh: function (frm) {
- frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
});
- frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false);
});
diff --git a/erpnext/non_profit/doctype/non_profit_settings/__init__.py b/erpnext/non_profit/doctype/non_profit_settings/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
similarity index 50%
rename from erpnext/non_profit/doctype/membership_settings/membership_settings.js
rename to erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
index c95aab2a7a1..cff92b42abb 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
@@ -1,16 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on("Membership Settings", {
+frappe.ui.form.on("Non Profit Settings", {
refresh: function(frm) {
- if (frm.doc.webhook_secret) {
- frm.add_custom_button(__("Revoke
" + key,
+ _("Webhook Secret")
+ )
+
+ def revoke_key(self, key):
+ self.set(key, None)
+ self.save()
+
+ def get_webhook_secret(self, endpoint="Membership"):
+ fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret"
+ return self.get_password(fieldname=fieldname, raise_exception=False)
+
+@frappe.whitelist()
+def get_plans_for_membership(*args, **kwargs):
+ controller = get_payment_gateway_controller("Razorpay")
+ plans = controller.get_plans()
+ return [plan.get("item") for plan in plans.get("items")]
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
similarity index 79%
rename from erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
rename to erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
index 2ad7984583d..3f0ede32e59 100644
--- a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
+++ b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
@@ -6,5 +6,5 @@ from __future__ import unicode_literals
# import frappe
import unittest
-class TestMembershipSettings(unittest.TestCase):
+class TestNonProfitSettings(unittest.TestCase):
pass
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
index da2a514810b..2557d77d881 100644
--- a/erpnext/non_profit/workspace/non_profit/non_profit.json
+++ b/erpnext/non_profit/workspace/non_profit/non_profit.json
@@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "non-profit",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Non Profit",
"links": [
@@ -109,7 +110,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Membership Settings",
- "link_to": "Membership Settings",
+ "link_to": "Non Profit Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -161,7 +162,7 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Donor",
+ "label": "Donation",
"onboard": 0,
"type": "Card Break"
},
@@ -184,9 +185,35 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donation",
+ "link_to": "Donation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Exemption Certification (India)",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Exemption 80G Certificate",
+ "link_to": "Tax Exemption 80G Certificate",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2020-12-01 13:38:38.351409",
+ "modified": "2021-03-11 11:38:09.140655",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",
@@ -201,8 +228,8 @@
"type": "DocType"
},
{
- "label": "Membership Settings",
- "link_to": "Membership Settings",
+ "label": "Non Profit Settings",
+ "link_to": "Non Profit Settings",
"type": "DocType"
},
{
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 02c7c09044d..42b98f569ef 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -756,6 +756,9 @@ erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
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
erpnext.patches.v13_0.create_website_items
erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items
diff --git a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py
new file mode 100644
index 00000000000..3fa09a7baaa
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py
@@ -0,0 +1,22 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ if frappe.db.table_exists("Membership Settings"):
+ frappe.rename_doc("DocType", "Membership Settings", "Non Profit Settings")
+ frappe.reload_doctype("Non Profit Settings", force=True)
+
+ if frappe.db.table_exists("Non Profit Settings"):
+ rename_fields_map = {
+ "enable_invoicing": "allow_invoicing",
+ "create_for_web_forms": "automate_membership_invoicing",
+ "make_payment_entry": "automate_membership_payment_entries",
+ "enable_razorpay": "enable_razorpay_for_memberships",
+ "debit_account": "membership_debit_account",
+ "payment_account": "membership_payment_account",
+ "webhook_secret": "membership_webhook_secret"
+ }
+
+ for old_name, new_name in rename_fields_map.items():
+ rename_field("Non Profit Settings", old_name, new_name)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
new file mode 100644
index 00000000000..aea53f8adda
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
@@ -0,0 +1,16 @@
+import frappe
+from erpnext.regional.india.setup import make_custom_fields
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ make_custom_fields()
+
+ if not frappe.db.exists('Party Type', 'Donor'):
+ frappe.get_doc({
+ 'doctype': 'Party Type',
+ 'party_type': 'Donor',
+ 'account_type': 'Receivable'
+ }).insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py
new file mode 100644
index 00000000000..01fd6a158e9
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py
@@ -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()
diff --git a/erpnext/payroll/doctype/gratuity/__init__.py b/erpnext/payroll/doctype/gratuity/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js
new file mode 100644
index 00000000000..565d2c49f94
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.js
@@ -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']);
+ });
+ }
+ }
+
+});
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json
new file mode 100644
index 00000000000..5cffd7eebf9
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.json
@@ -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"
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py
new file mode 100644
index 00000000000..1acd6e342fd
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.py
@@ -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
+
diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
new file mode 100644
index 00000000000..5b2489f22cd
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
@@ -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']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
new file mode 100644
index 00000000000..e89e3dd077a
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -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
diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py b/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json
new file mode 100644
index 00000000000..eea0e852b17
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py
new file mode 100644
index 00000000000..23e4340b04f
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py
@@ -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
diff --git a/erpnext/payroll/doctype/gratuity_rule/__init__.py b/erpnext/payroll/doctype/gratuity_rule/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
new file mode 100644
index 00000000000..ee6c5df7371
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
@@ -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) "));
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json
new file mode 100644
index 00000000000..84cdcf50386
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json
@@ -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 From and To 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
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py
new file mode 100644
index 00000000000..29a6ebe1a6a
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py
@@ -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
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py
new file mode 100644
index 00000000000..0d70163495a
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'gratuity_rule',
+ 'transactions': [
+ {
+ 'label': _('Gratuity'),
+ 'items': ['Gratuity']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py
new file mode 100644
index 00000000000..1f5dc4e571e
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py
@@ -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
diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py b/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json
new file mode 100644
index 00000000000..bc37b0f51ed
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py
new file mode 100644
index 00000000000..fa468e77beb
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py
@@ -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
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index d9aadbf3aa5..595d6974fd5 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -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:
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):
self.set_status()
self.update_status()
+ self.update_payment_status_for_gratuity()
self.cancel_loan_repayment_entry()
def on_trash(self):
@@ -506,7 +523,8 @@ class SalarySlip(TransactionBase):
return amount
except NameError as err:
- frappe.throw(_("Name error: {0}").format(err))
+ frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
except SyntaxError as err:
frappe.throw(_("Syntax error in formula or condition: {0}").format(err))
except Exception as e:
@@ -573,6 +591,7 @@ class SalarySlip(TransactionBase):
for d in self.get(key):
if d.salary_component == struct_row.salary_component:
component_row = d
+
if not component_row or (struct_row.get("is_additional_component") and not overwrite):
if amount:
self.append(key, {
@@ -930,7 +949,8 @@ class SalarySlip(TransactionBase):
if condition:
return frappe.safe_eval(condition, self.whitelisted_globals, data)
except NameError as err:
- frappe.throw(_("Name error: {0}").format(err))
+ frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
except SyntaxError as err:
frappe.throw(_("Syntax error in condition: {0}").format(err))
except Exception as e:
@@ -1242,4 +1262,4 @@ def unlink_ref_doc_from_salary_slip(ref_no):
def generate_password_for_pdf(policy_template, employee):
employee = frappe.get_doc("Employee", employee)
- return policy_template.format(**employee.as_dict())
\ No newline at end of file
+ return policy_template.format(**employee.as_dict())
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index f58a8e58c20..7289933d99e 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -21,6 +21,7 @@ from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_ta
class TestSalarySlip(unittest.TestCase):
def setUp(self):
setup_test()
+
def tearDown(self):
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator")
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js
index 1378bf0b913..6aa13873633 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -142,6 +142,8 @@ frappe.ui.form.on('Salary Structure', {
],
primary_action: function() {
var data = d.get_values();
+ delete data.company
+ delete data.currency
frappe.call({
doc: frm.doc,
method: "assign_salary_structure",
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index d81321b2916..3a3ee3858bf 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -158,16 +158,18 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
let me = this;
frappe.flags.round_off_applicable_accounts = [];
- return frappe.call({
- "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
- "args": {
- "company": me.frm.doc.company,
- "account_list": frappe.flags.round_off_applicable_accounts
- },
- callback: function(r) {
- frappe.flags.round_off_applicable_accounts.push(...r.message);
- }
- });
+ if (me.frm.doc.company) {
+ return frappe.call({
+ "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
+ "args": {
+ "company": me.frm.doc.company,
+ "account_list": frappe.flags.round_off_applicable_accounts
+ },
+ callback: function(r) {
+ frappe.flags.round_off_applicable_accounts.push(...r.message);
+ }
+ });
+ }
},
determine_exclusive_rate: function() {
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index e5f90490176..9351f6d2066 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1885,7 +1885,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.throw(__("Please enter Item Code to get batch no"));
} else if (doc.doctype == "Purchase Receipt" ||
(doc.doctype == "Purchase Invoice" && doc.update_stock)) {
-
return {
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) {
var item = frappe.get_doc(cdt, cdn);
if(!item.item_code) {
- frappe.throw(__("Please enter Item Code to get item taxes"));
+ return doc.company ? {filters: {company: doc.company}} : {};
} else {
-
let filters = {
'item_code': item.item_code,
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
@@ -2124,4 +2122,4 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => {
}
}
});
-};
\ No newline at end of file
+};
diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js
index b66126c2b8c..9548d6c5f36 100644
--- a/erpnext/public/js/telephony.js
+++ b/erpnext/public/js/telephony.js
@@ -1,11 +1,18 @@
frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( {
make_input() {
- if (!this.df.read_only) {
- this._super();
- }
+ this._super();
if (this.df.options == 'Phone') {
this.setup_phone();
}
+ if (this.frm && this.frm.fields_dict) {
+ Object.values(this.frm.fields_dict).forEach(function(field) {
+ if (field.df.read_only === 1 && field.df.options === 'Phone'
+ && field.disp_area.style[0] != 'display' && !field.has_icon) {
+ field.setup_phone();
+ field.has_icon = true;
+ }
+ });
+ }
},
setup_phone() {
if (frappe.phone_call.handler) {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index e5bd4d7e050..e5b50d86eda 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -595,21 +595,7 @@ erpnext.utils.update_child_items = function(opts) {
}
erpnext.utils.map_current_doc = function(opts) {
- let query_args = {};
- if (opts.get_query_filters) {
- query_args.filters = opts.get_query_filters;
- }
-
- if (opts.get_query_method) {
- query_args.query = opts.get_query_method;
- }
-
- if (query_args.filters || query_args.query) {
- opts.get_query = () => {
- return query_args;
- }
- }
- var _map = function() {
+ function _map() {
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {
// remove first item row if empty
if(!cur_frm.doc.items[0].item_code) {
@@ -683,8 +669,22 @@ erpnext.utils.map_current_doc = function(opts) {
}
});
}
- if(opts.source_doctype) {
- var d = new frappe.ui.form.MultiSelectDialog({
+
+ let query_args = {};
+ if (opts.get_query_filters) {
+ query_args.filters = opts.get_query_filters;
+ }
+
+ if (opts.get_query_method) {
+ query_args.query = opts.get_query_method;
+ }
+
+ if (query_args.filters || query_args.query) {
+ opts.get_query = () => query_args;
+ }
+
+ if (opts.source_doctype) {
+ const d = new frappe.ui.form.MultiSelectDialog({
doctype: opts.source_doctype,
target: opts.target,
date_field: opts.date_field || undefined,
@@ -703,7 +703,11 @@ erpnext.utils.map_current_doc = function(opts) {
_map();
},
});
- } else if(opts.source_name) {
+
+ return d;
+ }
+
+ if (opts.source_name) {
opts.source_name = [opts.source_name];
_map();
}
diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
index ead403d453a..e2125c3933a 100644
--- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
+++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
@@ -33,8 +33,7 @@
},
{
"fieldname": "sb_00",
- "fieldtype": "Section Break",
- "label": "Agenda"
+ "fieldtype": "Section Break"
},
{
"fieldname": "agenda",
@@ -44,13 +43,12 @@
},
{
"fieldname": "sb_01",
- "fieldtype": "Section Break",
- "label": "Minutes"
+ "fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-27 16:36:45.657883",
+ "modified": "2021-02-27 16:36:45.657883",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Meeting",
@@ -85,4 +83,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
new file mode 100644
index 00000000000..54cde9c0cf4
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
@@ -0,0 +1,67 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Tax Exemption 80G Certificate', {
+ refresh: function(frm) {
+ if (frm.doc.donor) {
+ frm.set_query('donation', function() {
+ return {
+ filters: {
+ docstatus: 1,
+ donor: frm.doc.donor
+ }
+ };
+ });
+ }
+ },
+
+ recipient: function(frm) {
+ if (frm.doc.recipient === 'Donor') {
+ frm.set_value({
+ 'member': '',
+ 'member_name': '',
+ 'member_email': '',
+ 'member_pan_number': '',
+ 'fiscal_year': '',
+ 'total': 0,
+ 'payments': []
+ });
+ } else {
+ frm.set_value({
+ 'donor': '',
+ 'donor_name': '',
+ 'donor_email': '',
+ 'donor_pan_number': '',
+ 'donation': '',
+ 'date_of_donation': '',
+ 'amount': 0,
+ 'mode_of_payment': '',
+ 'razorpay_payment_id': ''
+ });
+ }
+ },
+
+ get_payments: function(frm) {
+ frm.call({
+ doc: frm.doc,
+ method: 'get_payments',
+ freeze: true
+ });
+ },
+
+ company: function(frm) {
+ if ((frm.doc.member || frm.doc.donor) && frm.doc.company) {
+ frm.call({
+ doc: frm.doc,
+ method: 'set_company_address',
+ freeze: true
+ });
+ }
+ },
+
+ donation: function(frm) {
+ if (frm.doc.recipient === 'Donor' && !frm.doc.donor) {
+ frappe.msgprint(__('Please select donor first'));
+ }
+ }
+});
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
new file mode 100644
index 00000000000..9eee722f420
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
@@ -0,0 +1,297 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2021-02-15 12:37:21.577042",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "recipient",
+ "member",
+ "member_name",
+ "member_email",
+ "member_pan_number",
+ "donor",
+ "donor_name",
+ "donor_email",
+ "donor_pan_number",
+ "column_break_4",
+ "date",
+ "fiscal_year",
+ "section_break_11",
+ "company",
+ "company_address",
+ "company_address_display",
+ "column_break_14",
+ "company_pan_number",
+ "company_80g_number",
+ "company_80g_wef",
+ "title",
+ "section_break_6",
+ "get_payments",
+ "payments",
+ "total",
+ "donation_details_section",
+ "donation",
+ "date_of_donation",
+ "amount",
+ "column_break_27",
+ "mode_of_payment",
+ "razorpay_payment_id"
+ ],
+ "fields": [
+ {
+ "fieldname": "recipient",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Certificate Recipient",
+ "options": "Member\nDonor",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "member",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Member",
+ "mandatory_depends_on": "eval:doc.recipient === \"Member\";",
+ "options": "Member"
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.member_name",
+ "fieldname": "member_name",
+ "fieldtype": "Data",
+ "label": "Member Name",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fieldname": "donor",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Donor",
+ "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
+ "options": "Donor"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "payments",
+ "fieldtype": "Table",
+ "label": "Payments",
+ "options": "Tax Exemption 80G Certificate Detail"
+ },
+ {
+ "fieldname": "total",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "fiscal_year",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Fiscal Year",
+ "options": "Fiscal Year"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "get_payments",
+ "fieldtype": "Button",
+ "label": "Get Memberships"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "NPO-80G-.YYYY.-"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break",
+ "label": "Company Details"
+ },
+ {
+ "fieldname": "company_address",
+ "fieldtype": "Link",
+ "label": "Company Address",
+ "options": "Address"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "company.pan_details",
+ "fieldname": "company_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Number",
+ "read_only": 1
+ },
+ {
+ "fieldname": "company_address_display",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Company Address Display",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "company.company_80g_number",
+ "fieldname": "company_80g_number",
+ "fieldtype": "Data",
+ "label": "80G Number",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "company.with_effect_from",
+ "fieldname": "company_80g_wef",
+ "fieldtype": "Date",
+ "label": "80G With Effect From",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fieldname": "donation_details_section",
+ "fieldtype": "Section Break",
+ "label": "Donation Details"
+ },
+ {
+ "fieldname": "donation",
+ "fieldtype": "Link",
+ "label": "Donation",
+ "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
+ "options": "Donation"
+ },
+ {
+ "fetch_from": "donation.amount",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.mode_of_payment",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.razorpay_payment_id",
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "RazorPay Payment ID",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.date",
+ "fieldname": "date_of_donation",
+ "fieldtype": "Date",
+ "label": "Date of Donation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.donor_name",
+ "fieldname": "donor_name",
+ "fieldtype": "Data",
+ "label": "Donor Name",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.email",
+ "fieldname": "donor_email",
+ "fieldtype": "Data",
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.email_id",
+ "fieldname": "member_email",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.pan_number",
+ "fieldname": "member_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Details",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.pan_number",
+ "fieldname": "donor_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Details",
+ "read_only": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "print_hide": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-22 00:03:34.215633",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "Tax Exemption 80G Certificate",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "member, member_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "title",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
new file mode 100644
index 00000000000..d734a18c3ab
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import getdate, flt, get_link_to_form
+from erpnext.accounts.utils import get_fiscal_year
+from frappe.contacts.doctype.address.address import get_company_address
+
+class TaxExemption80GCertificate(Document):
+ def validate(self):
+ self.validate_date()
+ self.validate_duplicates()
+ self.validate_company_details()
+ self.set_company_address()
+ self.set_title()
+
+ def validate_date(self):
+ if self.recipient == 'Member':
+ if getdate(self.date):
+ fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
+
+ if not (fiscal_year.year_start_date <= getdate(self.date) \
+ <= fiscal_year.year_end_date):
+ frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
+
+ def validate_duplicates(self):
+ if self.recipient == 'Donor':
+ certificate = frappe.db.exists(self.doctype, {'donation': self.donation})
+ if certificate:
+ frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format(
+ get_link_to_form(self.doctype, certificate), frappe.bold(self.donation)
+ ), title=_('Duplicate Certificate'))
+
+ def validate_company_details(self):
+ fields = ['company_80g_number', 'with_effect_from', 'pan_details']
+ company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True)
+ if not company_details.company_80g_number:
+ frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'),
+ get_link_to_form('Company', self.company)))
+
+ if not company_details.pan_details:
+ frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
+ get_link_to_form('Company', self.company)))
+
+ def set_company_address(self):
+ address = get_company_address(self.company)
+ self.company_address = address.company_address
+ self.company_address_display = address.company_address_display
+
+ def set_title(self):
+ if self.recipient == "Member":
+ self.title = self.member_name
+ else:
+ self.title = self.donor_name
+
+ def get_payments(self):
+ if not self.member:
+ frappe.throw(_('Please select a Member first.'))
+
+ fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
+
+ memberships = frappe.db.get_all('Membership', {
+ 'member': self.member,
+ 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
+ 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
+ 'membership_status': ('!=', 'Cancelled')
+ }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'])
+
+ if not memberships:
+ frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
+
+ total = 0
+ self.payments = []
+
+ for doc in memberships:
+ self.append('payments', {
+ 'date': doc.from_date,
+ 'amount': doc.amount,
+ 'invoice_id': doc.invoice,
+ 'razorpay_payment_id': doc.payment_id,
+ 'membership': doc.name
+ })
+ total += flt(doc.amount)
+
+ self.total = total
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
new file mode 100644
index 00000000000..346ebbf6796
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import getdate
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.non_profit.doctype.donation.test_donation import create_donor, create_mode_of_payment, create_donor_type
+from erpnext.non_profit.doctype.donation.donation import create_donation
+from erpnext.non_profit.doctype.membership.test_membership import setup_membership, make_membership
+from erpnext.non_profit.doctype.member.member import create_member
+
+class TestTaxExemption80GCertificate(unittest.TestCase):
+ def setUp(self):
+ frappe.db.sql('delete from `tabTax Exemption 80G Certificate`')
+ frappe.db.sql('delete from `tabMembership`')
+ create_donor_type()
+ settings = frappe.get_doc('Non Profit Settings')
+ settings.company = '_Test Company'
+ settings.donation_company = '_Test Company'
+ settings.default_donor_type = '_Test Donor'
+ settings.creation_user = 'Administrator'
+ settings.save()
+
+ company = frappe.get_doc('Company', '_Test Company')
+ company.pan_details = 'BBBTI3374C'
+ company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087'
+ company.with_effect_from = getdate()
+ company.save()
+
+ def test_duplicate_donation_certificate(self):
+ donor = create_donor()
+ create_mode_of_payment()
+ payment = frappe._dict({
+ 'amount': 100,
+ 'method': 'Debit Card',
+ 'id': 'pay_MeXAmsgeKOhq7O'
+ })
+ donation = create_donation(donor, payment)
+
+ args = frappe._dict({
+ 'recipient': 'Donor',
+ 'donor': donor.name,
+ 'donation': donation.name
+ })
+ certificate = create_80g_certificate(args)
+ certificate.insert()
+
+ # check company details
+ self.assertEquals(certificate.company_pan_number, 'BBBTI3374C')
+ self.assertEquals(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087')
+
+ # check donation details
+ self.assertEquals(certificate.amount, donation.amount)
+
+ duplicate_certificate = create_80g_certificate(args)
+ # duplicate validation
+ self.assertRaises(frappe.ValidationError, duplicate_certificate.insert)
+
+ def test_membership_80g_certificate(self):
+ plan = setup_membership()
+
+ # make test member
+ member_doc = create_member(frappe._dict({
+ 'fullname': "_Test_Member",
+ 'email': "_test_member_erpnext@example.com",
+ 'plan_id': plan.name
+ }))
+ member_doc.make_customer_and_link()
+ member = member_doc.name
+
+ membership = make_membership(member, { "from_date": getdate() })
+ invoice = membership.generate_invoice(save=True)
+
+ args = frappe._dict({
+ 'recipient': 'Member',
+ 'member': member,
+ 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name')
+ })
+ certificate = create_80g_certificate(args)
+ certificate.get_payments()
+ certificate.insert()
+
+ self.assertEquals(len(certificate.payments), 1)
+ self.assertEquals(certificate.payments[0].amount, membership.amount)
+ self.assertEquals(certificate.payments[0].invoice_id, invoice.name)
+
+
+def create_80g_certificate(args):
+ certificate = frappe.get_doc({
+ 'doctype': 'Tax Exemption 80G Certificate',
+ 'recipient': args.recipient,
+ 'date': getdate(),
+ 'company': '_Test Company'
+ })
+
+ certificate.update(args)
+
+ return certificate
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
new file mode 100644
index 00000000000..dfa817dd271
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
@@ -0,0 +1,66 @@
+{
+ "actions": [],
+ "creation": "2021-02-15 12:43:52.754124",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "date",
+ "amount",
+ "invoice_id",
+ "column_break_4",
+ "razorpay_payment_id",
+ "membership"
+ ],
+ "fields": [
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "reqd": 1
+ },
+ {
+ "fieldname": "invoice_id",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Invoice ID",
+ "options": "Sales Invoice",
+ "reqd": 1
+ },
+ {
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "Razorpay Payment ID"
+ },
+ {
+ "fieldname": "membership",
+ "fieldtype": "Link",
+ "label": "Membership",
+ "options": "Membership"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-15 16:35:10.777587",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "Tax Exemption 80G Certificate Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
new file mode 100644
index 00000000000..bdad798d980
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class TaxExemption80GCertificateDetail(Document):
+ pass
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 526198424f3..ee49aae0501 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -21,6 +21,7 @@ def setup_company_independent_fixtures():
add_permissions()
add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
+ create_gratuity_rule()
add_print_formats()
def add_hsn_sac_codes():
@@ -105,8 +106,9 @@ def add_print_formats():
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
- frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
- name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
+ frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -398,9 +400,9 @@ def make_custom_fields(update=True):
si_einvoice_fields = [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
-
+
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
-
+
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
@@ -498,6 +500,14 @@ def make_custom_fields(update=True):
fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_component'),
+ dict(fieldname='non_profit_section', label='Non Profit Settings',
+ fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
+ dict(fieldname='company_80g_number', label='80G Number',
+ fieldtype='Data', insert_after='non_profit_section'),
+ dict(fieldname='with_effect_from', label='80G With Effect From',
+ fieldtype='Date', insert_after='company_80g_number'),
+ dict(fieldname='pan_details', label='PAN Number',
+ fieldtype='Data', insert_after='with_effect_from')
],
'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption',
@@ -580,7 +590,15 @@ def make_custom_fields(update=True):
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
],
- "Member": [
+ 'Member': [
+ {
+ 'fieldname': 'pan_number',
+ 'label': 'PAN Details',
+ 'fieldtype': 'Data',
+ 'insert_after': 'email_id'
+ }
+ ],
+ 'Donor': [
{
'fieldname': 'pan_number',
'label': 'PAN Details',
@@ -642,7 +660,7 @@ def set_tax_withholding_category(company):
pass
docs = get_tds_details(accounts, fiscal_year)
-
+
for d in docs:
try:
doc = frappe.get_doc(d)
@@ -660,7 +678,7 @@ def set_tax_withholding_category(company):
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
if not fy_exist:
doc.append("rates", d.get('rates')[0])
-
+
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.save()
@@ -822,4 +840,24 @@ def get_tds_details(accounts, fiscal_year):
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}])
- ]
\ No newline at end of file
+ ]
+
+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()
\ No newline at end of file
diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py
index 217d623a8d5..95b92e76a69 100644
--- a/erpnext/regional/italy/setup.py
+++ b/erpnext/regional/italy/setup.py
@@ -189,9 +189,7 @@ def make_custom_fields(update=True):
def setup_report():
report_name = 'Electronic Invoice Register'
-
- frappe.db.sql(""" update `tabReport` set disabled = 0 where
- name = %s """, report_name)
+ frappe.db.set_value("Report", report_name, "disabled", 0)
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
new file mode 100644
index 00000000000..a8da0bd2097
--- /dev/null
+++ b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
@@ -0,0 +1,26 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-02-22 00:17:33.878581",
+ "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Tax Exemption 80G Certificate",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "{% if letter_head and not no_letterhead -%}\n
{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n{{doc.company_address_display }}
\n\n", + "idx": 0, + "line_breaks": 0, + "modified": "2021-02-22 00:20:08.516600", + "modified_by": "Administrator", + "module": "Regional", + "name": "80G Certificate for Donation", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py b/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json new file mode 100644 index 00000000000..f1b15aab298 --- /dev/null +++ b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json @@ -0,0 +1,26 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2021-02-15 16:53:55.026611", + "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}", + "custom_format": 1, + "default_print_language": "en", + "disabled": 0, + "doc_type": "Tax Exemption 80G Certificate", + "docstatus": 0, + "doctype": "Print Format", + "font": "Default", + "html": "{% if letter_head and not no_letterhead -%}\n{{ _(\"Certificate No. : \") }} {{ doc.name }}
\n\n \t{{ _(\"Date\") }} : {{ doc.get_formatted(\"date\") }}
\n
| {{ _(\"Date\") }} | \n \t\t\t{{ _(\"Amount\") }} | \n \t\t\t{{ _(\"Invoice ID\") }} | \n \t\t
|---|---|---|
| {{ payment.date }} | \n \t\t\t{{ payment.get_formatted(\"amount\") }} | \n \t\t\t{{ payment.invoice_id }} | \n \t\t
\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n
\n\n{{doc.company_address_display }}
\n\n", + "idx": 0, + "line_breaks": 0, + "modified": "2021-02-21 23:29:00.778973", + "modified_by": "Administrator", + "module": "Regional", + "name": "80G Certificate for Membership", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 776a82c7306..68208ab31bf 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -7,12 +7,15 @@ import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax +from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule def setup(company=None, patch=True): make_custom_fields() add_print_formats() add_custom_roles_for_reports() add_permissions() + create_gratuity_rule() + if company: create_sales_tax(company) @@ -155,3 +158,93 @@ def add_permissions(): add_permission(doctype, role, 0) update_permission_property(doctype, role, 0, 'write', 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 + }] diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index 2b0ecafebc5..24ab1cf049f 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -36,5 +36,4 @@ def make_custom_fields(update=True): def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") - frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('IRS 1099 Form') """) + frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 87fdaa366f1..7761aa70fb2 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -54,7 +54,11 @@ class TestCustomer(unittest.TestCase): details = get_party_details("_Test Customer") for key, value in iteritems(to_check): - self.assertEqual(value, details.get(key)) + val = details.get(key) + if not val and not isinstance(val, list): + val = None + + self.assertEqual(value, val) def test_party_details_tax_category(self): from erpnext.accounts.party import get_party_details diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 52a0174798e..ee16f441715 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -555,12 +555,12 @@ class TestSalesOrder(unittest.TestCase): new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template", + "item_tax_template": "Test Update Items Template - _TC", "valid_from": nowdate() }) new_item_with_tax.save() - tax_template = "_Test Account Excise Duty @ 10" + tax_template = "_Test Account Excise Duty @ 10 - _TC" item = "_Test Item Home Desktop 100" if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): item_doc = frappe.get_doc("Item", item) @@ -614,7 +614,7 @@ class TestSalesOrder(unittest.TestCase): so.cancel() so.delete() new_item_with_tax.delete() - frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() + frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete() frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value) def test_warehouse_user(self): diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index d49ae7ce8ac..56f60dfcff0 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -725,7 +725,7 @@ { "fieldname": "default_in_transit_warehouse", "fieldtype": "Link", - "label": "Default In Transit Warehouse", + "label": "Default In-Transit Warehouse", "options": "Warehouse" }, { @@ -740,7 +740,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-12-03 12:27:27.085094", + "modified": "2021-02-16 15:53:37.167589", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 819ba78e666..433851cde53 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -390,8 +390,10 @@ class Company(NestedSet): frappe.db.sql("delete from tabDepartment where company=%s", self.name) frappe.db.sql("delete from `tabTax Withholding Account` where company=%s", self.name) + # delete tax templates frappe.db.sql("delete from `tabSales Taxes and Charges Template` where company=%s", self.name) frappe.db.sql("delete from `tabPurchase Taxes and Charges Template` where company=%s", self.name) + frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name) @frappe.whitelist() def enqueue_replace_abbr(company, old, new): diff --git a/erpnext/setup/doctype/company/company_list.js b/erpnext/setup/doctype/company/company_list.js index 017286560fe..1d1184f04d3 100644 --- a/erpnext/setup/doctype/company/company_list.js +++ b/erpnext/setup/doctype/company/company_list.js @@ -1,10 +1,5 @@ frappe.listview_settings['Company'] = { - onload: () => { - frappe.breadcrumbs.add({ - type: 'Custom', - module: __('Accounts'), - label: __('Accounts'), - route: '#modules/Accounts' - }); - } -} \ No newline at end of file + onload() { + frappe.breadcrumbs.add('Accounts'); + }, +}; diff --git a/erpnext/setup/doctype/item_group/test_records.json b/erpnext/setup/doctype/item_group/test_records.json index 71159643209..146da87bddc 100644 --- a/erpnext/setup/doctype/item_group/test_records.json +++ b/erpnext/setup/doctype/item_group/test_records.json @@ -79,13 +79,13 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", "tax_category": "" }, { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 12", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", "tax_category": "_Test Tax Category 1" } ] @@ -99,7 +99,7 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 15", + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC", "tax_category": "" } ] diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 4f25e53064d..2e033073d13 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -195,6 +195,7 @@ def install(country=None): {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, + {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"}, {'doctype': "Opportunity Type", "name": "Hub"}, {'doctype': "Opportunity Type", "name": _("Sales")}, diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 42f29f261d1..810d88ae7b3 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -85,7 +85,7 @@ frappe.ui.form.on("Item", { } if (frm.doc.variant_of) { frm.set_intro(__('This Item is a Variant of {0} (Template).', - [`${frm.doc.variant_of}`]), true); + [`${frm.doc.variant_of}`]), true); } if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) { diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 109731abb53..36d0de1e5df 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -104,41 +104,41 @@ class TestItem(unittest.TestCase): def test_item_tax_template(self): expected_item_tax_template = [ {"item_code": "_Test Item With Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10"}, + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12"}, + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10"}, + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12"}, + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 15"}, + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12"}, + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 20"}, + "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"}, {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Item Tax Template 1"}, + "item_tax_template": "_Test Item Tax Template 1 - _TC"}, {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, ] expected_item_tax_map = { None: {}, - "_Test Account Excise Duty @ 10": {"_Test Account Excise Duty - _TC": 10}, - "_Test Account Excise Duty @ 12": {"_Test Account Excise Duty - _TC": 12}, - "_Test Account Excise Duty @ 15": {"_Test Account Excise Duty - _TC": 15}, - "_Test Account Excise Duty @ 20": {"_Test Account Excise Duty - _TC": 20}, - "_Test Item Tax Template 1": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10, + "_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10}, + "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12}, + "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15}, + "_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20}, + "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10, "_Test Account S&H Education Cess - _TC": 15} } diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index bbada4c2583..87573e028c2 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -88,7 +88,7 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10" + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC" } ], "stock_uom": "_Test UOM 1" @@ -363,12 +363,12 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10" + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC" }, { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 12", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", "tax_category": "_Test Tax Category 1" } ] @@ -442,13 +442,13 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 20" + "item_tax_template": "_Test Account Excise Duty @ 20 - _TC" }, { "doctype": "Item Tax", "parentfield": "taxes", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Item Tax Template 1" + "item_tax_template": "_Test Item Tax Template 1 - _TC" } ] }, diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 6bacf1f8a33..c8d8ca9e17e 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -554,7 +554,7 @@ def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch if batch_nos: try: - filters["batch_no"] = json.loads(batch_nos) + filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)] except: filters["batch_no"] = [batch_nos] @@ -626,4 +626,4 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]): batch_no_condition=batch_no_condition ), filters, as_dict=1) - return serial_numbers \ No newline at end of file + return serial_numbers diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 726118d06d1..64dcbed1d85 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -231,15 +231,37 @@ frappe.ui.form.on('Stock Entry', { }, __("Get Items From")); frm.add_custom_button(__('Material Request'), function() { - erpnext.utils.map_current_doc({ + const allowed_request_types = ["Material Transfer", "Material Issue", "Customer Provided"]; + const depends_on_condition = "eval:doc.material_request_type==='Customer Provided'"; + const d = erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.material_request.material_request.make_stock_entry", source_doctype: "Material Request", target: frm, date_field: "schedule_date", - setters: {}, + setters: [{ + fieldtype: 'Select', + label: __('Purpose'), + options: allowed_request_types.join("\n"), + fieldname: 'material_request_type', + default: "Material Transfer", + mandatory: 1, + change() { + if (this.value === 'Customer Provided') { + d.dialog.get_field("customer").set_focus(); + } + }, + }, + { + fieldtype: 'Link', + label: __('Customer'), + options: 'Customer', + fieldname: 'customer', + depends_on: depends_on_condition, + mandatory_depends_on: depends_on_condition, + }], get_query_filters: { docstatus: 1, - material_request_type: ["in", ["Material Transfer", "Material Issue"]], + material_request_type: ["in", allowed_request_types], status: ["not in", ["Transferred", "Issued"]] } }) @@ -569,6 +591,7 @@ frappe.ui.form.on('Stock Entry', { add_to_transit: function(frm) { if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') { + frm.set_value('to_warehouse', ''); frm.set_value('stock_entry_type', 'Material Transfer'); frm.fields_dict.to_warehouse.get_query = function() { return { @@ -579,7 +602,15 @@ frappe.ui.form.on('Stock Entry', { } }; }; - frappe.db.get_value('Company', frm.doc.company, 'default_in_transit_warehouse', (r) => { + frm.trigger('set_tansit_warehouse'); + } + }, + + set_tansit_warehouse: function(frm) { + if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse) { + let dt = frm.doc.from_warehouse ? 'Warehouse' : 'Company'; + let dn = frm.doc.from_warehouse ? frm.doc.from_warehouse : frm.doc.company; + frappe.db.get_value(dt, dn, 'default_in_transit_warehouse', (r) => { if (r.default_in_transit_warehouse) { frm.set_value('to_warehouse', r.default_in_transit_warehouse); } @@ -946,6 +977,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ }, from_warehouse: function(doc) { + this.frm.trigger('set_tansit_warehouse'); this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse); }, diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 1bea00e2632..1f172504a7f 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -3,6 +3,18 @@ frappe.ui.form.on("Warehouse", { + onload: function(frm) { + frm.set_query("default_in_transit_warehouse", function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); + }, + refresh: function(frm) { frm.toggle_display('warehouse_name', frm.doc.__islocal); frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 1cc600b9ca7..bddb114c9de 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -13,6 +13,7 @@ "column_break_3", "warehouse_type", "parent_warehouse", + "default_in_transit_warehouse", "is_group", "column_break_4", "account", @@ -230,13 +231,20 @@ { "fieldname": "column_break_3", "fieldtype": "Section Break" + }, + { + "depends_on": "eval: doc.warehouse_type !== 'Transit';", + "fieldname": "default_in_transit_warehouse", + "fieldtype": "Link", + "label": "Default In-Transit Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-building", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-08-03 18:41:52.442502", + "modified": "2021-02-16 17:21:52.380098", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 6f12c2731bb..fe2417bba7e 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -82,11 +82,6 @@ frappe.query_reports["Stock Ledger"] = { "label": __("Include UOM"), "fieldtype": "Link", "options": "UOM" - }, - { - "fieldname": "show_cancelled_entries", - "label": __("Show Cancelled Entries"), - "fieldtype": "Check" } ], "formatter": function (value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 7b5701a9932..36996e96745 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -138,7 +138,7 @@ def get_stock_ledger_entries(filters, items): `tabStock Ledger Entry` sle WHERE company = %(company)s - AND posting_date BETWEEN %(from_date)s AND %(to_date)s + AND is_cancelled = 0 AND posting_date BETWEEN %(from_date)s AND %(to_date)s {sle_conditions} {item_conditions_sql} ORDER BY @@ -209,9 +209,6 @@ def get_sle_conditions(filters): if filters.get("project"): conditions.append("project=%(project)s") - if not filters.get("show_cancelled_entries"): - conditions.append("is_cancelled = 0") - return "and {}".format(" and ".join(conditions)) if conditions else "" diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py index 00f66e72c3e..26f59a2395e 100644 --- a/erpnext/www/lms/index.py +++ b/erpnext/www/lms/index.py @@ -13,4 +13,4 @@ def get_context(context): def get_featured_programs(): - return utils.get_portal_programs() \ No newline at end of file + return utils.get_portal_programs() or [] \ No newline at end of file diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py index d3b04c2f8f6..104d3fa315a 100644 --- a/erpnext/www/lms/program.py +++ b/erpnext/www/lms/program.py @@ -26,4 +26,4 @@ def get_program(program_name): def get_course_progress(courses, program): progress = {course.name: utils.get_course_progress(course, program) for course in courses} - return progress \ No newline at end of file + return progress or {} \ No newline at end of file