Merge branch 'develop' into e-commerce-refactor

This commit is contained in:
Marica
2021-03-11 21:43:45 +05:30
committed by GitHub
125 changed files with 3453 additions and 592 deletions

View File

@@ -63,17 +63,21 @@
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
"account_number": "1371" "account_number": "1371"
}, },
"Abziehbare VSt. 7%": { "Abziehbare Vorsteuer": {
"account_number": "1571" "account_type": "Tax",
}, "is_group": 1,
"Abziehbare VSt. 19%": { "Abziehbare Vorsteuer 7%": {
"account_number": "1576" "account_number": "1571"
}, },
"Abziehbare VStr. nach \u00a713b UStG 19%": { "Abziehbare Vorsteuer 19%": {
"account_number": "1577" "account_number": "1576"
}, },
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { "Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
"account_number": "3120" "account_number": "1577"
},
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
"account_number": "3120"
}
} }
}, },
"III. Wertpapiere": { "III. Wertpapiere": {
@@ -196,6 +200,7 @@
}, },
"Umsatzsteuer": { "Umsatzsteuer": {
"is_group": 1, "is_group": 1,
"account_type": "Tax",
"Umsatzsteuer 7%": { "Umsatzsteuer 7%": {
"account_number": "1771" "account_number": "1771"
}, },

View File

@@ -292,18 +292,21 @@
"Umsatzsteuerforderungen fr\u00fchere Jahre": {} "Umsatzsteuerforderungen fr\u00fchere Jahre": {}
}, },
"Sonstige Verm\u00f6gensgegenst\u00e4nde oder sonstige Verbindlichkeiten": { "Sonstige Verm\u00f6gensgegenst\u00e4nde oder sonstige Verbindlichkeiten": {
"Abziehbare Vorsteuer": {}, "Abziehbare Vorsteuer": {
"Abziehbare Vorsteuer 16%": {}, "account_type": "Tax",
"Abziehbare Vorsteuer 19%": {}, "is_group": 1,
"Abziehbare Vorsteuer 7%": {}, "Abziehbare Vorsteuer 16%": {},
"Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {}, "Abziehbare Vorsteuer 19%": {},
"Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {}, "Abziehbare Vorsteuer 7%": {},
"Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {}, "Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {},
"Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {}, "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {},
"Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {}, "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {},
"Abziehbare Vorsteuer nach \u00a7 13b UStG ": {}, "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {},
"Abziehbare Vorsteuer nach \u00a7 13b UStG 16%": {}, "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {},
"Abziehbare Vorsteuer nach \u00a7 13b UStG 19%": {}, "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": {}, "Aufl\u00f6sung Vorsteuer aus Vorjahr \u00a7 4/3 EStG": {},
"Aufzuteilende Vorsteuer": {}, "Aufzuteilende Vorsteuer": {},
"Aufzuteilende Vorsteuer 16%": {}, "Aufzuteilende Vorsteuer 16%": {},
@@ -673,23 +676,26 @@
"Sonstige Verrechnungskonten (Interimskonto)": { "Sonstige Verrechnungskonten (Interimskonto)": {
"account_type": "Stock Received But Not Billed" "account_type": "Stock Received But Not Billed"
}, },
"Umsatzsteuer": {}, "Umsatzsteuer": {
"Umsatzsteuer 16%": {}, "account_type": "Tax",
"Umsatzsteuer 19%": {}, "is_group": 1,
"Umsatzsteuer 7%": {}, "Umsatzsteuer 16%": {},
"Umsatzsteuer Vorjahr": {}, "Umsatzsteuer 19%": {},
"Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {}, "Umsatzsteuer 7%": {},
"Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {}, "Umsatzsteuer Vorjahr": {},
"Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {}, "Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {},
"Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {}, "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {},
"Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {}, "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {},
"Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {}, "Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {},
"Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {}, "Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {},
"Umsatzsteuer fr\u00fchere Jahre": {}, "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {},
"Umsatzsteuer laufendes Jahr": {}, "Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {},
"Umsatzsteuer nach \u00a713b UStG": {}, "Umsatzsteuer fr\u00fchere Jahre": {},
"Umsatzsteuer nach \u00a713b UStG 16%": {}, "Umsatzsteuer laufendes Jahr": {},
"Umsatzsteuer nach \u00a713b UStG 19%": {}, "Umsatzsteuer nach \u00a713b UStG": {},
"Umsatzsteuer nach \u00a713b UStG 16%": {},
"Umsatzsteuer nach \u00a713b UStG 19%": {}
},
"Umsatzsteuer- Vorauszahlungen": {}, "Umsatzsteuer- Vorauszahlungen": {},
"Umsatzsteuer- Vorauszahlungen 1/11": {}, "Umsatzsteuer- Vorauszahlungen 1/11": {},
"Verbindlichkeiten aus Lohn- und Kirchensteuer": {} "Verbindlichkeiten aus Lohn- und Kirchensteuer": {}

View File

@@ -659,6 +659,7 @@
}, },
"Abziehbare Vorsteuer (Gruppe)": { "Abziehbare Vorsteuer (Gruppe)": {
"is_group": 1, "is_group": 1,
"account_type": "Tax",
"Abziehbare Vorsteuer": { "Abziehbare Vorsteuer": {
"account_number": "1400" "account_number": "1400"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:title",
"creation": "2018-11-22 22:45:00.370913", "creation": "2018-11-22 22:45:00.370913",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
@@ -20,8 +20,7 @@
"in_list_view": 1, "in_list_view": 1,
"label": "Title", "label": "Title",
"no_copy": 1, "no_copy": 1,
"reqd": 1, "reqd": 1
"unique": 1
}, },
{ {
"fieldname": "taxes", "fieldname": "taxes",
@@ -33,12 +32,14 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1
} }
], ],
"modified": "2020-09-18 17:26:09.703215", "links": [],
"modified": "2021-03-08 19:50:21.416513",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Item Tax Template", "name": "Item Tax Template",
@@ -81,5 +82,6 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -11,6 +11,11 @@ class ItemTaxTemplate(Document):
def validate(self): def validate(self):
self.validate_tax_accounts() 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): def validate_tax_accounts(self):
"""Check whether Tax Rate is not entered twice for same Tax Type""" """Check whether Tax Rate is not entered twice for same Tax Type"""
check_list = [] check_list = []

View File

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

View File

@@ -198,6 +198,7 @@ def start_import(invoices):
try: try:
publish(idx, len(invoices), d.doctype) publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d) doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
doc.insert() doc.insert()
doc.submit() doc.submit()
frappe.db.commit() frappe.db.commit()

View File

@@ -92,14 +92,16 @@ frappe.ui.form.on('Payment Entry', {
}); });
frm.set_query("reference_doctype", "references", function() { frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") { if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
} else if (frm.doc.party_type=="Supplier") { } else if (frm.doc.party_type == "Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else if (frm.doc.party_type=="Employee") { } else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"]; var doctypes = ["Expense Claim", "Journal Entry"];
} else if (frm.doc.party_type=="Student") { } else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"]; var doctypes = ["Fees"];
} else if (frm.doc.party_type == "Donor") {
var doctypes = ["Donation"];
} else { } else {
var doctypes = ["Journal Entry"]; var doctypes = ["Journal Entry"];
} }
@@ -128,7 +130,7 @@ frappe.ui.form.on('Payment Entry', {
const child = locals[cdt][cdn]; const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company}; const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation'];
if (in_list(party_type_doctypes, child.reference_doctype)) { if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party; filters[doc.party_type.toLowerCase()] = doc.party;
@@ -281,7 +283,7 @@ frappe.ui.form.on('Payment Entry', {
let party_types = Object.keys(frappe.boot.party_account_types); let party_types = Object.keys(frappe.boot.party_account_types);
if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){ if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){
frm.set_value("party_type", ""); frm.set_value("party_type", "");
frappe.throw(__("Party can only be one of "+ party_types.join(", "))); frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")]));
} }
frm.set_query("party", function() { frm.set_query("party", function() {
@@ -705,7 +707,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) { ) {
if(total_positive_outstanding > total_negative_outstanding) if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount) if (!frm.doc.paid_amount)
@@ -748,7 +751,8 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) { ) {
if(total_positive_outstanding_including_order > paid_amount) { if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
@@ -905,6 +909,12 @@ frappe.ui.form.on('Payment Entry', {
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx])); frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false; return false;
} }
if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") {
frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx]));
return false;
}
} }
if (row) { if (row) {

View File

@@ -536,7 +536,8 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Title", "label": "Title",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"depends_on": "party", "depends_on": "party",
@@ -588,7 +589,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-30 13:56:20.007336", "modified": "2021-03-08 13:05:16.958866",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",
@@ -632,4 +633,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -72,6 +72,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation()
self.update_payment_schedule() self.update_payment_schedule()
self.set_status() self.set_status()
@@ -82,6 +83,7 @@ class PaymentEntry(AccountsController):
self.update_outstanding_amounts() self.update_outstanding_amounts()
self.update_advance_paid() self.update_advance_paid()
self.update_expense_claim() self.update_expense_claim()
self.update_donation(cancel=1)
self.delink_advance_entry_references() self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1) self.update_payment_schedule(cancel=1)
self.set_payment_req_status() self.set_payment_req_status()
@@ -242,9 +244,11 @@ class PaymentEntry(AccountsController):
elif self.party_type == "Supplier": elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
elif self.party_type == "Employee": elif self.party_type == "Employee":
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity")
elif self.party_type == "Shareholder": elif self.party_type == "Shareholder":
valid_reference_doctypes = ("Journal Entry") valid_reference_doctypes = ("Journal Entry")
elif self.party_type == "Donor":
valid_reference_doctypes = ("Donation")
for d in self.get("references"): for d in self.get("references"):
if not d.allocated_amount: if not d.allocated_amount:
@@ -455,6 +459,10 @@ class PaymentEntry(AccountsController):
.format(total_negative_outstanding), InvalidPaymentEntry) .format(total_negative_outstanding), InvalidPaymentEntry)
def set_title(self): 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"): if self.payment_type in ("Receive", "Pay"):
self.title = self.party self.title = self.party
else: else:
@@ -604,7 +612,7 @@ class PaymentEntry(AccountsController):
if self.payment_type in ("Receive", "Pay") and self.party: if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"): for d in self.get("references"):
if d.allocated_amount \ if d.allocated_amount \
and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance"): and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance", "Gratuity"):
frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
def update_expense_claim(self): def update_expense_claim(self):
@@ -614,6 +622,13 @@ class PaymentEntry(AccountsController):
doc = frappe.get_doc("Expense Claim", d.reference_name) doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, self.name) update_reimbursed_amount(doc, self.name)
def update_donation(self, cancel=0):
if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:
for d in self.get("references"):
if d.reference_doctype=="Donation" and d.reference_name:
is_paid = 0 if cancel else 1
frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
def on_recurring(self, reference_doc, auto_repeat_doc): def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name self.reference_no = reference_doc.name
self.reference_date = nowdate() self.reference_date = nowdate()
@@ -913,6 +928,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
total_amount = ref_doc.get("grand_total") total_amount = ref_doc.get("grand_total")
exchange_rate = 1 exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount") outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Donation":
total_amount = ref_doc.get("amount")
exchange_rate = 1
elif reference_doctype == "Dunning": elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount") total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1 exchange_rate = 1
@@ -932,6 +950,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
exchange_rate = ref_doc.get("exchange_rate") exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency: if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate) total_amount = flt(total_amount) * flt(exchange_rate)
elif ref_doc.doctype == "Gratuity":
total_amount = ref_doc.amount
if not total_amount: if not total_amount:
if party_account_currency == company_currency: if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total total_amount = ref_doc.base_grand_total
@@ -955,6 +975,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency: if party_account_currency == company_currency:
exchange_rate = 1 exchange_rate = 1
elif reference_doctype == "Gratuity":
outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount)
else: else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else: else:
@@ -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) total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance": elif ref_doc.doctype == "Employee Advance":
total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc) total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc)
if not total_amount: if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
party_account_currency, company_currency, ref_doc) party_account_currency, company_currency, ref_doc)
@@ -1160,10 +1182,12 @@ def set_party_type(dt):
party_type = "Customer" party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"): elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier" party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance"): elif dt in ("Expense Claim", "Employee Advance", "Gratuity"):
party_type = "Employee" party_type = "Employee"
elif dt in ("Fees"): elif dt == "Fees":
party_type = "Student" party_type = "Student"
elif dt == "Donation":
party_type = "Donor"
return party_type return party_type
def set_party_account(dt, dn, doc, party_type): def set_party_account(dt, dn, doc, party_type):
@@ -1177,6 +1201,8 @@ def set_party_account(dt, dn, doc, party_type):
party_account = doc.advance_account party_account = doc.advance_account
elif dt == "Expense Claim": elif dt == "Expense Claim":
party_account = doc.payable_account party_account = doc.payable_account
elif dt == "Gratuity":
party_account = doc.payable_account
else: else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account return party_account
@@ -1189,7 +1215,7 @@ def set_party_account_currency(dt, party_account, doc):
return party_account_currency return party_account_currency
def set_payment_type(dt, doc): def set_payment_type(dt, doc):
if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive" payment_type = "Receive"
else: else:
@@ -1222,6 +1248,12 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
elif dt == "Dunning": elif dt == "Dunning":
grand_total = doc.grand_total grand_total = doc.grand_total
outstanding_amount = doc.grand_total outstanding_amount = doc.grand_total
elif dt == "Donation":
grand_total = doc.amount
outstanding_amount = doc.amount
elif dt == "Gratuity":
grand_total = doc.amount
outstanding_amount = flt(doc.amount) - flt(doc.paid_amount)
else: else:
if party_account_currency == doc.company_currency: if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
@@ -1326,4 +1358,4 @@ def make_payment_order(source_name, target_doc=None):
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doclist return doclist

View File

@@ -99,10 +99,10 @@ class TestPOSInvoice(unittest.TestCase):
item_row = inv.get("items")[0] item_row = inv.get("items")[0]
add_items = [ add_items = [
(54, '_Test Account Excise Duty @ 12'), (54, '_Test Account Excise Duty @ 12 - _TC'),
(288, '_Test Account Excise Duty @ 15'), (288, '_Test Account Excise Duty @ 15 - _TC'),
(144, '_Test Account Excise Duty @ 20'), (144, '_Test Account Excise Duty @ 20 - _TC'),
(430, '_Test Item Tax Template 1') (430, '_Test Item Tax Template 1 - _TC')
] ]
for qty, item_tax_template in add_items: for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row) item_row_copy = copy.deepcopy(item_row)

View File

@@ -58,6 +58,7 @@
"rejected_warehouse", "rejected_warehouse",
"col_break_warehouse", "col_break_warehouse",
"set_from_warehouse", "set_from_warehouse",
"supplier_warehouse",
"is_subcontracted", "is_subcontracted",
"items_section", "items_section",
"update_stock", "update_stock",
@@ -1350,7 +1351,7 @@
"options": "Company" "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.", "description": "Sets 'From Warehouse' in each row of the items table.",
"fieldname": "set_from_warehouse", "fieldname": "set_from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
@@ -1360,13 +1361,24 @@
"print_hide": 1, "print_hide": 1,
"print_width": "50px", "print_width": "50px",
"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", "icon": "fa fa-file-text",
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-26 20:49:03.305063", "modified": "2021-03-09 21:12:30.422084",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

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

View File

@@ -456,7 +456,7 @@ class TestPurchaseInvoice(unittest.TestCase):
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", 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") 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", company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - 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.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC" pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No" pi.is_subcontracted = args.is_subcontracted or "No"
if args.supplier_warehouse: if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", { pi.append("items", {

View File

@@ -18,7 +18,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC", "expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100", "item_code": "_Test Item Home Desktop 100",
"item_name": "_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", "parentfield": "items",
"qty": 10, "qty": 10,
"rate": 50, "rate": 50,

View File

@@ -148,7 +148,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC", "expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100", "item_code": "_Test Item Home Desktop 100",
"item_name": "_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", "parentfield": "items",
"price_list_rate": 50, "price_list_rate": 50,
"qty": 10, "qty": 10,
@@ -276,7 +276,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC", "expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100", "item_code": "_Test Item Home Desktop 100",
"item_name": "_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", "parentfield": "items",
"price_list_rate": 62.5, "price_list_rate": 62.5,
"qty": 10, "qty": 10,

View File

@@ -405,10 +405,10 @@ class TestSalesInvoice(unittest.TestCase):
item_row = si.get("items")[0] item_row = si.get("items")[0]
add_items = [ add_items = [
(54, '_Test Account Excise Duty @ 12'), (54, '_Test Account Excise Duty @ 12 - _TC'),
(288, '_Test Account Excise Duty @ 15'), (288, '_Test Account Excise Duty @ 15 - _TC'),
(144, '_Test Account Excise Duty @ 20'), (144, '_Test Account Excise Duty @ 20 - _TC'),
(430, '_Test Item Tax Template 1') (430, '_Test Item Tax Template 1 - _TC')
] ]
for qty, item_tax_template in add_items: for qty, item_tax_template in add_items:
item_row_copy = copy.deepcopy(item_row) 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.save()
item.append("taxes", { 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) "valid_from": add_days(nowdate(), 1)
}) })
item.save() item.save()
sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1) 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) self.assertRaises(frappe.ValidationError, sales_invoice.save)
item.taxes = [] item.taxes = []

View File

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

View File

@@ -129,6 +129,9 @@ def get_gl_entries(filters, accounting_dimensions):
order_by_statement = "order by posting_date, account, creation" 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"): if filters.get("group_by") == _("Group by Voucher"):
order_by_statement = "order by posting_date, voucher_type, voucher_no" 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 = "" distributed_cost_center_query = ""
if filters and filters.get('cost_center'): 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 """ credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """ distributed_cost_center_query = """
@@ -200,7 +205,7 @@ def get_gl_entries(filters, accounting_dimensions):
def get_conditions(filters): def get_conditions(filters):
conditions = [] 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"]) lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
conditions.append("""account in (select name from tabAccount conditions.append("""account in (select name from tabAccount
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
@@ -245,17 +250,19 @@ def get_conditions(filters):
if match_conditions: if match_conditions:
conditions.append(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: if accounting_dimensions:
for dimension in accounting_dimensions: for dimension in accounting_dimensions:
if filters.get(dimension.fieldname): if not dimension.disabled:
if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): if filters.get(dimension.fieldname):
filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'):
filters.get(dimension.fieldname)) filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type,
conditions.append("{0} in %({0})s".format(dimension.fieldname)) filters.get(dimension.fieldname))
else: conditions.append("{0} in %({0})s".format(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 "" return "and {}".format(" and ".join(conditions)) if conditions else ""

View File

@@ -55,7 +55,7 @@ def get_result(filters):
except IndexError: except IndexError:
account = [] account = []
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, 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: if total_invoiced_amount or tds_deducted:
row = [supplier.pan, supplier.name] row = [supplier.pan, supplier.name]
@@ -68,7 +68,7 @@ def get_result(filters):
return out 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 ''' ''' calculate total invoice amount and total tds deducted for given supplier '''
entries = frappe.db.sql(""" 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])), """.format(', '.join(["'%s'" % d for d in vouchers])),
(account, from_date, to_date, company))[0][0]) (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 total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount

View File

@@ -231,12 +231,12 @@ class TestPurchaseOrder(unittest.TestCase):
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", { new_item_with_tax.append("taxes", {
"item_tax_template": "Test Update Items Template", "item_tax_template": "Test Update Items Template - _TC",
"valid_from": nowdate() "valid_from": nowdate()
}) })
new_item_with_tax.save() 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" item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item) item_doc = frappe.get_doc("Item", item)
@@ -287,7 +287,7 @@ class TestPurchaseOrder(unittest.TestCase):
po.cancel() po.cancel()
po.delete() po.delete()
new_item_with_tax.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): def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")

View File

@@ -278,7 +278,7 @@ class BuyingController(StockController):
if self.is_subcontracted == "Yes": if self.is_subcontracted == "Yes":
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: 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"): for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom: if item in self.sub_contracted_items and not item.bom:

View File

@@ -406,7 +406,8 @@ class StockController(AccountsController):
def set_rate_of_stock_uom(self): def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"): 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): def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \

View File

@@ -36,6 +36,7 @@ def enroll_student(source_name):
student.save() student.save()
program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name program_enrollment.student = student.name
program_enrollment.student_category = student.student_category
program_enrollment.student_name = student.title program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") 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) frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)

View File

@@ -30,7 +30,7 @@ class ProgramEnrollmentTool(Document):
.format(condition), self.as_dict(), as_dict=1) .format(condition), self.as_dict(), as_dict=1)
elif self.get_students_from == "Program Enrollment": elif self.get_students_from == "Program Enrollment":
condition2 = 'and student_batch_name=%(student_batch)s' if self.student_batch else " " 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''' where program=%(program)s and academic_year=%(academic_year)s {0} {1} and docstatus != 2'''
.format(condition, condition2), self.as_dict(), as_dict=1) .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 = frappe.new_doc("Program Enrollment")
prog_enrollment.student = stud.student prog_enrollment.student = stud.student
prog_enrollment.student_name = stud.student_name prog_enrollment.student_name = stud.student_name
prog_enrollment.student_category = stud.student_category
prog_enrollment.program = self.new_program prog_enrollment.program = self.new_program
prog_enrollment.academic_year = self.new_academic_year prog_enrollment.academic_year = self.new_academic_year
prog_enrollment.academic_term = self.new_academic_term prog_enrollment.academic_term = self.new_academic_term

View File

@@ -11,6 +11,7 @@
"middle_name", "middle_name",
"last_name", "last_name",
"program", "program",
"student_category",
"lms_only", "lms_only",
"paid", "paid",
"column_break_8", "column_break_8",
@@ -257,12 +258,18 @@
"options": "Student Applicant", "options": "Student Applicant",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "student_category",
"fieldtype": "Link",
"label": "Student Category",
"options": "Student Category"
} }
], ],
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-05 13:59:45.631647", "modified": "2021-03-01 23:00:25.119241",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Student Applicant", "name": "Student Applicant",

View File

@@ -364,7 +364,7 @@ let calculate_age = function(birth) {
let age = new Date(); let age = new Date();
age.setTime(ageMS); age.setTime(ageMS);
let years = age.getFullYear() - 1970; 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 // List Stock items

View File

@@ -258,5 +258,5 @@ var calculate_age = function (dob) {
var age = new Date(); var age = new Date();
age.setTime(ageMS); age.setTime(ageMS);
var years = age.getFullYear() - 1970; 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)')}`;
}; };

View File

@@ -46,11 +46,11 @@ frappe.ui.form.on('Patient', {
} }
}, },
onload: function (frm) { onload: function (frm) {
if(!frm.doc.dob){ if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html(''); $(frm.fields_dict['age_html'].wrapper).html('');
} }
if(frm.doc.dob){ if (frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('AGE : ' + get_age(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 { else {
let age_str = get_age(frm.doc.dob); 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 { else {

View File

@@ -108,7 +108,7 @@ class Patient(Document):
if self.dob: if self.dob:
dob = getdate(self.dob) dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), 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 return age_str
def invoice_patient_registration(self): def invoice_patient_registration(self):

View File

@@ -596,5 +596,5 @@ let calculate_age = function(birth) {
let age = new Date(); let age = new Date();
age.setTime(ageMS); age.setTime(ageMS);
let years = age.getFullYear() - 1970; 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)')}`;
}; };

View File

@@ -358,5 +358,5 @@ let calculate_age = function(birth) {
let age = new Date(); let age = new Date();
age.setTime(ageMS); age.setTime(ageMS);
let years = age.getFullYear() - 1970; 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)')}`;
}; };

View File

@@ -36,5 +36,5 @@ var calculate_age = function(birth) {
var age = new Date(); var age = new Date();
age.setTime(ageMS); age.setTime(ageMS);
var years = age.getFullYear() - 1970; 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)')}`;
}; };

View File

@@ -131,6 +131,10 @@ def mark_bulk_attendance(data):
data = json.loads(data) data = json.loads(data)
data = frappe._dict(data) data = frappe._dict(data)
company = frappe.get_value('Employee', data.employee, 'company') 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: for date in data.unmarked_days:
doc_dict = { doc_dict = {
'doctype': 'Attendance', 'doctype': 'Attendance',

View File

@@ -12,7 +12,7 @@ frappe.listview_settings['Attendance'] = {
onload: function(list_view) { onload: function(list_view) {
let me = this; let me = this;
const months = moment.months() 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({ let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"), title: __("Mark Attendance"),
fields: [ fields: [
@@ -22,11 +22,12 @@ frappe.listview_settings['Attendance'] = {
fieldtype: 'Link', fieldtype: 'Link',
options: 'Employee', options: 'Employee',
reqd: 1, reqd: 1,
onchange: function(){ onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1); dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1); dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", ''); dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []); dialog.set_df_property("unmarked_days", "options", []);
dialog.no_unmarked_days_left = false;
} }
}, },
{ {
@@ -35,13 +36,18 @@ frappe.listview_settings['Attendance'] = {
fieldname: "month", fieldname: "month",
options: months, options: months,
reqd: 1, reqd: 1,
onchange: function(){ onchange: function() {
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0); dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []); 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 =>{ 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); if (options.length > 0) {
dialog.set_df_property("unmarked_days", "options", options); 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 hidden: 1
}, },
], ],
primary_action(data){ primary_action(data) {
frappe.confirm(__('Mark attendance as <b>' + data.status + '</b> for <b>' + data.month +'</b>' + ' on selected dates?'), () => { if (cur_dialog.no_unmarked_days_left) {
frappe.call({ 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]));
method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance", } else {
args: { frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
data : data frappe.call({
}, method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
callback: function(r) { args: {
if(r.message === 1) { data: data
frappe.show_alert({message:__("Attendance Marked"), indicator:'blue'}); },
cur_dialog.hide(); callback: function(r) {
if (r.message === 1) {
frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
cur_dialog.hide();
}
} }
} });
}); });
}); }
dialog.hide(); dialog.hide();
list_view.refresh(); list_view.refresh();
}, },

View File

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

View File

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

View File

@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals import erpnext
import frappe, erpnext import frappe
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
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee 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 DuplicateDeclarationError(frappe.ValidationError): pass
class EmployeeBoardingController(Document): class EmployeeBoardingController(Document):
''' '''
Create the project and the task for the boarding process Create the project and the task for the boarding process
@@ -48,27 +51,38 @@ class EmployeeBoardingController(Document):
continue continue
task = frappe.get_doc({ task = frappe.get_doc({
"doctype": "Task", "doctype": "Task",
"project": self.project, "project": self.project,
"subject": activity.activity_name + " : " + self.employee_name, "subject": activity.activity_name + " : " + self.employee_name,
"description": activity.description, "description": activity.description,
"department": self.department, "department": self.department,
"company": self.company, "company": self.company,
"task_weight": activity.task_weight "task_weight": activity.task_weight
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
activity.db_set("task", task.name) activity.db_set("task", task.name)
users = [activity.user] if activity.user else [] users = [activity.user] if activity.user else []
if activity.role: if activity.role:
user_list = frappe.db.sql_list('''select distinct(parent) from `tabHas Role` user_list = frappe.db.sql_list('''
where parenttype='User' and role=%s''', activity.role) SELECT
users = users + user_list 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: if "Administrator" in users:
users.remove("Administrator") users.remove("Administrator")
# assign the task the users # assign the task the users
if 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): def assign_task_to_users(self, task, users):
for user in users: for user in users:

View File

@@ -0,0 +1,26 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Donation', {
refresh: function(frm) {
if (frm.doc.docstatus === 1 && !frm.doc.paid) {
frm.add_custom_button(__('Create Payment Entry'), function() {
frm.events.make_payment_entry(frm);
});
}
},
make_payment_entry: function(frm) {
return frappe.call({
method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
args: {
'dt': frm.doc.doctype,
'dn': frm.doc.name
},
callback: function(r) {
var doc = frappe.model.sync(r.message);
frappe.set_route('Form', doc[0].doctype, doc[0].name);
}
});
},
});

View File

@@ -0,0 +1,156 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2021-02-17 10:28:52.645731",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"donor",
"donor_name",
"email",
"column_break_4",
"company",
"date",
"payment_details_section",
"paid",
"amount",
"mode_of_payment",
"razorpay_payment_id",
"amended_from"
],
"fields": [
{
"fieldname": "donor",
"fieldtype": "Link",
"label": "Donor",
"options": "Donor",
"reqd": 1
},
{
"fetch_from": "donor.donor_name",
"fieldname": "donor_name",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Donor Name",
"read_only": 1
},
{
"fetch_from": "donor.email",
"fieldname": "email",
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Email",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"reqd": 1
},
{
"fieldname": "payment_details_section",
"fieldtype": "Section Break",
"label": "Payment Details"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"reqd": 1
},
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment"
},
{
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "Razorpay Payment ID",
"read_only": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "NPO-DTN-.YYYY.-"
},
{
"default": "0",
"fieldname": "paid",
"fieldtype": "Check",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Paid"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Donation",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-03-11 10:53:11.269005",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Donation",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Non Profit Manager",
"select": 1,
"share": 1,
"submit": 1,
"write": 1
}
],
"search_fields": "donor_name, email",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "donor_name",
"track_changes": 1
}

View File

@@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
import six
import json
from frappe.model.document import Document
from frappe import _
from frappe.utils import getdate, flt, get_link_to_form
from frappe.email import sendmail_to_system_managers
from erpnext.non_profit.doctype.membership.membership import verify_signature
class Donation(Document):
def validate(self):
if not self.donor or not frappe.db.exists('Donor', self.donor):
# for web forms
user_type = frappe.db.get_value('User', frappe.session.user, 'user_type')
if user_type == 'Website User':
self.create_donor_for_website_user()
else:
frappe.throw(_('Please select a Member'))
def create_donor_for_website_user(self):
donor_name = frappe.get_value('Donor', dict(email=frappe.session.user))
if not donor_name:
user = frappe.get_doc('User', frappe.session.user)
donor = frappe.get_doc(dict(
doctype='Donor',
donor_type=self.get('donor_type'),
email=frappe.session.user,
member_name=user.get_fullname()
)).insert(ignore_permissions=True)
donor_name = donor.name
if self.get('__islocal'):
self.donor = donor_name
def on_payment_authorized(self, *args, **kwargs):
self.load_from_db()
self.create_payment_entry()
def create_payment_entry(self):
settings = frappe.get_doc('Non Profit Settings')
if not settings.automate_donation_payment_entries:
return
if not settings.donation_payment_account:
frappe.throw(_('You need to set <b>Payment Account</b> for Donation in {0}').format(
get_link_to_form('Non Profit Settings', 'Non Profit Settings')))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt=self.doctype, dn=self.name)
frappe.flags.ignore_account_permission = False
pe.paid_from = settings.donation_debit_account
pe.paid_to = settings.donation_payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
pe.flags.ignore_mandatory = True
pe.insert()
pe.submit()
@frappe.whitelist(allow_guest=True)
def capture_razorpay_donations(*args, **kwargs):
"""
Creates Donation from Razorpay Webhook Request Data on payment.captured event
Creates Donor from email if not found
"""
data = frappe.request.get_data(as_text=True)
try:
verify_signature(data, endpoint='Donation')
except Exception as e:
log = frappe.log_error(e, 'Donation Webhook Verification Error')
notify_failure(log)
return { 'status': 'Failed', 'reason': e }
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
payment = data.payload.get('payment', {}).get('entity', {})
payment = frappe._dict(payment)
try:
if not data.event == 'payment.captured':
return
donor = get_donor(payment.email)
if not donor:
donor = create_donor(payment)
donation = create_donation(donor, payment)
donation.run_method('create_payment_entry')
except Exception as e:
message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id)
log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name))
notify_failure(log)
return { 'status': 'Failed', 'reason': e }
return { 'status': 'Success' }
def create_donation(donor, payment):
if not frappe.db.exists('Mode of Payment', payment.method):
create_mode_of_payment(payment.method)
company = get_company_for_donations()
donation = frappe.get_doc({
'doctype': 'Donation',
'company': company,
'donor': donor.name,
'donor_name': donor.donor_name,
'email': donor.email,
'date': getdate(),
'amount': flt(payment.amount),
'mode_of_payment': payment.method,
'razorpay_payment_id': payment.id
}).insert(ignore_mandatory=True)
donation.submit()
return donation
def get_donor(email):
donors = frappe.get_all('Donor',
filters={'email': email},
order_by='creation desc')
try:
return frappe.get_doc('Donor', donors[0]['name'])
except Exception:
return None
@frappe.whitelist()
def create_donor(payment):
donor_details = frappe._dict(payment)
donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type')
donor = frappe.new_doc('Donor')
donor.update({
'donor_name': donor_details.email,
'donor_type': donor_type,
'email': donor_details.email,
'contact': donor_details.contact
})
if donor_details.get('notes'):
donor = get_additional_notes(donor, donor_details)
donor.insert(ignore_mandatory=True)
return donor
def get_company_for_donations():
company = frappe.db.get_single_value('Non Profit Settings', 'donation_company')
if not company:
from erpnext.healthcare.setup import get_company
company = get_company()
return company
def get_additional_notes(donor, donor_details):
if type(donor_details.notes) == dict:
for k, v in donor_details.notes.items():
notes = '\n'.join('{}: {}'.format(k, v))
# extract donor name from notes
if 'name' in k.lower():
donor.update({
'donor_name': donor_details.notes.get(k)
})
# extract pan from notes
if 'pan' in k.lower():
donor.update({
'pan_number': donor_details.notes.get(k)
})
donor.add_comment('Comment', notes)
elif type(donor_details.notes) == str:
donor.add_comment('Comment', donor_details.notes)
return donor
def create_mode_of_payment(method):
frappe.get_doc({
'doctype': 'Mode of Payment',
'mode_of_payment': method
}).insert(ignore_mandatory=True)
def notify_failure(log):
try:
content = '''
Dear System Manager,
Razorpay webhook for creating donation failed due to some reason.
Please check the error log linked below
Error Log: {0}
Regards, Administrator
'''.format(get_link_to_form('Error Log', log.name))
sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content)
except Exception:
pass

View File

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

View File

@@ -0,0 +1,76 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from erpnext.non_profit.doctype.donation.donation import create_donation
class TestDonation(unittest.TestCase):
def setUp(self):
create_donor_type()
settings = frappe.get_doc('Non Profit Settings')
settings.company = '_Test Company'
settings.donation_company = '_Test Company'
settings.default_donor_type = '_Test Donor'
settings.automate_donation_payment_entries = 1
settings.donation_debit_account = 'Debtors - _TC'
settings.donation_payment_account = 'Cash - _TC'
settings.creation_user = 'Administrator'
settings.flags.ignore_permissions = True
settings.save()
def test_payment_entry_for_donations(self):
donor = create_donor()
create_mode_of_payment()
payment = frappe._dict({
'amount': 100,
'method': 'Debit Card',
'id': 'pay_MeXAmsgeKOhq7O'
})
donation = create_donation(donor, payment)
self.assertTrue(donation.name)
# Naive test to check if at all payment entry is generated
# This method is actually triggered from Payment Gateway
# In any case if details were missing, this would throw an error
donation.on_payment_authorized()
donation.reload()
self.assertEquals(donation.paid, 1)
self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name}))
def create_donor_type():
if not frappe.db.exists('Donor Type', '_Test Donor'):
frappe.get_doc({
'doctype': 'Donor Type',
'donor_type': '_Test Donor'
}).insert()
def create_donor():
donor = frappe.db.exists('Donor', 'donor@test.com')
if donor:
return frappe.get_doc('Donor', 'donor@test.com')
else:
return frappe.get_doc({
'doctype': 'Donor',
'donor_name': '_Test Donor',
'donor_type': '_Test Donor',
'email': 'donor@test.com'
}).insert()
def create_mode_of_payment():
if not frappe.db.exists('Mode of Payment', 'Debit Card'):
frappe.get_doc({
'doctype': 'Mode of Payment',
'mode_of_payment': 'Debit Card',
'accounts': [{
'company': '_Test Company',
'default_account': 'Cash - _TC'
}]
}).insert()

View File

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

View File

@@ -11,3 +11,8 @@ class Donor(Document):
"""Load address and contacts in `__onload`""" """Load address and contacts in `__onload`"""
load_address_and_contact(self) load_address_and_contact(self)
def validate(self):
from frappe.utils import validate_email_address
if self.email:
validate_email_address(self.email.strip(), True)

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on('Member', { frappe.ui.form.on('Member', {
setup: function(frm) { setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val && (frm.doc.subscription_id || frm.doc.customer_id)) { if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
frm.set_df_property('razorpay_details_section', 'hidden', false); frm.set_df_property('razorpay_details_section', 'hidden', false);
} }

View File

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

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on('Membership', { frappe.ui.form.on('Membership', {
setup: function(frm) { setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => {
if (val) frm.set_df_property("razorpay_details_section", "hidden", false); if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
}) })
}, },
@@ -26,7 +26,7 @@ frappe.ui.form.on('Membership', {
}); });
}); });
frappe.db.get_single_value("Membership Settings", "send_email").then(val => { frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => {
if (val) frm.add_custom_button("Send Acknowledgement", () => { if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => { frm.call("send_acknowlement").then(() => {
frm.reload_doc(); frm.reload_doc();

View File

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

View File

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

View File

@@ -10,33 +10,7 @@ from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase): class TestMembership(unittest.TestCase):
def setUp(self): def setUp(self):
# Get default company plan = setup_membership()
company = frappe.get_doc("Company", erpnext.get_default_company())
# update membership settings
settings = frappe.get_doc("Membership Settings")
# Enable razorpay
settings.enable_razorpay = 1
settings.billing_cycle = "Monthly"
settings.billing_frequency = 24
# Enable invoicing
settings.enable_invoicing = 1
settings.make_payment_entry = 1
settings.company = company.name
settings.payment_account = company.default_cash_account
settings.debit_account = company.default_receivable_account
settings.save()
# make test plan
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
plan = frappe.new_doc("Membership Type")
plan.membership_type = "_rzpy_test_milythm"
plan.amount = 100
plan.razorpay_plan_id = "_rzpy_test_milythm"
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
plan.insert()
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
# make test member # make test member
self.member_doc = create_member(frappe._dict({ self.member_doc = create_member(frappe._dict({
@@ -78,7 +52,7 @@ class TestMembership(unittest.TestCase):
}) })
def set_config(key, value): def set_config(key, value):
frappe.db.set_value("Membership Settings", None, key, value) frappe.db.set_value("Non Profit Settings", None, key, value)
def make_membership(member, payload={}): def make_membership(member, payload={}):
data = { data = {
@@ -109,3 +83,36 @@ def create_item(item_code):
else: else:
item = frappe.get_doc("Item", item_code) item = frappe.get_doc("Item", item_code)
return item return item
def setup_membership():
# Get default company
company = frappe.get_doc("Company", erpnext.get_default_company())
# update non profit settings
settings = frappe.get_doc("Non Profit Settings")
# Enable razorpay
settings.enable_razorpay_for_memberships = 1
settings.billing_cycle = "Monthly"
settings.billing_frequency = 24
# Enable invoicing
settings.allow_invoicing = 1
settings.automate_membership_payment_entries = 1
settings.company = company.name
settings.donation_company = company.name
settings.membership_payment_account = company.default_cash_account
settings.membership_debit_account = company.default_receivable_account
settings.flags.ignore_mandatory = True
settings.save()
# make test plan
if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
plan = frappe.new_doc("Membership Type")
plan.membership_type = "_rzpy_test_milythm"
plan.amount = 100
plan.razorpay_plan_id = "_rzpy_test_milythm"
plan.linked_item = create_item("_Test Item for Non Profit Membership").name
plan.insert()
else:
plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
return plan

View File

@@ -1,192 +0,0 @@
{
"actions": [],
"creation": "2020-03-29 12:57:03.005120",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable_razorpay",
"razorpay_settings_section",
"billing_cycle",
"billing_frequency",
"webhook_secret",
"column_break_6",
"enable_invoicing",
"create_for_web_forms",
"make_payment_entry",
"company",
"debit_account",
"payment_account",
"column_break_9",
"send_email",
"send_invoice",
"membership_print_format",
"inv_print_format",
"email_template"
],
"fields": [
{
"fieldname": "billing_cycle",
"fieldtype": "Select",
"label": "Billing Cycle",
"options": "Monthly\nYearly"
},
{
"default": "0",
"fieldname": "enable_razorpay",
"fieldtype": "Check",
"label": "Enable RazorPay For Memberships"
},
{
"depends_on": "eval:doc.enable_razorpay",
"fieldname": "razorpay_settings_section",
"fieldtype": "Section Break",
"label": "RazorPay Settings"
},
{
"description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
"fieldname": "billing_frequency",
"fieldtype": "Int",
"label": "Billing Frequency"
},
{
"fieldname": "webhook_secret",
"fieldtype": "Password",
"label": "Webhook Secret",
"read_only": 1
},
{
"fieldname": "column_break_6",
"fieldtype": "Section Break",
"label": "Invoicing"
},
{
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Account"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.enable_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"mandatory_depends_on": "eval:doc.enable_auto_invoicing",
"options": "Company"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
},
{
"default": "0",
"fieldname": "send_email",
"fieldtype": "Check",
"label": "Send Membership Acknowledgement"
},
{
"depends_on": "eval: doc.send_invoice",
"fieldname": "inv_print_format",
"fieldtype": "Link",
"label": "Invoice Print Format",
"mandatory_depends_on": "eval: doc.send_invoice",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "membership_print_format",
"fieldtype": "Link",
"label": "Membership Print Format",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "email_template",
"fieldtype": "Link",
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
},
{
"default": "0",
"fieldname": "enable_invoicing",
"fieldtype": "Check",
"label": "Enable Invoicing",
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
"fieldname": "make_payment_entry",
"fieldtype": "Check",
"label": "Make Payment Entry"
},
{
"depends_on": "eval:doc.make_payment_entry",
"fieldname": "payment_account",
"fieldtype": "Link",
"label": "Payment To",
"mandatory_depends_on": "eval:doc.make_payment_entry",
"options": "Account"
},
{
"default": "0",
"depends_on": "eval:doc.enable_invoicing",
"description": "Automatically create an invoice when payment is authorized from a web form entry",
"fieldname": "create_for_web_forms",
"fieldtype": "Check",
"label": "Auto Create Invoice for Web Forms"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Member",
"share": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -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.") + "<br><br>" + 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")]

View File

@@ -3,11 +3,11 @@
frappe.ui.form.on('Membership Type', { frappe.ui.form.on('Membership Type', {
refresh: function (frm) { refresh: function (frm) {
frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => { frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false); if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
}); });
frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => { frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false); if (val) frm.set_df_property('linked_item', 'hidden', false);
}); });

View File

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

View File

@@ -0,0 +1,273 @@
{
"actions": [],
"creation": "2020-03-29 12:57:03.005120",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"enable_razorpay_for_memberships",
"razorpay_settings_section",
"billing_cycle",
"billing_frequency",
"membership_webhook_secret",
"column_break_6",
"allow_invoicing",
"automate_membership_invoicing",
"automate_membership_payment_entries",
"company",
"membership_debit_account",
"membership_payment_account",
"column_break_9",
"send_email",
"send_invoice",
"membership_print_format",
"inv_print_format",
"email_template",
"donation_settings_section",
"donation_company",
"default_donor_type",
"donation_webhook_secret",
"column_break_22",
"automate_donation_payment_entries",
"donation_debit_account",
"donation_payment_account",
"section_break_27",
"creation_user"
],
"fields": [
{
"fieldname": "billing_cycle",
"fieldtype": "Select",
"label": "Billing Cycle",
"options": "Monthly\nYearly"
},
{
"depends_on": "eval:doc.enable_razorpay_for_memberships",
"fieldname": "razorpay_settings_section",
"fieldtype": "Section Break",
"label": "RazorPay Settings for Memberships"
},
{
"description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
"fieldname": "billing_frequency",
"fieldtype": "Int",
"label": "Billing Frequency"
},
{
"fieldname": "column_break_6",
"fieldtype": "Section Break",
"label": "Membership Invoicing"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"description": "This company will be set for the Memberships created via webhook.",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"default": "0",
"depends_on": "eval:doc.allow_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
},
{
"default": "0",
"fieldname": "send_email",
"fieldtype": "Check",
"label": "Send Membership Acknowledgement"
},
{
"depends_on": "eval: doc.send_invoice",
"fieldname": "inv_print_format",
"fieldtype": "Link",
"label": "Invoice Print Format",
"mandatory_depends_on": "eval: doc.send_invoice",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "membership_print_format",
"fieldtype": "Link",
"label": "Membership Print Format",
"options": "Print Format"
},
{
"depends_on": "eval:doc.send_email",
"fieldname": "email_template",
"fieldtype": "Link",
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
},
{
"default": "0",
"fieldname": "allow_invoicing",
"fieldtype": "Check",
"label": "Allow Invoicing for Memberships",
"mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
},
{
"default": "0",
"depends_on": "eval:doc.allow_invoicing",
"description": "Automatically create an invoice when payment is authorized from a web form entry",
"fieldname": "automate_membership_invoicing",
"fieldtype": "Check",
"label": "Automate Invoicing for Web Forms"
},
{
"default": "0",
"depends_on": "eval:doc.allow_invoicing",
"description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
"fieldname": "automate_membership_payment_entries",
"fieldtype": "Check",
"label": "Automate Payment Entry Creation"
},
{
"default": "0",
"fieldname": "enable_razorpay_for_memberships",
"fieldtype": "Check",
"label": "Enable RazorPay For Memberships"
},
{
"depends_on": "eval:doc.automate_membership_payment_entries",
"description": "Account for accepting membership payments",
"fieldname": "membership_payment_account",
"fieldtype": "Link",
"label": "Membership Payment To",
"mandatory_depends_on": "eval:doc.automate_membership_payment_entries",
"options": "Account"
},
{
"fieldname": "membership_webhook_secret",
"fieldtype": "Password",
"label": "Membership Webhook Secret",
"read_only": 1
},
{
"fieldname": "donation_webhook_secret",
"fieldtype": "Password",
"label": "Donation Webhook Secret",
"read_only": 1
},
{
"depends_on": "automate_donation_payment_entries",
"description": "Account for accepting donation payments",
"fieldname": "donation_payment_account",
"fieldtype": "Link",
"label": "Donation Payment To",
"mandatory_depends_on": "automate_donation_payment_entries",
"options": "Account"
},
{
"default": "0",
"description": "Auto creates Payment Entry for Donations created from web forms.",
"fieldname": "automate_donation_payment_entries",
"fieldtype": "Check",
"label": "Automate Donation Payment Entries"
},
{
"depends_on": "eval:doc.allow_invoicing",
"fieldname": "membership_debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "eval:doc.allow_invoicing",
"options": "Account"
},
{
"depends_on": "automate_donation_payment_entries",
"fieldname": "donation_debit_account",
"fieldtype": "Link",
"label": "Debit Account",
"mandatory_depends_on": "automate_donation_payment_entries",
"options": "Account"
},
{
"description": "This company will be set for the Donations created via webhook.",
"fieldname": "donation_company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "donation_settings_section",
"fieldtype": "Section Break",
"label": "Donation Settings"
},
{
"fieldname": "column_break_22",
"fieldtype": "Column Break"
},
{
"description": "This Donor Type will be set for the Donor created via Donation web form entry.",
"fieldname": "default_donor_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Default Donor Type",
"options": "Donor Type",
"reqd": 1
},
{
"fieldname": "section_break_27",
"fieldtype": "Section Break"
},
{
"description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.",
"fieldname": "creation_user",
"fieldtype": "Link",
"label": "Creation User",
"options": "User",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-03-11 10:43:38.124240",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Manager",
"share": 1,
"write": 1
},
{
"email": 1,
"print": 1,
"read": 1,
"role": "Non Profit Member",
"share": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.integrations.utils import get_payment_gateway_controller
from frappe.model.document import Document
class NonProfitSettings(Document):
def generate_webhook_secret(self, field="membership_webhook_secret"):
key = frappe.generate_hash(length=20)
self.set(field, key)
self.save()
secret_for = "Membership" if field == "membership_webhook_secret" else "Donation"
frappe.msgprint(
_("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "<br><br>" + 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")]

View File

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

View File

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

View File

@@ -756,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.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
erpnext.patches.v13_0.create_website_items erpnext.patches.v13_0.create_website_items
erpnext.patches.v13_0.populate_e_commerce_settings erpnext.patches.v13_0.populate_e_commerce_settings
erpnext.patches.v13_0.make_homepage_products_website_items erpnext.patches.v13_0.make_homepage_products_website_items

View File

@@ -0,0 +1,22 @@
from __future__ import unicode_literals
import frappe
from frappe.model.utils.rename_field import rename_field
def execute():
if frappe.db.table_exists("Membership Settings"):
frappe.rename_doc("DocType", "Membership Settings", "Non Profit Settings")
frappe.reload_doctype("Non Profit Settings", force=True)
if frappe.db.table_exists("Non Profit Settings"):
rename_fields_map = {
"enable_invoicing": "allow_invoicing",
"create_for_web_forms": "automate_membership_invoicing",
"make_payment_entry": "automate_membership_payment_entries",
"enable_razorpay": "enable_razorpay_for_memberships",
"debit_account": "membership_debit_account",
"payment_account": "membership_payment_account",
"webhook_secret": "membership_webhook_secret"
}
for old_name, new_name in rename_fields_map.items():
rename_field("Non Profit Settings", old_name, new_name)

View File

@@ -0,0 +1,16 @@
import frappe
from erpnext.regional.india.setup import make_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
make_custom_fields()
if not frappe.db.exists('Party Type', 'Donor'):
frappe.get_doc({
'doctype': 'Party Type',
'party_type': 'Donor',
'account_type': 'Receivable'
}).insert(ignore_permissions=True)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class GratuityApplicableComponent(Document):
pass

View File

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

View File

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

View File

@@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe import _
class GratuityRule(Document):
def validate(self):
for current_slab in self.gratuity_rule_slabs:
if (current_slab.from_year > current_slab.to_year) and current_slab.to_year != 0:
frappe(_("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx))
if current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1:
frappe.throw(_("You can not define multiple slabs if you have a slab with no lower and upper limits."))
def get_gratuity_rule(name, slabs, **args):
args = frappe._dict(args)
rule = frappe.new_doc("Gratuity Rule")
rule.name = name
rule.calculate_gratuity_amount_based_on = args.calculate_gratuity_amount_based_on or "Current Slab"
rule.work_experience_calculation_method = args.work_experience_calculation_method or "Take Exact Completed Years"
rule.minimum_year_for_gratuity = 1
for slab in slabs:
slab = frappe._dict(slab)
rule.append("gratuity_rule_slabs", slab)
return rule

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class GratuityRuleSlab(Document):
pass

View File

@@ -80,9 +80,26 @@ class SalarySlip(TransactionBase):
if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip() self.email_salary_slip()
self.update_payment_status_for_gratuity()
def update_payment_status_for_gratuity(self):
add_salary = frappe.db.get_all("Additional Salary",
filters = {
"payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
"employee": self.employee,
"ref_doctype": "Gratuity",
"docstatus": 1,
}, fields = ["ref_docname", "name"], limit=1)
if len(add_salary):
status = "Paid" if self.docstatus == 1 else "Unpaid"
if add_salary[0].name in [data.additional_salary for data in self.earnings]:
frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
def on_cancel(self): def on_cancel(self):
self.set_status() self.set_status()
self.update_status() self.update_status()
self.update_payment_status_for_gratuity()
self.cancel_loan_repayment_entry() self.cancel_loan_repayment_entry()
def on_trash(self): def on_trash(self):
@@ -506,7 +523,8 @@ class SalarySlip(TransactionBase):
return amount return amount
except NameError as err: except NameError as err:
frappe.throw(_("Name error: {0}").format(err)) frappe.throw(_("{0} <br> This error can be due to missing or deleted field.").format(err),
title=_("Name error"))
except SyntaxError as err: except SyntaxError as err:
frappe.throw(_("Syntax error in formula or condition: {0}").format(err)) frappe.throw(_("Syntax error in formula or condition: {0}").format(err))
except Exception as e: except Exception as e:
@@ -573,6 +591,7 @@ class SalarySlip(TransactionBase):
for d in self.get(key): for d in self.get(key):
if d.salary_component == struct_row.salary_component: if d.salary_component == struct_row.salary_component:
component_row = d component_row = d
if not component_row or (struct_row.get("is_additional_component") and not overwrite): if not component_row or (struct_row.get("is_additional_component") and not overwrite):
if amount: if amount:
self.append(key, { self.append(key, {
@@ -930,7 +949,8 @@ class SalarySlip(TransactionBase):
if condition: if condition:
return frappe.safe_eval(condition, self.whitelisted_globals, data) return frappe.safe_eval(condition, self.whitelisted_globals, data)
except NameError as err: except NameError as err:
frappe.throw(_("Name error: {0}").format(err)) frappe.throw(_("{0} <br> This error can be due to missing or deleted field.").format(err),
title=_("Name error"))
except SyntaxError as err: except SyntaxError as err:
frappe.throw(_("Syntax error in condition: {0}").format(err)) frappe.throw(_("Syntax error in condition: {0}").format(err))
except Exception as e: 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): def generate_password_for_pdf(policy_template, employee):
employee = frappe.get_doc("Employee", employee) employee = frappe.get_doc("Employee", employee)
return policy_template.format(**employee.as_dict()) return policy_template.format(**employee.as_dict())

View File

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

View File

@@ -142,6 +142,8 @@ frappe.ui.form.on('Salary Structure', {
], ],
primary_action: function() { primary_action: function() {
var data = d.get_values(); var data = d.get_values();
delete data.company
delete data.currency
frappe.call({ frappe.call({
doc: frm.doc, doc: frm.doc,
method: "assign_salary_structure", method: "assign_salary_structure",

View File

@@ -158,16 +158,18 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
let me = this; let me = this;
frappe.flags.round_off_applicable_accounts = []; frappe.flags.round_off_applicable_accounts = [];
return frappe.call({ if (me.frm.doc.company) {
"method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", return frappe.call({
"args": { "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
"company": me.frm.doc.company, "args": {
"account_list": frappe.flags.round_off_applicable_accounts "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); callback: function(r) {
} frappe.flags.round_off_applicable_accounts.push(...r.message);
}); }
});
}
}, },
determine_exclusive_rate: function() { determine_exclusive_rate: function() {

View File

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

View File

@@ -1,11 +1,18 @@
frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( {
make_input() { make_input() {
if (!this.df.read_only) { this._super();
this._super();
}
if (this.df.options == 'Phone') { if (this.df.options == 'Phone') {
this.setup_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() { setup_phone() {
if (frappe.phone_call.handler) { if (frappe.phone_call.handler) {

View File

@@ -595,21 +595,7 @@ erpnext.utils.update_child_items = function(opts) {
} }
erpnext.utils.map_current_doc = function(opts) { erpnext.utils.map_current_doc = function(opts) {
let query_args = {}; function _map() {
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() {
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) { if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {
// remove first item row if empty // remove first item row if empty
if(!cur_frm.doc.items[0].item_code) { 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, doctype: opts.source_doctype,
target: opts.target, target: opts.target,
date_field: opts.date_field || undefined, date_field: opts.date_field || undefined,
@@ -703,7 +703,11 @@ erpnext.utils.map_current_doc = function(opts) {
_map(); _map();
}, },
}); });
} else if(opts.source_name) {
return d;
}
if (opts.source_name) {
opts.source_name = [opts.source_name]; opts.source_name = [opts.source_name];
_map(); _map();
} }

View File

@@ -33,8 +33,7 @@
}, },
{ {
"fieldname": "sb_00", "fieldname": "sb_00",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Agenda"
}, },
{ {
"fieldname": "agenda", "fieldname": "agenda",
@@ -44,13 +43,12 @@
}, },
{ {
"fieldname": "sb_01", "fieldname": "sb_01",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"label": "Minutes"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2020-10-27 16:36:45.657883", "modified": "2021-02-27 16:36:45.657883",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Quality Management", "module": "Quality Management",
"name": "Quality Meeting", "name": "Quality Meeting",
@@ -85,4 +83,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -0,0 +1,67 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Tax Exemption 80G Certificate', {
refresh: function(frm) {
if (frm.doc.donor) {
frm.set_query('donation', function() {
return {
filters: {
docstatus: 1,
donor: frm.doc.donor
}
};
});
}
},
recipient: function(frm) {
if (frm.doc.recipient === 'Donor') {
frm.set_value({
'member': '',
'member_name': '',
'member_email': '',
'member_pan_number': '',
'fiscal_year': '',
'total': 0,
'payments': []
});
} else {
frm.set_value({
'donor': '',
'donor_name': '',
'donor_email': '',
'donor_pan_number': '',
'donation': '',
'date_of_donation': '',
'amount': 0,
'mode_of_payment': '',
'razorpay_payment_id': ''
});
}
},
get_payments: function(frm) {
frm.call({
doc: frm.doc,
method: 'get_payments',
freeze: true
});
},
company: function(frm) {
if ((frm.doc.member || frm.doc.donor) && frm.doc.company) {
frm.call({
doc: frm.doc,
method: 'set_company_address',
freeze: true
});
}
},
donation: function(frm) {
if (frm.doc.recipient === 'Donor' && !frm.doc.donor) {
frappe.msgprint(__('Please select donor first'));
}
}
});

View File

@@ -0,0 +1,297 @@
{
"actions": [],
"autoname": "naming_series:",
"creation": "2021-02-15 12:37:21.577042",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"recipient",
"member",
"member_name",
"member_email",
"member_pan_number",
"donor",
"donor_name",
"donor_email",
"donor_pan_number",
"column_break_4",
"date",
"fiscal_year",
"section_break_11",
"company",
"company_address",
"company_address_display",
"column_break_14",
"company_pan_number",
"company_80g_number",
"company_80g_wef",
"title",
"section_break_6",
"get_payments",
"payments",
"total",
"donation_details_section",
"donation",
"date_of_donation",
"amount",
"column_break_27",
"mode_of_payment",
"razorpay_payment_id"
],
"fields": [
{
"fieldname": "recipient",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Certificate Recipient",
"options": "Member\nDonor",
"reqd": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fieldname": "member",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Member",
"mandatory_depends_on": "eval:doc.recipient === \"Member\";",
"options": "Member"
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fetch_from": "member.member_name",
"fieldname": "member_name",
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fieldname": "donor",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Donor",
"mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
"options": "Donor"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "date",
"fieldtype": "Date",
"label": "Date",
"reqd": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"fieldname": "payments",
"fieldtype": "Table",
"label": "Payments",
"options": "Tax Exemption 80G Certificate Detail"
},
{
"fieldname": "total",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fieldname": "fiscal_year",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Fiscal Year",
"options": "Fiscal Year"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "get_payments",
"fieldtype": "Button",
"label": "Get Memberships"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "NPO-80G-.YYYY.-"
},
{
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"label": "Company Details"
},
{
"fieldname": "company_address",
"fieldtype": "Link",
"label": "Company Address",
"options": "Address"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"fetch_from": "company.pan_details",
"fieldname": "company_pan_number",
"fieldtype": "Data",
"label": "PAN Number",
"read_only": 1
},
{
"fieldname": "company_address_display",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Company Address Display",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "company.company_80g_number",
"fieldname": "company_80g_number",
"fieldtype": "Data",
"label": "80G Number",
"read_only": 1
},
{
"fetch_from": "company.with_effect_from",
"fieldname": "company_80g_wef",
"fieldtype": "Date",
"label": "80G With Effect From",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fieldname": "donation_details_section",
"fieldtype": "Section Break",
"label": "Donation Details"
},
{
"fieldname": "donation",
"fieldtype": "Link",
"label": "Donation",
"mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
"options": "Donation"
},
{
"fetch_from": "donation.amount",
"fieldname": "amount",
"fieldtype": "Currency",
"label": "Amount",
"read_only": 1
},
{
"fetch_from": "donation.mode_of_payment",
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1
},
{
"fetch_from": "donation.razorpay_payment_id",
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "RazorPay Payment ID",
"read_only": 1
},
{
"fetch_from": "donation.date",
"fieldname": "date_of_donation",
"fieldtype": "Date",
"label": "Date of Donation",
"read_only": 1
},
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fetch_from": "donor.donor_name",
"fieldname": "donor_name",
"fieldtype": "Data",
"label": "Donor Name",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fetch_from": "donor.email",
"fieldname": "donor_email",
"fieldtype": "Data",
"label": "Email",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fetch_from": "member.email_id",
"fieldname": "member_email",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Email",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Member\";",
"fetch_from": "member.pan_number",
"fieldname": "member_pan_number",
"fieldtype": "Data",
"label": "PAN Details",
"read_only": 1
},
{
"depends_on": "eval:doc.recipient === \"Donor\";",
"fetch_from": "donor.pan_number",
"fieldname": "donor_pan_number",
"fieldtype": "Data",
"label": "PAN Details",
"read_only": 1
},
{
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
"print_hide": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-02-22 00:03:34.215633",
"modified_by": "Administrator",
"module": "Regional",
"name": "Tax Exemption 80G Certificate",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"search_fields": "member, member_name",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "title",
"track_changes": 1
}

View File

@@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate, flt, get_link_to_form
from erpnext.accounts.utils import get_fiscal_year
from frappe.contacts.doctype.address.address import get_company_address
class TaxExemption80GCertificate(Document):
def validate(self):
self.validate_date()
self.validate_duplicates()
self.validate_company_details()
self.set_company_address()
self.set_title()
def validate_date(self):
if self.recipient == 'Member':
if getdate(self.date):
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
if not (fiscal_year.year_start_date <= getdate(self.date) \
<= fiscal_year.year_end_date):
frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
def validate_duplicates(self):
if self.recipient == 'Donor':
certificate = frappe.db.exists(self.doctype, {'donation': self.donation})
if certificate:
frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format(
get_link_to_form(self.doctype, certificate), frappe.bold(self.donation)
), title=_('Duplicate Certificate'))
def validate_company_details(self):
fields = ['company_80g_number', 'with_effect_from', 'pan_details']
company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True)
if not company_details.company_80g_number:
frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'),
get_link_to_form('Company', self.company)))
if not company_details.pan_details:
frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
get_link_to_form('Company', self.company)))
def set_company_address(self):
address = get_company_address(self.company)
self.company_address = address.company_address
self.company_address_display = address.company_address_display
def set_title(self):
if self.recipient == "Member":
self.title = self.member_name
else:
self.title = self.donor_name
def get_payments(self):
if not self.member:
frappe.throw(_('Please select a Member first.'))
fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
memberships = frappe.db.get_all('Membership', {
'member': self.member,
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled')
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'])
if not memberships:
frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
total = 0
self.payments = []
for doc in memberships:
self.append('payments', {
'date': doc.from_date,
'amount': doc.amount,
'invoice_id': doc.invoice,
'razorpay_payment_id': doc.payment_id,
'membership': doc.name
})
total += flt(doc.amount)
self.total = total

View File

@@ -0,0 +1,101 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
from frappe.utils import getdate
from erpnext.accounts.utils import get_fiscal_year
from erpnext.non_profit.doctype.donation.test_donation import create_donor, create_mode_of_payment, create_donor_type
from erpnext.non_profit.doctype.donation.donation import create_donation
from erpnext.non_profit.doctype.membership.test_membership import setup_membership, make_membership
from erpnext.non_profit.doctype.member.member import create_member
class TestTaxExemption80GCertificate(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabTax Exemption 80G Certificate`')
frappe.db.sql('delete from `tabMembership`')
create_donor_type()
settings = frappe.get_doc('Non Profit Settings')
settings.company = '_Test Company'
settings.donation_company = '_Test Company'
settings.default_donor_type = '_Test Donor'
settings.creation_user = 'Administrator'
settings.save()
company = frappe.get_doc('Company', '_Test Company')
company.pan_details = 'BBBTI3374C'
company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087'
company.with_effect_from = getdate()
company.save()
def test_duplicate_donation_certificate(self):
donor = create_donor()
create_mode_of_payment()
payment = frappe._dict({
'amount': 100,
'method': 'Debit Card',
'id': 'pay_MeXAmsgeKOhq7O'
})
donation = create_donation(donor, payment)
args = frappe._dict({
'recipient': 'Donor',
'donor': donor.name,
'donation': donation.name
})
certificate = create_80g_certificate(args)
certificate.insert()
# check company details
self.assertEquals(certificate.company_pan_number, 'BBBTI3374C')
self.assertEquals(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087')
# check donation details
self.assertEquals(certificate.amount, donation.amount)
duplicate_certificate = create_80g_certificate(args)
# duplicate validation
self.assertRaises(frappe.ValidationError, duplicate_certificate.insert)
def test_membership_80g_certificate(self):
plan = setup_membership()
# make test member
member_doc = create_member(frappe._dict({
'fullname': "_Test_Member",
'email': "_test_member_erpnext@example.com",
'plan_id': plan.name
}))
member_doc.make_customer_and_link()
member = member_doc.name
membership = make_membership(member, { "from_date": getdate() })
invoice = membership.generate_invoice(save=True)
args = frappe._dict({
'recipient': 'Member',
'member': member,
'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name')
})
certificate = create_80g_certificate(args)
certificate.get_payments()
certificate.insert()
self.assertEquals(len(certificate.payments), 1)
self.assertEquals(certificate.payments[0].amount, membership.amount)
self.assertEquals(certificate.payments[0].invoice_id, invoice.name)
def create_80g_certificate(args):
certificate = frappe.get_doc({
'doctype': 'Tax Exemption 80G Certificate',
'recipient': args.recipient,
'date': getdate(),
'company': '_Test Company'
})
certificate.update(args)
return certificate

View File

@@ -0,0 +1,66 @@
{
"actions": [],
"creation": "2021-02-15 12:43:52.754124",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"date",
"amount",
"invoice_id",
"column_break_4",
"razorpay_payment_id",
"membership"
],
"fields": [
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Date",
"reqd": 1
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"reqd": 1
},
{
"fieldname": "invoice_id",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Invoice ID",
"options": "Sales Invoice",
"reqd": 1
},
{
"fieldname": "razorpay_payment_id",
"fieldtype": "Data",
"label": "Razorpay Payment ID"
},
{
"fieldname": "membership",
"fieldtype": "Link",
"label": "Membership",
"options": "Membership"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-02-15 16:35:10.777587",
"modified_by": "Administrator",
"module": "Regional",
"name": "Tax Exemption 80G Certificate Detail",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
# import frappe
from frappe.model.document import Document
class TaxExemption80GCertificateDetail(Document):
pass

View File

@@ -21,6 +21,7 @@ def setup_company_independent_fixtures():
add_permissions() add_permissions()
add_custom_roles_for_reports() add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
create_gratuity_rule()
add_print_formats() add_print_formats()
def add_hsn_sac_codes(): def add_hsn_sac_codes():
@@ -105,8 +106,9 @@ def add_print_formats():
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice") frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) 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): def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -398,9 +400,9 @@ def make_custom_fields(update=True):
si_einvoice_fields = [ si_einvoice_fields = [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
@@ -498,6 +500,14 @@ def make_custom_fields(update=True):
fieldtype='Link', options='Salary Component', insert_after='basic_component'), fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='arrear_component', label='Arrear Component', dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_component'), fieldtype='Link', options='Salary Component', insert_after='hra_component'),
dict(fieldname='non_profit_section', label='Non Profit Settings',
fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
dict(fieldname='company_80g_number', label='80G Number',
fieldtype='Data', insert_after='non_profit_section'),
dict(fieldname='with_effect_from', label='80G With Effect From',
fieldtype='Date', insert_after='company_80g_number'),
dict(fieldname='pan_details', label='PAN Number',
fieldtype='Data', insert_after='with_effect_from')
], ],
'Employee Tax Exemption Declaration':[ 'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption', dict(fieldname='hra_section', label='HRA Exemption',
@@ -580,7 +590,15 @@ def make_custom_fields(update=True):
'options': '\nWith Payment of Tax\nWithout Payment of Tax' 'options': '\nWith Payment of Tax\nWithout Payment of Tax'
} }
], ],
"Member": [ 'Member': [
{
'fieldname': 'pan_number',
'label': 'PAN Details',
'fieldtype': 'Data',
'insert_after': 'email_id'
}
],
'Donor': [
{ {
'fieldname': 'pan_number', 'fieldname': 'pan_number',
'label': 'PAN Details', 'label': 'PAN Details',
@@ -642,7 +660,7 @@ def set_tax_withholding_category(company):
pass pass
docs = get_tds_details(accounts, fiscal_year) docs = get_tds_details(accounts, fiscal_year)
for d in docs: for d in docs:
try: try:
doc = frappe.get_doc(d) doc = frappe.get_doc(d)
@@ -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] fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
if not fy_exist: if not fy_exist:
doc.append("rates", d.get('rates')[0]) doc.append("rates", d.get('rates')[0])
doc.flags.ignore_permissions = True doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True doc.flags.ignore_mandatory = True
doc.save() doc.save()
@@ -822,4 +840,24 @@ def get_tds_details(accounts, fiscal_year):
doctype="Tax Withholding Category", accounts=accounts, doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}]) "single_threshold": 2500, "cumulative_threshold": 0}])
] ]
def create_gratuity_rule():
# Standard Indain Gratuity Rule
if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
rule = frappe.new_doc("Gratuity Rule")
rule.name = "Indian Standard Gratuity Rule"
rule.calculate_gratuity_amount_based_on = "Current Slab"
rule.work_experience_calculation_method = "Round Off Work Experience"
rule.minimum_year_for_gratuity = 5
fraction = 15/26
rule.append("gratuity_rule_slabs", {
"from_year": 0,
"to_year":0,
"fraction_of_applicable_earnings": fraction
})
rule.flags.ignore_mandatory = True
rule.save()

Some files were not shown because too many files have changed in this diff Show More