From fc2cb3a85e4fd24f767b4c1e3bbd7ea321d0f4c5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 10 Aug 2020 11:52:34 +0530 Subject: [PATCH 01/64] Feat: gratuity --- erpnext/hr/doctype/employee/employee.json | 2 +- erpnext/payroll/doctype/gratuity/__init__.py | 0 erpnext/payroll/doctype/gratuity/gratuity.js | 46 +++++ .../payroll/doctype/gratuity/gratuity.json | 192 ++++++++++++++++++ erpnext/payroll/doctype/gratuity/gratuity.py | 113 +++++++++++ .../payroll/doctype/gratuity/test_gratuity.py | 10 + .../gratuity_applicable_component/__init__.py | 0 .../gratuity_applicable_component.json | 32 +++ .../gratuity_applicable_component.py | 10 + .../payroll/doctype/gratuity_rule/__init__.py | 0 .../doctype/gratuity_rule/gratuity_rule.js | 8 + .../doctype/gratuity_rule/gratuity_rule.json | 88 ++++++++ .../doctype/gratuity_rule/gratuity_rule.py | 10 + .../gratuity_rule/test_gratuity_rule.py | 10 + .../doctype/gratuity_rule_slab/__init__.py | 0 .../gratuity_rule_slab.json | 45 ++++ .../gratuity_rule_slab/gratuity_rule_slab.py | 10 + 17 files changed, 575 insertions(+), 1 deletion(-) create mode 100644 erpnext/payroll/doctype/gratuity/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity/gratuity.js create mode 100644 erpnext/payroll/doctype/gratuity/gratuity.json create mode 100644 erpnext/payroll/doctype/gratuity/gratuity.py create mode 100644 erpnext/payroll/doctype/gratuity/test_gratuity.py create mode 100644 erpnext/payroll/doctype/gratuity_applicable_component/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json create mode 100644 erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py create mode 100644 erpnext/payroll/doctype/gratuity_rule/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js create mode 100644 erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json create mode 100644 erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py create mode 100644 erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py create mode 100644 erpnext/payroll/doctype/gratuity_rule_slab/__init__.py create mode 100644 erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json create mode 100644 erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json index 4f1c04ff5d0..b60e39282b8 100644 --- a/erpnext/hr/doctype/employee/employee.json +++ b/erpnext/hr/doctype/employee/employee.json @@ -813,7 +813,7 @@ "idx": 24, "image_field": "image", "links": [], - "modified": "2020-10-16 15:02:04.283657", + "modified": "2020-12-02 15:58:23.805489", "modified_by": "Administrator", "module": "HR", "name": "Employee", diff --git a/erpnext/payroll/doctype/gratuity/__init__.py b/erpnext/payroll/doctype/gratuity/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js new file mode 100644 index 00000000000..cbf5119061f --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -0,0 +1,46 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Gratuity', { + refresh: function(frm){ + if(frm.doc.docstatus === 1 && frm.doc.pay_via_salary_slip === 0 && frm.doc.status === "Unpaid") { + frm.add_custom_button(__("Make Payment Entry"), function() { + frm.trigger('make_payment_entry'); + }); + } + }, + onload: function(frm){ + frm.set_query('salary_component', function() { + return { + filters: { + type: "Earning" + } + }; + }); + }, + 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']); + }); + } + }, + make_payment_entry: function(frm){ + console.log("Hello"); + } + +}); diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json new file mode 100644 index 00000000000..8e7bb8616b9 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -0,0 +1,192 @@ +{ + "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", + "expense_account", + "mode_of_payment", + "column_break_15", + "current_work_experience", + "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": "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" + } + ], + "is_submittable": 1, + "links": [], + "modified": "2020-08-06 15:51:16.047698", + "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": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC" +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py new file mode 100644 index 00000000000..fe31f4d7a61 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -0,0 +1,113 @@ +# -*- 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.model.document import Document + +from dateutil.relativedelta import relativedelta + +class Gratuity(Document): + def validate(self): + calculate_work_experience_and_amount(self.employee, self.gratuity_rule) + + def on_submit(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() + self.status = "Paid" + else: + self.status = "Unpaid" + +@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): + 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(employee))) + + time_difference = relativedelta(relieving_date, date_of_joining) + method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function") + + current_work_experience = time_difference.years + + if method == "Round off Work Experience": + if time_difference.months >= 6 and time_difference.days > 0: + current_work_experience += 1 + + return current_work_experience + +def calculate_gratuity_amount(employee, gratuity_rule, experience): + applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"]) + applicable_earnings_component = [component.salary_component for component in applicable_earnings_component] + + slabs = get_gratuity_rule_slabs(gratuity_rule) + + total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule) + + + fraction_to_be_paid = 0 + + for slab in slabs: + if experience > slab.get("from", 0) and (slab.to == 0 or experience < slab.to): + fraction_to_be_paid = slab.fraction_of_applicable_earnings + if fraction_to_be_paid: + break + + gratuity_amount = total_applicable_components_amount * experience * fraction_to_be_paid + + return gratuity_amount + +def 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") + if calculate_gratuity_amount_based_on == "Last Month Salary": + 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 + elif calculate_gratuity_amount_based_on == "Actual Salary": + pass + + return total_applicable_components_amount + +def get_gratuity_rule_slabs(gratuity_rule): + return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"]) + +def get_salary_structure(employee): + return frappe.get_list("Salary Structure Assignment", filters = {"employee": employee, 'docstatus': 1}, fields=["from_date", "salary_structure"], order_by = "from_date desc")[0].salary_structure + +def get_last_salary_slip(employee): + return frappe.get_list("Salary Slip", filters = {"employee": employee, 'docstatus': 1}, order_by = "start_date desc")[0].name + + + + diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py new file mode 100644 index 00000000000..92c1248b73f --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestGratuity(unittest.TestCase): + pass diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py b/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json new file mode 100644 index 00000000000..eea0e852b17 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json @@ -0,0 +1,32 @@ +{ + "actions": [], + "creation": "2020-08-05 19:00:28.097265", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "salary_component" + ], + "fields": [ + { + "fieldname": "salary_component", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Salary Component ", + "options": "Salary Component", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-05 20:17:13.855035", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Gratuity Applicable Component", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py new file mode 100644 index 00000000000..23e4340b04f --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class GratuityApplicableComponent(Document): + pass diff --git a/erpnext/payroll/doctype/gratuity_rule/__init__.py b/erpnext/payroll/doctype/gratuity_rule/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js new file mode 100644 index 00000000000..929370fb171 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -0,0 +1,8 @@ +// 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) { + + // } +}); diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json new file mode 100644 index 00000000000..b5de28173ab --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -0,0 +1,88 @@ +{ + "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", + "column_break_3", + "disable", + "calculate_gratuity_amount_based_on", + "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": "Last Month Salary\nActual Salary", + "reqd": 1 + }, + { + "description": "Salary components should be part of the Salary Structure.", + "fieldname": "applicable_earnings_component", + "fieldtype": "Table MultiSelect", + "label": "Applicable Earnings Component", + "options": "Gratuity Applicable Component", + "reqd": 1 + }, + { + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, + { + "fieldname": "gratuity_rules_section", + "fieldtype": "Section Break", + "label": "Gratuity Rules" + }, + { + "description": "Leave From and To blank 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" + } + ], + "links": [], + "modified": "2020-08-06 12:28:13.757792", + "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": "All", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py new file mode 100644 index 00000000000..10b2a87b976 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class GratuityRule(Document): + pass diff --git a/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py new file mode 100644 index 00000000000..1f5dc4e571e --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestGratuityRule(unittest.TestCase): + pass diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py b/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json new file mode 100644 index 00000000000..615829f45ae --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json @@ -0,0 +1,45 @@ +{ + "actions": [], + "creation": "2020-08-05 19:12:49.423500", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from", + "to", + "fraction_of_applicable_earnings" + ], + "fields": [ + { + "fieldname": "from", + "fieldtype": "Int", + "in_list_view": 1, + "label": "From(Year)" + }, + { + "fieldname": "to", + "fieldtype": "Int", + "in_list_view": 1, + "label": "To(Year)" + }, + { + "fieldname": "fraction_of_applicable_earnings", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Fraction of Applicable Earnings ", + "reqd": 1 + } + ], + "istable": 1, + "links": [], + "modified": "2020-08-05 20:03:25.955448", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Gratuity Rule Slab", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py new file mode 100644 index 00000000000..fa468e77beb --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +# import frappe +from frappe.model.document import Document + +class GratuityRuleSlab(Document): + pass From 49b326fc08dbf44b87e8c97fe6b9582ab6e34449 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 14 Aug 2020 12:53:06 +0530 Subject: [PATCH 02/64] feat(HR): Gratuity Payment --- .../doctype/payment_entry/payment_entry.py | 19 ++- erpnext/payroll/doctype/gratuity/gratuity.js | 24 +++- .../payroll/doctype/gratuity/gratuity.json | 13 +- erpnext/payroll/doctype/gratuity/gratuity.py | 116 +++++++++++++----- .../doctype/gratuity_rule/gratuity_rule.json | 10 +- 5 files changed, 135 insertions(+), 47 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 31a4c8a3879..df49667ed2d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -242,7 +242,7 @@ class PaymentEntry(AccountsController): elif self.party_type == "Supplier": valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Employee": - valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") + valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity") elif self.party_type == "Shareholder": valid_reference_doctypes = ("Journal Entry") @@ -604,7 +604,7 @@ class PaymentEntry(AccountsController): if self.payment_type in ("Receive", "Pay") and self.party: for d in self.get("references"): if d.allocated_amount \ - and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance"): + and d.reference_doctype in ("Sales Order", "Purchase Order", "Employee Advance", "Gratuity"): frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid() def update_expense_claim(self): @@ -932,6 +932,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre exchange_rate = ref_doc.get("exchange_rate") if party_account_currency != ref_doc.currency: total_amount = flt(total_amount) * flt(exchange_rate) + elif ref_doc.doctype == "Gratuity": + total_amount = ref_doc.amount if not total_amount: if party_account_currency == company_currency: total_amount = ref_doc.base_grand_total @@ -955,6 +957,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre outstanding_amount = flt(outstanding_amount) * flt(exchange_rate) if party_account_currency == company_currency: exchange_rate = 1 + elif reference_doctype == "Gratuity": + outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount) else: outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid) else: @@ -996,7 +1000,7 @@ def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_curre total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) elif ref_doc.doctype == "Employee Advance": total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc) - + if not total_amount: total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( party_account_currency, company_currency, ref_doc) @@ -1032,7 +1036,7 @@ def get_total_amount_exchange_rate_base_on_currency(party_account_currency, comp def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency): outstanding_amount, bill_no = None - if reference_doctype in ("Sales Invoice", "Purchase Invoice"): +if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") bill_no = ref_doc.get("bill_no") elif reference_doctype == "Expense Claim": @@ -1160,7 +1164,7 @@ def set_party_type(dt): party_type = "Customer" elif dt in ("Purchase Invoice", "Purchase Order"): party_type = "Supplier" - elif dt in ("Expense Claim", "Employee Advance"): + elif dt in ("Expense Claim", "Employee Advance", "Gratuity"): party_type = "Employee" elif dt in ("Fees"): party_type = "Student" @@ -1177,6 +1181,8 @@ def set_party_account(dt, dn, doc, party_type): party_account = doc.advance_account elif dt == "Expense Claim": party_account = doc.payable_account + elif dt == "Gratuity": + party_account = doc.expense_account else: party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) return party_account @@ -1222,6 +1228,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total + elif dt == "Gratuity": + grand_total = doc.amount + outstanding_amount = flt(doc.amount) - flt(doc.paid_amount) else: if party_account_currency == doc.company_currency: grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js index cbf5119061f..d6e93af5244 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.js +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -5,7 +5,17 @@ frappe.ui.form.on('Gratuity', { refresh: function(frm){ if(frm.doc.docstatus === 1 && frm.doc.pay_via_salary_slip === 0 && frm.doc.status === "Unpaid") { frm.add_custom_button(__("Make Payment Entry"), function() { - frm.trigger('make_payment_entry'); + return frappe.call({ + method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry', + args: { + "dt": cur_frm.doc.doctype, + "dn": cur_frm.doc.name + }, + callback: function(r) { + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); + } + }); }); } }, @@ -17,6 +27,15 @@ frappe.ui.form.on('Gratuity', { } }; }); + frm.set_query("expense_account", function() { + return { + filters: { + "root_type": "Asset", + "is_group": 0, + "company": frm.doc.company + } + }; + }); }, employee: function(frm) { frm.events.calculate_work_experience_and_amount(frm); @@ -38,9 +57,6 @@ frappe.ui.form.on('Gratuity', { frm.set_value("amount", r.message['amount']); }); } - }, - make_payment_entry: function(frm){ - console.log("Hello"); } }); diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index 8e7bb8616b9..b8122dfb894 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -24,6 +24,7 @@ "column_break_15", "current_work_experience", "amount", + "paid_amount", "amended_from" ], "fields": [ @@ -77,7 +78,7 @@ "default": "0", "fieldname": "amount", "fieldtype": "Currency", - "label": "Amount", + "label": "Total Amount", "read_only": 1, "reqd": 1 }, @@ -164,11 +165,19 @@ "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 } ], "is_submittable": 1, "links": [], - "modified": "2020-08-06 15:51:16.047698", + "modified": "2020-08-14 11:59:15.499548", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index fe31f4d7a61..23cc16b9947 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -6,13 +6,19 @@ from __future__ import unicode_literals import frappe from frappe import _, bold from frappe.model.document import Document +from frappe.utils import flt +from math import floor -from dateutil.relativedelta import relativedelta - +from frappe.utils import get_datetime class Gratuity(Document): def validate(self): calculate_work_experience_and_amount(self.employee, self.gratuity_rule) + def before_submit(self): + self.status = "Unpaid" + if self.pay_via_salary_slip: + self.status = "Paid" + def on_submit(self): if self.pay_via_salary_slip: additional_salary = frappe.new_doc('Additional Salary') @@ -25,9 +31,27 @@ class Gratuity(Document): additional_salary.ref_doctype = self.doctype additional_salary.ref_docname = self.name additional_salary.submit() - self.status = "Paid" - else: - self.status = "Unpaid" + + + 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"), + EmployeeAdvanceOverPayment) + + + 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): @@ -37,18 +61,29 @@ def calculate_work_experience_and_amount(employee, gratuity_rule): return {'current_work_experience': current_work_experience, "amount": gratuity_amount} def calculate_work_experience(employee, gratuity_rule): + + total_working_days_per_year = frappe.db.get_value("Gratuity Rule", gratuity_rule, "total_working_days_per_year") + 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(employee))) - time_difference = relativedelta(relieving_date, date_of_joining) + # time_difference = relativedelta(relieving_date, date_of_joining) method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function") - current_work_experience = time_difference.years + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days + + # current_work_experience = time_difference.years + + current_work_experience = employee_total_workings_days/total_working_days_per_year or 1 + + print("--->", current_work_experience) if method == "Round off Work Experience": - if time_difference.months >= 6 and time_difference.days > 0: - current_work_experience += 1 + current_work_experience = round(current_work_experience) + else: + current_work_experience = floor(current_work_experience) + return current_work_experience @@ -61,41 +96,54 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule) - fraction_to_be_paid = 0 + calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on") + + gratuity_amount = 0 + fraction_to_be_paid = 0 + year_left = experience for slab in slabs: - if experience > slab.get("from", 0) and (slab.to == 0 or experience < slab.to): - fraction_to_be_paid = slab.fraction_of_applicable_earnings - if fraction_to_be_paid: + if calculate_gratuity_amount_based_on == "Single Slab": + if experience >= slab.get("from", 0) and (slab.to == 0 or experience <= slab.to): + gratuity_amount = total_applicable_components_amount * experience * slab.fraction_of_applicable_earnings + if slab.fraction_of_applicable_earnings: + break + elif calculate_gratuity_amount_based_on == "Sum of all previous slabs": + if slab.get("to") == 0 and slab.get("from") == 0: + gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings break - gratuity_amount = total_applicable_components_amount * experience * fraction_to_be_paid + if experience > slab.get("to") and experience > slab.get("from"): + gratuity_amount += (slab.get("to") - slab.get("from")) * total_applicable_components_amount * slab.fraction_of_applicable_earnings + year_left -= (slab.get("to") - slab.get("from")) + print(experience, year_left) + elif slab.get("from") < experience < slab.get("to"): + print(year_left) + gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + + return gratuity_amount def 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") - if calculate_gratuity_amount_based_on == "Last Month Salary": - sal_slip = get_last_salary_slip(employee) + sal_slip = get_last_salary_slip(employee) - if not sal_slip: - frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(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 - elif calculate_gratuity_amount_based_on == "Actual Salary": - pass + 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 diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json index b5de28173ab..40906fa6e43 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -8,6 +8,7 @@ "field_order": [ "applicable_earnings_component", "work_experience_calculation_function", + "total_working_days_per_year", "column_break_3", "disable", "calculate_gratuity_amount_based_on", @@ -26,7 +27,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Calculate Gratuity Amount Based on", - "options": "Last Month Salary\nActual Salary", + "options": "Single Slab\nSum of all previous slabs", "reqd": 1 }, { @@ -60,10 +61,15 @@ "fieldtype": "Select", "label": "Work Experience Calculation method", "options": "Round off Work Experience\nTake Exact Completed Years" + }, + { + "fieldname": "total_working_days_per_year", + "fieldtype": "Int", + "label": "Total Working Days per year" } ], "links": [], - "modified": "2020-08-06 12:28:13.757792", + "modified": "2020-08-13 16:21:10.466739", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule", From dedc0015c0094653bb6335fad41f420cb5ebc7e9 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 14 Aug 2020 14:30:31 +0530 Subject: [PATCH 03/64] feat(HR): Working day calulation based on attendance or Leave --- erpnext/payroll/doctype/gratuity/gratuity.py | 38 ++++++++++++++----- .../doctype/gratuity_rule/gratuity_rule.json | 6 +-- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 23cc16b9947..dc2e773c0fe 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _, bold from frappe.model.document import Document -from frappe.utils import flt +from frappe.utils import flt, get_datetime from math import floor from frappe.utils import get_datetime @@ -68,17 +68,22 @@ def calculate_work_experience(employee, gratuity_rule): if not relieving_date: frappe.throw(_("Please set Relieving Date for employee: {0}").format(bold(employee))) - # time_difference = relativedelta(relieving_date, date_of_joining) method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function") 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 + # current_work_experience = time_difference.years current_work_experience = employee_total_workings_days/total_working_days_per_year or 1 - print("--->", current_work_experience) - if method == "Round off Work Experience": current_work_experience = round(current_work_experience) else: @@ -87,6 +92,24 @@ def calculate_work_experience(employee, gratuity_rule): 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"], debug = 1) + return record[0].total_lwp if len(record) else 0 + def calculate_gratuity_amount(employee, gratuity_rule, experience): applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"]) applicable_earnings_component = [component.salary_component for component in applicable_earnings_component] @@ -95,15 +118,13 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): 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 fraction_to_be_paid = 0 year_left = experience for slab in slabs: - if calculate_gratuity_amount_based_on == "Single Slab": + if calculate_gratuity_amount_based_on == "Current Slab": if experience >= slab.get("from", 0) and (slab.to == 0 or experience <= slab.to): gratuity_amount = total_applicable_components_amount * experience * slab.fraction_of_applicable_earnings if slab.fraction_of_applicable_earnings: @@ -116,13 +137,10 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): if experience > slab.get("to") and experience > slab.get("from"): gratuity_amount += (slab.get("to") - slab.get("from")) * total_applicable_components_amount * slab.fraction_of_applicable_earnings year_left -= (slab.get("to") - slab.get("from")) - print(experience, year_left) elif slab.get("from") < experience < slab.get("to"): - print(year_left) gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings - return gratuity_amount def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule): diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json index 40906fa6e43..0df274d157f 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -27,7 +27,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Calculate Gratuity Amount Based on", - "options": "Single Slab\nSum of all previous slabs", + "options": "Current slab\nSum of all previous slabs", "reqd": 1 }, { @@ -65,11 +65,11 @@ { "fieldname": "total_working_days_per_year", "fieldtype": "Int", - "label": "Total Working Days per year" + "label": "Total Working Days Per Year" } ], "links": [], - "modified": "2020-08-13 16:21:10.466739", + "modified": "2020-08-14 14:17:36.599008", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule", From 30299c6f499ff810a99160f8ddb7e29f67b1e473 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 14 Aug 2020 16:44:22 +0530 Subject: [PATCH 04/64] feat: validating and ordeing the rule slabr --- .../doctype/gratuity_rule/gratuity_rule.js | 33 +++++++++++++++++ .../doctype/gratuity_rule/gratuity_rule.py | 14 ++++++-- .../gratuity_rule_slab.json | 35 +++++++++++-------- 3 files changed, 65 insertions(+), 17 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js index 929370fb171..feaf6a8e18b 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -6,3 +6,36 @@ frappe.ui.form.on('Gratuity Rule', { // } }); + +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 + + */ + + + 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){ + frappe.throw(__("To(Year) year can not be less than From(year) ")); + } + } +}); + diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py index 10b2a87b976..71adbe5b310 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -3,8 +3,18 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document +from frappe import _ class GratuityRule(Document): - pass + + def validate(self): + for current_slab in self.gratuity_rule_slabs: + if current_slab.from_year > current_slab.to_year: + frappe(_("Row {0}: From (Year) can not be greater than To (Year)").format(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.")) + + diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json index 615829f45ae..dd642f4cd05 100644 --- a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json +++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json @@ -5,34 +5,39 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "from", - "to", + "from_year", + "to_year", "fraction_of_applicable_earnings" ], "fields": [ - { - "fieldname": "from", - "fieldtype": "Int", - "in_list_view": 1, - "label": "From(Year)" - }, - { - "fieldname": "to", - "fieldtype": "Int", - "in_list_view": 1, - "label": "To(Year)" - }, { "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-05 20:03:25.955448", + "modified": "2020-08-14 15:23:12.041375", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule Slab", From 6f1538026aa36550692b9427bbec785c57f7c41a Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 14 Aug 2020 16:45:34 +0530 Subject: [PATCH 05/64] feat: added minimum year for gratuity and condition utilisation --- erpnext/payroll/doctype/gratuity/gratuity.py | 17 +++++++++-------- .../doctype/gratuity_rule/gratuity_rule.json | 11 +++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index dc2e773c0fe..4439d81b8c9 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -32,7 +32,6 @@ class Gratuity(Document): 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 @@ -62,7 +61,7 @@ def calculate_work_experience_and_amount(employee, gratuity_rule): def calculate_work_experience(employee, gratuity_rule): - total_working_days_per_year = frappe.db.get_value("Gratuity Rule", gratuity_rule, "total_working_days_per_year") + 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: @@ -89,6 +88,8 @@ def calculate_work_experience(employee, gratuity_rule): 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 @@ -125,19 +126,19 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): year_left = experience for slab in slabs: if calculate_gratuity_amount_based_on == "Current Slab": - if experience >= slab.get("from", 0) and (slab.to == 0 or experience <= slab.to): + if experience >= slab.from_year and (slab.to_year == 0 or experience < slab.to_year): gratuity_amount = total_applicable_components_amount * experience * slab.fraction_of_applicable_earnings if slab.fraction_of_applicable_earnings: break elif calculate_gratuity_amount_based_on == "Sum of all previous slabs": - if slab.get("to") == 0 and slab.get("from") == 0: + if slab.to_year == 0 and slab.from_year == 0: gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings break - if experience > slab.get("to") and experience > slab.get("from"): - gratuity_amount += (slab.get("to") - slab.get("from")) * total_applicable_components_amount * slab.fraction_of_applicable_earnings - year_left -= (slab.get("to") - slab.get("from")) - elif slab.get("from") < experience < slab.get("to"): + if experience > slab.to_year and experience > slab.from_year: + 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) + elif slab.from_year <= experience < slab.to_year: gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json index 0df274d157f..41d5a976413 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -12,6 +12,7 @@ "column_break_3", "disable", "calculate_gratuity_amount_based_on", + "minimum_year_for_gratuity", "gratuity_rules_section", "gratuity_rule_slabs" ], @@ -48,7 +49,7 @@ "label": "Gratuity Rules" }, { - "description": "Leave From and To blank for no upper and lower limit.", + "description": "Leave From and To 0 for no upper and lower limit.", "fieldname": "gratuity_rule_slabs", "fieldtype": "Table", "label": "Current Work Experience", @@ -63,13 +64,19 @@ "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" } ], "links": [], - "modified": "2020-08-14 14:17:36.599008", + "modified": "2020-08-14 16:23:05.287545", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule", From 47f3a3a5bd5e0378dc98aef4acc8c2a4ebf2c079 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 14 Aug 2020 17:02:41 +0530 Subject: [PATCH 06/64] feat: validation for if no slab found --- erpnext/payroll/doctype/gratuity/gratuity.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 4439d81b8c9..6b624140e29 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -122,24 +122,31 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on") gratuity_amount = 0 - fraction_to_be_paid = 0 + slab_found = False year_left = experience for slab in slabs: if calculate_gratuity_amount_based_on == "Current Slab": if experience >= slab.from_year and (slab.to_year == 0 or experience < slab.to_year): gratuity_amount = total_applicable_components_amount * experience * slab.fraction_of_applicable_earnings if slab.fraction_of_applicable_earnings: + slab_found = True 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: 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 < slab.to_year: 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 From 0761301c2eee23cca45ccdc00ad477ea9948d299 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 17 Aug 2020 14:50:35 +0530 Subject: [PATCH 07/64] feat: Added all 3 standard slab for UAE --- .../regional/united_arab_emirates/setup.py | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 013ae5cf73c..690c6feedd0 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -13,6 +13,8 @@ def setup(company=None, patch=True): add_print_formats() add_custom_roles_for_reports() add_permissions() + create_standard_documents() + if company: create_sales_tax(company) @@ -153,3 +155,104 @@ def add_permissions(): add_permission(doctype, role, 0) update_permission_property(doctype, role, 0, 'write', 1) update_permission_property(doctype, role, 0, 'create', 1) +def create_standard_documents(): + + # Standard Gratuity Rules for UAE + + # Rule Under Limited Contract + rule_1 = frappe.new_doc("Gratuity Rule") + rule_1.name = "Rule Under Limited Contract" + rule_1.calculate_gratuity_amount_based_on = "Sum of all previous slabs" + rule_1.work_experience_calculation_method = "Take Exact Completed Years" + rule_1.minimum_year_for_gratuity = 1 + + rule_1.append("gratuity_rule_slabs", { + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }) + + rule_1.append("gratuity_rule_slabs", { + "from_year": 1, + "to_year":5, + "fraction_of_applicable_earnings": 21/30 + }) + + rule_1.append("gratuity_rule_slabs", { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": 1 + }) + + # Rule Under Unlimited Contract on termination + rule_2 = frappe.new_doc("Gratuity Rule") + rule_2.name = "Rule Under Unlimited Contract on termination" + rule_2.calculate_gratuity_amount_based_on = "Current Slab" + rule_2.work_experience_calculation_method = "Take Exact Completed Years" + rule_2.minimum_year_for_gratuity = 1 + + rule_2.append("gratuity_rule_slabs", { + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }) + + rule_2.append("gratuity_rule_slabs", { + "from_year": 1, + "to_year":5, + "fraction_of_applicable_earnings": 21/30 + }) + + rule_2.append("gratuity_rule_slabs", { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": 1 + }) + + # Rule Under Unlimited Contract + rule_3 = frappe.new_doc("Gratuity Rule") + rule_3.name = "Rule Under Unlimited Contract on resignation" + rule_3.calculate_gratuity_amount_based_on = "Current Slab" + rule_3.work_experience_calculation_method = "Take Exact Completed Years" + rule_3.minimum_year_for_gratuity = 1 + + rule_3.append("gratuity_rule_slabs", { + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }) + + fraction_of_applicable_earnings = 1/3 * 21/30 + rule_3.append("gratuity_rule_slabs", { + "from_year": 1, + "to_year":3, + "fraction_of_applicable_earnings": fraction_of_applicable_earnings + }) + + fraction_of_applicable_earnings = 2/3 * 21/30 + rule_3.append("gratuity_rule_slabs", { + "from_year": 3, + "to_year":5, + "fraction_of_applicable_earnings": fraction_of_applicable_earnings + }) + + fraction_of_applicable_earnings = 21/30 + rule_3.append("gratuity_rule_slabs", { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": fraction_of_applicable_earnings + }) + + + #for applicable salary component user need to set this by its own + rule_1.flags.ignore_mandatory = True + rule_2.flags.ignore_mandatory = True + rule_3.flags.ignore_mandatory = True + + rule_1.save() + rule_2.save() + rule_3.save() + + return rule_1, rule_2, rule_3 + + From 34a7250a2d9f7233e14589ae3d097629110418c5 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 17 Aug 2020 14:51:25 +0530 Subject: [PATCH 08/64] fix: Some enhancements and better validation --- erpnext/payroll/doctype/gratuity/gratuity.py | 21 ++++++++++--------- .../doctype/gratuity_rule/gratuity_rule.js | 3 ++- .../doctype/gratuity_rule/gratuity_rule.json | 4 ++-- .../doctype/gratuity_rule/gratuity_rule.py | 4 ++-- .../gratuity_rule_slab.json | 2 +- .../regional/united_arab_emirates/setup.py | 6 +++--- 6 files changed, 21 insertions(+), 19 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 6b624140e29..9f592803933 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe from frappe import _, bold from frappe.model.document import Document -from frappe.utils import flt, get_datetime +from frappe.utils import flt, get_datetime, get_link_to_form from math import floor from frappe.utils import get_datetime @@ -43,8 +43,7 @@ class Gratuity(Document): """, (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"), - EmployeeAdvanceOverPayment) + frappe.throw(_("Row {0}# Paid Amount cannot be greater than Total amount")) self.db_set("paid_amount", paid_amount) @@ -65,7 +64,7 @@ def calculate_work_experience(employee, gratuity_rule): 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(employee))) + 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") @@ -108,11 +107,13 @@ def get_non_working_days(employee, relieving_date, status): filters["leave_type"] = ("IN", lwp_leave_types) - record = frappe.get_all("Attendance", filters=filters, fields = ["COUNT(name) as total_lwp"], debug = 1) + 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 = 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] slabs = get_gratuity_rule_slabs(gratuity_rule) @@ -137,16 +138,16 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): slab_found = True break - if experience > slab.to_year and experience > slab.from_year: + 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 < slab.to_year: + 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))) + 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 @@ -174,7 +175,7 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen return total_applicable_components_amount def get_gratuity_rule_slabs(gratuity_rule): - return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"]) + 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 diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js index feaf6a8e18b..69099bb39d5 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -19,6 +19,7 @@ frappe.ui.form.on('Gratuity Rule Slab', { 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 */ @@ -33,7 +34,7 @@ frappe.ui.form.on('Gratuity Rule Slab', { to_year(frm, cdt, cdn) { let row = locals[cdt][cdn]; - if (row.to_year <= row.from_year){ + if (row.to_year <= row.from_year && row.to_year === 0){ frappe.throw(__("To(Year) year can not be less than From(year) ")); } } diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json index 41d5a976413..18053ba88e7 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -28,7 +28,7 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Calculate Gratuity Amount Based on", - "options": "Current slab\nSum of all previous slabs", + "options": "Current Slab\nSum of all previous slabs", "reqd": 1 }, { @@ -76,7 +76,7 @@ } ], "links": [], - "modified": "2020-08-14 16:23:05.287545", + "modified": "2020-08-17 14:17:02.594665", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule", diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py index 71adbe5b310..00b5752eb8b 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -11,8 +11,8 @@ class GratuityRule(Document): def validate(self): for current_slab in self.gratuity_rule_slabs: - if current_slab.from_year > current_slab.to_year: - frappe(_("Row {0}: From (Year) can not be greater than To (Year)").format(slab.idx)) + 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.")) diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json index dd642f4cd05..bc37b0f51ed 100644 --- a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json +++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json @@ -37,7 +37,7 @@ ], "istable": 1, "links": [], - "modified": "2020-08-14 15:23:12.041375", + "modified": "2020-08-17 14:09:56.781712", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule Slab", diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 690c6feedd0..f23698e19bf 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -161,7 +161,7 @@ def create_standard_documents(): # Rule Under Limited Contract rule_1 = frappe.new_doc("Gratuity Rule") - rule_1.name = "Rule Under Limited Contract" + rule_1.name = "Rule Under Limited Contract (UAE)" rule_1.calculate_gratuity_amount_based_on = "Sum of all previous slabs" rule_1.work_experience_calculation_method = "Take Exact Completed Years" rule_1.minimum_year_for_gratuity = 1 @@ -186,7 +186,7 @@ def create_standard_documents(): # Rule Under Unlimited Contract on termination rule_2 = frappe.new_doc("Gratuity Rule") - rule_2.name = "Rule Under Unlimited Contract on termination" + rule_2.name = "Rule Under Unlimited Contract on termination (UAE)" rule_2.calculate_gratuity_amount_based_on = "Current Slab" rule_2.work_experience_calculation_method = "Take Exact Completed Years" rule_2.minimum_year_for_gratuity = 1 @@ -211,7 +211,7 @@ def create_standard_documents(): # Rule Under Unlimited Contract rule_3 = frappe.new_doc("Gratuity Rule") - rule_3.name = "Rule Under Unlimited Contract on resignation" + rule_3.name = "Rule Under Unlimited Contract on resignation (UAE)" rule_3.calculate_gratuity_amount_based_on = "Current Slab" rule_3.work_experience_calculation_method = "Take Exact Completed Years" rule_3.minimum_year_for_gratuity = 1 From e1464a7bf0a986b48e39ebdafbad174a678a6258 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 17 Aug 2020 15:03:32 +0530 Subject: [PATCH 09/64] feat: Indian Standard Gratuity Rule --- erpnext/regional/india/setup.py | 23 ++++++++++++++++++- .../regional/united_arab_emirates/setup.py | 2 -- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index cbcd6e3203a..a8ff3f84849 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -21,6 +21,7 @@ def setup_company_independent_fixtures(): add_permissions() add_custom_roles_for_reports() frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) + create_standard_documents() add_print_formats() def add_hsn_sac_codes(): @@ -793,4 +794,24 @@ def get_tds_details(accounts, fiscal_year): doctype="Tax Withholding Category", accounts=accounts, rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20, "single_threshold": 2500, "cumulative_threshold": 0}]) - ] \ No newline at end of file + ] + +def create_standard_documents(): + + # Standard Indain Gratuity Rule + + rule = frappe.new_doc("Gratuity Rule") + rule.name = "Indian Standard Gratuity Rule" + rule.calculate_gratuity_amount_based_on = "Current Slab" + rule.work_experience_calculation_method = "Round Off Work Experience" + rule.minimum_year_for_gratuity = 5 + + fraction = 15/26 + rule.append("gratuity_rule_slabs", { + "from_year": 0, + "to_year":0, + "fraction_of_applicable_earnings": fraction + }) + + rule.flags.ignore_mandatory = True + rule.save() \ No newline at end of file diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index f23698e19bf..1a899272f45 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -253,6 +253,4 @@ def create_standard_documents(): rule_2.save() rule_3.save() - return rule_1, rule_2, rule_3 - From f63df91186ec45802a2f2c06a99675ec655b046c Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 17 Aug 2020 15:13:25 +0530 Subject: [PATCH 10/64] patch: to create standard Gratuity Rule for india and UAE --- erpnext/patches.txt | 1 + .../setup_gratuity_rule_for_india_and_uae.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9e33014c38e..fd9c78ab4d4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -740,3 +740,4 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy erpnext.patches.v13_0.add_po_to_global_search erpnext.patches.v13_0.update_returned_qty_in_pr_dn +erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py new file mode 100644 index 00000000000..5de355f747a --- /dev/null +++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py @@ -0,0 +1,15 @@ +# Copyright (c) 2019, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals + +import erpnext + +def execute(): + region = erpnext.get_region() + if region == "India": + from erpnext.regional.india.setup import create_standard_documents + create_standard_documents() + elif region == "United Arab Emirates": + from erpnext.regional.united_arab_emirates.setup import create_standard_documents + create_standard_documents() \ No newline at end of file From 25c356894d58b46692f4de0a34affa8008b8d707 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 18 Aug 2020 14:13:54 +0530 Subject: [PATCH 11/64] test: gratuity --- .../mode_of_payment/mode_of_payment.py | 6 +- .../setup_gratuity_rule_for_india_and_uae.py | 13 +- erpnext/payroll/doctype/gratuity/gratuity.py | 4 +- .../payroll/doctype/gratuity/test_gratuity.py | 174 +++++++++++++++++- erpnext/regional/india/setup.py | 4 +- .../regional/united_arab_emirates/setup.py | 7 +- 6 files changed, 194 insertions(+), 14 deletions(-) diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py index d54a47e3c96..32473694c80 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py @@ -12,7 +12,7 @@ class ModeofPayment(Document): self.validate_accounts() self.validate_repeating_companies() self.validate_pos_mode_of_payment() - + def validate_repeating_companies(self): """Error when Same Company is entered multiple times in accounts""" accounts_list = [] @@ -31,10 +31,10 @@ class ModeofPayment(Document): def validate_pos_mode_of_payment(self): if not self.enabled: - pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip + pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name)) pos_profiles = list(map(lambda x: x[0], pos_profiles)) - + if pos_profiles: message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \ Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode." diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py index 5de355f747a..93cadf576cb 100644 --- a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py +++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py @@ -3,13 +3,16 @@ from __future__ import unicode_literals -import erpnext +import erpnext, 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') region = erpnext.get_region() if region == "India": - from erpnext.regional.india.setup import create_standard_documents - create_standard_documents() + from erpnext.regional.india.setup import create_gratuity_rule + create_gratuity_rule() elif region == "United Arab Emirates": - from erpnext.regional.united_arab_emirates.setup import create_standard_documents - create_standard_documents() \ No newline at end of file + from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule + create_gratuity_rule() \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 9f592803933..815e24d6e4b 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -12,7 +12,9 @@ from math import floor from frappe.utils import get_datetime class Gratuity(Document): def validate(self): - calculate_work_experience_and_amount(self.employee, self.gratuity_rule) + data = calculate_work_experience_and_amount(self.employee, self.gratuity_rule) + self.current_work_experience = data["current_work_experience"] + self.amount = data["amount"] def before_submit(self): self.status = "Unpaid" diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 92c1248b73f..fb2488cf85e 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -3,8 +3,178 @@ # See license.txt from __future__ import unicode_literals -# import frappe +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 +from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip +from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule +from frappe.utils import getdate, add_days, get_datetime, flt + class TestGratuity(unittest.TestCase): - pass + + def setUp(self): + 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 = frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)") + if not rule: + create_gratuity_rule() + else: + rule = frappe.get_doc("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)") + rule.applicable_earnings_component = [] + rule.append("applicable_earnings_component", { + "salary_component": "Basic Salary" + }) + rule.save() + rule.reload() + + gra = frappe.new_doc("Gratuity") + gra.employee = employee + gra.posting_date = getdate() + gra.gratuity_rule = rule.name + gra.pay_via_salary_slip = 1 + gra.salary_component = "Performance Bonus" + gra.payroll_date = getdate() + gra.save() + gra.submit() + + #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 + + gra.reload() + + from math import floor + + self.assertEqual(floor(experience), gra.current_work_experience) + + #amount Calculation 6 + 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 + gra.reload() + + self.assertEqual(flt(gratuity_amount, 2), flt(gra.amount, 2)) + + #additional salary creation (Pay via salary slip) + self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gra.name})) + self.assertEqual(gra.status, "Paid") + + + + def test_check_gratuity_amount_based_on_all_previous_slabs(self): + employee, sal_slip = create_employee_and_get_last_salary_slip() + rule = frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)") + if not rule: + create_gratuity_rule() + else: + rule = frappe.get_doc("Gratuity Rule", rule) + rule.applicable_earnings_component = [] + rule = frappe.get_doc("Gratuity Rule", "Rule Under Limited Contract (UAE)") + rule.append("applicable_earnings_component", { + "salary_component": "Basic Salary" + }) + rule.save() + rule.reload() + + mof = frappe.get_doc("Mode of Payment", "Cheque") + mof.accounts = [] + mof.append("accounts", { + "company": "_Test Company", + "default_account": "_Test Bank - _TC" + }) + + mof.save() + + gra = frappe.new_doc("Gratuity") + gra.employee = employee + gra.posting_date = getdate() + gra.gratuity_rule = rule.name + gra.pay_via_salary_slip = 0 + gra.payroll_date = getdate() + gra.expense_account = "Payment Account - _TC" + gra.mode_of_payment = "Cheque" + + gra.save() + gra.submit() + + #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 + + gra.reload() + + from math import floor + + self.assertEqual(floor(experience), gra.current_work_experience) + + #amount Calculation 6 + 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 + gra.reload() + + self.assertEqual(flt(gratuity_amount, 2), flt(gra.amount, 2)) + self.assertEqual(gra.status, "Unpaid") + + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + pay_entry = get_payment_entry("Gratuity", gra.name) + pay_entry.reference_no = "123467" + pay_entry.reference_date = getdate() + + pay_entry.save() + pay_entry.submit() + + gra.reload() + + self.assertEqual(gra.status, "Paid") + self.assertEqual(gra.paid_amount, flt(gra.amount, 2)) + +def create_employee_and_get_last_salary_slip(): + employee = make_employee("test_employee@salary.com") + 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) + + return employee, salary_slip + diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index a8ff3f84849..9be5239d5e1 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -21,7 +21,7 @@ def setup_company_independent_fixtures(): add_permissions() add_custom_roles_for_reports() frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) - create_standard_documents() + create_gratuity_rule() add_print_formats() def add_hsn_sac_codes(): @@ -796,7 +796,7 @@ def get_tds_details(accounts, fiscal_year): "single_threshold": 2500, "cumulative_threshold": 0}]) ] -def create_standard_documents(): +def create_gratuity_rule(): # Standard Indain Gratuity Rule diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 1a899272f45..72d7c132042 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -11,9 +11,13 @@ from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax def setup(company=None, patch=True): make_custom_fields() add_print_formats() +<<<<<<< HEAD add_custom_roles_for_reports() add_permissions() create_standard_documents() +======= + create_gratuity_rule() +>>>>>>> test: gratuity if company: create_sales_tax(company) @@ -155,7 +159,8 @@ def add_permissions(): add_permission(doctype, role, 0) update_permission_property(doctype, role, 0, 'write', 1) update_permission_property(doctype, role, 0, 'create', 1) -def create_standard_documents(): + +def create_gratuity_rule(): # Standard Gratuity Rules for UAE From 493eea19e84b6596c3410fa921ab84633ae4b7db Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 18 Aug 2020 15:02:32 +0530 Subject: [PATCH 12/64] feat: added link from desk page and some minor fixes --- .../payroll/desk_page/payroll/payroll.json | 4 +- erpnext/regional/india/setup.py | 28 ++-- .../regional/united_arab_emirates/setup.py | 153 +++++++++--------- 3 files changed, 94 insertions(+), 91 deletions(-) diff --git a/erpnext/payroll/desk_page/payroll/payroll.json b/erpnext/payroll/desk_page/payroll/payroll.json index 285e3b3a135..1caf9c7b4e1 100644 --- a/erpnext/payroll/desk_page/payroll/payroll.json +++ b/erpnext/payroll/desk_page/payroll/payroll.json @@ -13,7 +13,7 @@ { "hidden": 0, "label": "Compensations", - "links": "[\n {\n \"label\": \"Additional Salary\",\n \"name\": \"Additional Salary\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Retention Bonus\",\n \"name\": \"Retention Bonus\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Incentive\",\n \"name\": \"Employee Incentive\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Application\",\n \"name\": \"Employee Benefit Application\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Claim\",\n \"name\": \"Employee Benefit Claim\",\n \"type\": \"doctype\"\n }\n]" + "links": "[\n {\n \"label\": \"Additional Salary\",\n \"name\": \"Additional Salary\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Retention Bonus\",\n \"name\": \"Retention Bonus\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Incentive\",\n \"name\": \"Employee Incentive\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Gratuity Rule\",\n \"name\": \"Gratuity Rule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Gratuity\",\n \"name\": \"Gratuity\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Application\",\n \"name\": \"Employee Benefit Application\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Claim\",\n \"name\": \"Employee Benefit Claim\",\n \"type\": \"doctype\"\n }\n]" }, { "hidden": 0, @@ -38,7 +38,7 @@ "idx": 0, "is_standard": 1, "label": "Payroll", - "modified": "2020-08-10 19:38:45.976209", + "modified": "2020-08-18 15:00:55.671767", "modified_by": "Administrator", "module": "Payroll", "name": "Payroll", diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 9be5239d5e1..d46c372fdde 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -799,19 +799,19 @@ def get_tds_details(accounts, fiscal_year): 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 - 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 + }) - fraction = 15/26 - rule.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":0, - "fraction_of_applicable_earnings": fraction - }) - - rule.flags.ignore_mandatory = True - rule.save() \ No newline at end of file + rule.flags.ignore_mandatory = True + rule.save() \ No newline at end of file diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 72d7c132042..b91318c9af7 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -165,97 +165,100 @@ def create_gratuity_rule(): # Standard Gratuity Rules for UAE # Rule Under Limited Contract - rule_1 = frappe.new_doc("Gratuity Rule") - rule_1.name = "Rule Under Limited Contract (UAE)" - rule_1.calculate_gratuity_amount_based_on = "Sum of all previous slabs" - rule_1.work_experience_calculation_method = "Take Exact Completed Years" - rule_1.minimum_year_for_gratuity = 1 + if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"): + rule_1 = frappe.new_doc("Gratuity Rule") + rule_1.name = "Rule Under Limited Contract (UAE)" + rule_1.calculate_gratuity_amount_based_on = "Sum of all previous slabs" + rule_1.work_experience_calculation_method = "Take Exact Completed Years" + rule_1.minimum_year_for_gratuity = 1 - rule_1.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }) + rule_1.append("gratuity_rule_slabs", { + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }) - rule_1.append("gratuity_rule_slabs", { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }) + rule_1.append("gratuity_rule_slabs", { + "from_year": 1, + "to_year":5, + "fraction_of_applicable_earnings": 21/30 + }) - rule_1.append("gratuity_rule_slabs", { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }) + rule_1.append("gratuity_rule_slabs", { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": 1 + }) # Rule Under Unlimited Contract on termination - rule_2 = frappe.new_doc("Gratuity Rule") - rule_2.name = "Rule Under Unlimited Contract on termination (UAE)" - rule_2.calculate_gratuity_amount_based_on = "Current Slab" - rule_2.work_experience_calculation_method = "Take Exact Completed Years" - rule_2.minimum_year_for_gratuity = 1 + if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)"): + rule_2 = frappe.new_doc("Gratuity Rule") + rule_2.name = "Rule Under Unlimited Contract on termination (UAE)" + rule_2.calculate_gratuity_amount_based_on = "Current Slab" + rule_2.work_experience_calculation_method = "Take Exact Completed Years" + rule_2.minimum_year_for_gratuity = 1 - rule_2.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }) + rule_2.append("gratuity_rule_slabs", { + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }) - rule_2.append("gratuity_rule_slabs", { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }) + rule_2.append("gratuity_rule_slabs", { + "from_year": 1, + "to_year":5, + "fraction_of_applicable_earnings": 21/30 + }) - rule_2.append("gratuity_rule_slabs", { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }) + rule_2.append("gratuity_rule_slabs", { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": 1 + }) # Rule Under Unlimited Contract - rule_3 = frappe.new_doc("Gratuity Rule") - rule_3.name = "Rule Under Unlimited Contract on resignation (UAE)" - rule_3.calculate_gratuity_amount_based_on = "Current Slab" - rule_3.work_experience_calculation_method = "Take Exact Completed Years" - rule_3.minimum_year_for_gratuity = 1 + if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"): + rule_3 = frappe.new_doc("Gratuity Rule") + rule_3.name = "Rule Under Unlimited Contract on resignation (UAE)" + rule_3.calculate_gratuity_amount_based_on = "Current Slab" + rule_3.work_experience_calculation_method = "Take Exact Completed Years" + rule_3.minimum_year_for_gratuity = 1 - rule_3.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }) + rule_3.append("gratuity_rule_slabs", { + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }) - fraction_of_applicable_earnings = 1/3 * 21/30 - rule_3.append("gratuity_rule_slabs", { - "from_year": 1, - "to_year":3, - "fraction_of_applicable_earnings": fraction_of_applicable_earnings - }) + fraction_of_applicable_earnings = 1/3 * 21/30 + rule_3.append("gratuity_rule_slabs", { + "from_year": 1, + "to_year":3, + "fraction_of_applicable_earnings": fraction_of_applicable_earnings + }) - fraction_of_applicable_earnings = 2/3 * 21/30 - rule_3.append("gratuity_rule_slabs", { - "from_year": 3, - "to_year":5, - "fraction_of_applicable_earnings": fraction_of_applicable_earnings - }) + fraction_of_applicable_earnings = 2/3 * 21/30 + rule_3.append("gratuity_rule_slabs", { + "from_year": 3, + "to_year":5, + "fraction_of_applicable_earnings": fraction_of_applicable_earnings + }) - fraction_of_applicable_earnings = 21/30 - rule_3.append("gratuity_rule_slabs", { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": fraction_of_applicable_earnings - }) + fraction_of_applicable_earnings = 21/30 + rule_3.append("gratuity_rule_slabs", { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": fraction_of_applicable_earnings + }) - #for applicable salary component user need to set this by its own - rule_1.flags.ignore_mandatory = True - rule_2.flags.ignore_mandatory = True - rule_3.flags.ignore_mandatory = True + #for applicable salary component user need to set this by its own + rule_1.flags.ignore_mandatory = True + rule_2.flags.ignore_mandatory = True + rule_3.flags.ignore_mandatory = True - rule_1.save() - rule_2.save() - rule_3.save() + rule_1.save() + rule_2.save() + rule_3.save() From 4d6c3c9449541fc2983b19b9c88426b43c240a33 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 27 Oct 2020 14:42:55 +0530 Subject: [PATCH 13/64] Fix: Changes Requested, Sider, codacy, Transalation --- .../setup_gratuity_rule_for_india_and_uae.py | 5 +- erpnext/payroll/doctype/gratuity/gratuity.js | 38 +++++----- .../payroll/doctype/gratuity/gratuity.json | 17 ++++- erpnext/payroll/doctype/gratuity/gratuity.py | 17 ++++- .../payroll/doctype/gratuity/test_gratuity.py | 76 +++++++++---------- .../doctype/gratuity_rule/gratuity_rule.js | 2 +- .../doctype/gratuity_rule/gratuity_rule.json | 17 ++++- 7 files changed, 103 insertions(+), 69 deletions(-) diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py index 93cadf576cb..2dd064ebcac 100644 --- a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py +++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py @@ -9,10 +9,9 @@ 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') - region = erpnext.get_region() - if region == "India": + if frappe.db.exists("company", {"country": "India"}): from erpnext.regional.india.setup import create_gratuity_rule create_gratuity_rule() - elif region == "United Arab Emirates": + if frappe.db.exists("company", {"country": "United Arab Emirates"}): from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule create_gratuity_rule() \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js index d6e93af5244..dfdf08bdea0 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.js +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -2,24 +2,7 @@ // For license information, please see license.txt frappe.ui.form.on('Gratuity', { - refresh: function(frm){ - if(frm.doc.docstatus === 1 && frm.doc.pay_via_salary_slip === 0 && frm.doc.status === "Unpaid") { - frm.add_custom_button(__("Make Payment Entry"), function() { - return frappe.call({ - method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry', - args: { - "dt": cur_frm.doc.doctype, - "dn": cur_frm.doc.name - }, - callback: function(r) { - var doclist = frappe.model.sync(r.message); - frappe.set_route("Form", doclist[0].doctype, doclist[0].name); - } - }); - }); - } - }, - onload: function(frm){ + setup: function(frm){ frm.set_query('salary_component', function() { return { filters: { @@ -30,13 +13,30 @@ frappe.ui.form.on('Gratuity', { frm.set_query("expense_account", function() { return { filters: { - "root_type": "Asset", + "root_type": "Expense", "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); }, diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index b8122dfb894..b81ae588eaa 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -175,9 +175,10 @@ "read_only": 1 } ], + "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-08-14 11:59:15.499548", + "modified": "2020-10-27 14:04:41.886934", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", @@ -191,7 +192,19 @@ "print": 1, "read": 1, "report": 1, - "role": "System Manager", + "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 } diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 815e24d6e4b..e6c519a482f 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -9,7 +9,6 @@ from frappe.model.document import Document from frappe.utils import flt, get_datetime, get_link_to_form from math import floor -from frappe.utils import get_datetime class Gratuity(Document): def validate(self): data = calculate_work_experience_and_amount(self.employee, self.gratuity_rule) @@ -22,6 +21,9 @@ class Gratuity(Document): self.status = "Paid" def on_submit(self): + create_additional_salary() + + def create_additional_salary(self): if self.pay_via_salary_slip: additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = self.employee @@ -170,7 +172,7 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen 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") + frappe.throw(_("No Applicable Component is present in last month salary slip")) for data in component_and_amounts: total_applicable_components_amount += data.amount @@ -180,10 +182,17 @@ 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 + 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 + return frappe.get_list("Salary Slip", filters = { + "employee": employee, 'docstatus': 1 + }, + order_by = "start_date desc")[0].name diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index fb2488cf85e..680ecbcfc1b 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -13,12 +13,10 @@ from frappe.utils import getdate, add_days, get_datetime, flt class TestGratuity(unittest.TestCase): - def setUp(self): 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 = frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)") @@ -33,15 +31,15 @@ class TestGratuity(unittest.TestCase): rule.save() rule.reload() - gra = frappe.new_doc("Gratuity") - gra.employee = employee - gra.posting_date = getdate() - gra.gratuity_rule = rule.name - gra.pay_via_salary_slip = 1 - gra.salary_component = "Performance Bonus" - gra.payroll_date = getdate() - gra.save() - gra.submit() + gratuity = frappe.new_doc("Gratuity") + gratuity.employee = employee + gratuity.posting_date = getdate() + gratuity.gratuity_rule = rule.name + gratuity.pay_via_salary_slip = 1 + gratuity.salary_component = "Performance Bonus" + gratuity.payroll_date = getdate() + gratuity.save() + gratuity.submit() #work experience calculation date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) @@ -49,11 +47,11 @@ class TestGratuity(unittest.TestCase): experience = employee_total_workings_days/rule.total_working_days_per_year - gra.reload() + gratuity.reload() from math import floor - self.assertEqual(floor(experience), gra.current_work_experience) + self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation 6 component_amount = frappe.get_list("Salary Detail", @@ -68,15 +66,13 @@ class TestGratuity(unittest.TestCase): ''' 5 - 0 fraction is 1 ''' gratuity_amount = component_amount[0].amount * experience - gra.reload() + gratuity.reload() - self.assertEqual(flt(gratuity_amount, 2), flt(gra.amount, 2)) + 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": gra.name})) - self.assertEqual(gra.status, "Paid") - - + self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) + self.assertEqual(gratuity.status, "Paid") def test_check_gratuity_amount_based_on_all_previous_slabs(self): employee, sal_slip = create_employee_and_get_last_salary_slip() @@ -102,17 +98,17 @@ class TestGratuity(unittest.TestCase): mof.save() - gra = frappe.new_doc("Gratuity") - gra.employee = employee - gra.posting_date = getdate() - gra.gratuity_rule = rule.name - gra.pay_via_salary_slip = 0 - gra.payroll_date = getdate() - gra.expense_account = "Payment Account - _TC" - gra.mode_of_payment = "Cheque" + gratuity = frappe.new_doc("Gratuity") + gratuity.employee = employee + gratuity.posting_date = getdate() + gratuity.gratuity_rule = rule.name + gratuity.pay_via_salary_slip = 0 + gratuity.payroll_date = getdate() + gratuity.expense_account = "Payment Account - _TC" + gratuity.mode_of_payment = "Cheque" - gra.save() - gra.submit() + gratuity.save() + gratuity.submit() #work experience calculation date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) @@ -120,11 +116,11 @@ class TestGratuity(unittest.TestCase): experience = employee_total_workings_days/rule.total_working_days_per_year - gra.reload() + gratuity.reload() from math import floor - self.assertEqual(floor(experience), gra.current_work_experience) + self.assertEqual(floor(experience), gratuity.current_work_experience) #amount Calculation 6 component_amount = frappe.get_list("Salary Detail", @@ -145,25 +141,29 @@ class TestGratuity(unittest.TestCase): gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount - gra.reload() + gratuity.reload() - self.assertEqual(flt(gratuity_amount, 2), flt(gra.amount, 2)) - self.assertEqual(gra.status, "Unpaid") + 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", gra.name) + pay_entry = get_payment_entry("Gratuity", gratuity.name) pay_entry.reference_no = "123467" pay_entry.reference_date = getdate() pay_entry.save() pay_entry.submit() - gra.reload() + gratuity.reload() - self.assertEqual(gra.status, "Paid") - self.assertEqual(gra.paid_amount, flt(gra.amount, 2)) + self.assertEqual(gratuity.status, "Paid") + self.assertEqual(gratuity.paid_amount, 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 create_employee_and_get_last_salary_slip(): employee = make_employee("test_employee@salary.com") diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js index 69099bb39d5..9e9f3e204bc 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -25,7 +25,7 @@ frappe.ui.form.on('Gratuity Rule Slab', { gratuity_rule_slabs_add(frm, cdt, cdn) { let row = locals[cdt][cdn]; - let array_idx = row.idx - 1 + 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(); diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json index 18053ba88e7..7d24e41f485 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -75,8 +75,9 @@ "label": "Minimum Year for Gratuity" } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-08-17 14:17:02.594665", + "modified": "2020-10-27 14:04:31.617621", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule", @@ -90,7 +91,19 @@ "print": 1, "read": 1, "report": 1, - "role": "All", + "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 } From 46e1c09b4558079f36bebf4a96cce3b077391f22 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 27 Oct 2020 15:44:43 +0530 Subject: [PATCH 14/64] style: Broken into smaller function --- .../setup_gratuity_rule_for_india_and_uae.py | 3 +- erpnext/payroll/doctype/gratuity/gratuity.py | 5 +- .../doctype/gratuity_rule/gratuity_rule.js | 1 - .../doctype/gratuity_rule/gratuity_rule.py | 13 ++ .../regional/united_arab_emirates/setup.py | 160 ++++++++---------- 5 files changed, 89 insertions(+), 93 deletions(-) diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py index 2dd064ebcac..a71f33c233e 100644 --- a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py +++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py @@ -2,8 +2,7 @@ # License: GNU General Public License v3. See license.txt from __future__ import unicode_literals - -import erpnext, frappe +import frappe def execute(): frappe.reload_doc('payroll', 'doctype', 'gratuity_rule') diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index e6c519a482f..0693583c88a 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -21,7 +21,7 @@ class Gratuity(Document): self.status = "Paid" def on_submit(self): - create_additional_salary() + self.create_additional_salary() def create_additional_salary(self): if self.pay_via_salary_slip: @@ -194,6 +194,3 @@ def get_last_salary_slip(employee): }, order_by = "start_date desc")[0].name - - - diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js index 9e9f3e204bc..1a5347e792d 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -22,7 +22,6 @@ frappe.ui.form.on('Gratuity Rule Slab', { 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; diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py index 00b5752eb8b..29a6ebe1a6a 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -17,4 +17,17 @@ class GratuityRule(Document): if current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1: frappe.throw(_("You can not define multiple slabs if you have a slab with no lower and upper limits.")) +def get_gratuity_rule(name, slabs, **args): + args = frappe._dict(args) + rule = frappe.new_doc("Gratuity Rule") + rule.name = name + rule.calculate_gratuity_amount_based_on = args.calculate_gratuity_amount_based_on or "Current Slab" + rule.work_experience_calculation_method = args.work_experience_calculation_method or "Take Exact Completed Years" + rule.minimum_year_for_gratuity = 1 + + + for slab in slabs: + slab = frappe._dict(slab) + rule.append("gratuity_rule_slabs", slab) + return rule diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index b91318c9af7..2a45c225906 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -7,6 +7,7 @@ import frappe, os, json from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.permissions import add_permission, update_permission_property from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax +from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule def setup(company=None, patch=True): make_custom_fields() @@ -161,104 +162,91 @@ def add_permissions(): update_permission_property(doctype, role, 0, 'create', 1) def create_gratuity_rule(): - - # Standard Gratuity Rules for UAE + rule_1 = rule_2 = rule_3 = None # Rule Under Limited Contract + slabs = get_slab_for_limited_contract() if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"): - rule_1 = frappe.new_doc("Gratuity Rule") - rule_1.name = "Rule Under Limited Contract (UAE)" - rule_1.calculate_gratuity_amount_based_on = "Sum of all previous slabs" - rule_1.work_experience_calculation_method = "Take Exact Completed Years" - rule_1.minimum_year_for_gratuity = 1 - - rule_1.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }) - - rule_1.append("gratuity_rule_slabs", { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }) - - rule_1.append("gratuity_rule_slabs", { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }) + rule_1 = get_gratuity_rule("Rule Under Limited Contract (UAE)", slabs, calculate_gratuity_amount_based_on="Sum of all previous slabs") # Rule Under Unlimited Contract on termination + slabs = get_slab_for_unlimited_contract_on_termination() if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)"): - rule_2 = frappe.new_doc("Gratuity Rule") - rule_2.name = "Rule Under Unlimited Contract on termination (UAE)" - rule_2.calculate_gratuity_amount_based_on = "Current Slab" - rule_2.work_experience_calculation_method = "Take Exact Completed Years" - rule_2.minimum_year_for_gratuity = 1 + rule_2 = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)", slabs) - rule_2.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }) - - rule_2.append("gratuity_rule_slabs", { - "from_year": 1, - "to_year":5, - "fraction_of_applicable_earnings": 21/30 - }) - - rule_2.append("gratuity_rule_slabs", { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": 1 - }) - - # Rule Under Unlimited Contract + # Rule Under Unlimited Contract on resignation + slabs = get_slab_for_unlimited_contract_on_resignation() if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"): - rule_3 = frappe.new_doc("Gratuity Rule") - rule_3.name = "Rule Under Unlimited Contract on resignation (UAE)" - rule_3.calculate_gratuity_amount_based_on = "Current Slab" - rule_3.work_experience_calculation_method = "Take Exact Completed Years" - rule_3.minimum_year_for_gratuity = 1 + rule_3 = get_gratuity_rule("Rule Under Unlimited Contract on resignation (UAE)", slabs) - rule_3.append("gratuity_rule_slabs", { - "from_year": 0, - "to_year":1, - "fraction_of_applicable_earnings": 0 - }) - - fraction_of_applicable_earnings = 1/3 * 21/30 - rule_3.append("gratuity_rule_slabs", { - "from_year": 1, - "to_year":3, - "fraction_of_applicable_earnings": fraction_of_applicable_earnings - }) - - fraction_of_applicable_earnings = 2/3 * 21/30 - rule_3.append("gratuity_rule_slabs", { - "from_year": 3, - "to_year":5, - "fraction_of_applicable_earnings": fraction_of_applicable_earnings - }) - - fraction_of_applicable_earnings = 21/30 - rule_3.append("gratuity_rule_slabs", { - "from_year": 5, - "to_year":0, - "fraction_of_applicable_earnings": fraction_of_applicable_earnings - }) - - - #for applicable salary component user need to set this by its own + #for applicable salary component user need to set this by its own + if rule_1: rule_1.flags.ignore_mandatory = True - rule_2.flags.ignore_mandatory = True - rule_3.flags.ignore_mandatory = True - rule_1.save() + if rule_2: + rule_2.flags.ignore_mandatory = True rule_2.save() + if rule_3: + rule_3.flags.ignore_mandatory = True rule_3.save() +def get_slab_for_limited_contract(): + return [{ + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }, + { + "from_year": 1, + "to_year":5, + "fraction_of_applicable_earnings": 21/30 + }, + { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": 1 + }] + +def get_slab_for_unlimited_contract_on_termination(): + return [{ + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }, + { + "from_year": 1, + "to_year":5, + "fraction_of_applicable_earnings": 21/30 + }, + { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": 1 + }] + +def get_slab_for_unlimited_contract_on_resignation(): + fraction_1 = 1/3 * 21/30 + fraction_2 = 2/3 * 21/30 + fraction_3 = 21/30 + + return [{ + "from_year": 0, + "to_year":1, + "fraction_of_applicable_earnings": 0 + }, + { + "from_year": 1, + "to_year":3, + "fraction_of_applicable_earnings": fraction_1 + }, + { + "from_year": 3, + "to_year":5, + "fraction_of_applicable_earnings": fraction_2 + }, + { + "from_year": 5, + "to_year":0, + "fraction_of_applicable_earnings": fraction_3 + }] From 78fdd5d9b43061cd9be8da84973e8aaa30487f8e Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 28 Oct 2020 15:46:51 +0530 Subject: [PATCH 15/64] fix: update status on salary Slip submission --- erpnext/payroll/doctype/gratuity/gratuity.py | 7 ++----- .../doctype/salary_slip/salary_slip.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 0693583c88a..db353e9d71a 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -14,11 +14,8 @@ class Gratuity(Document): data = calculate_work_experience_and_amount(self.employee, self.gratuity_rule) self.current_work_experience = data["current_work_experience"] self.amount = data["amount"] - - def before_submit(self): - self.status = "Unpaid" - if self.pay_via_salary_slip: - self.status = "Paid" + if self.docstatus == 1: + self.status = "Unpaid" def on_submit(self): self.create_additional_salary() diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 20365b191d0..68147269b37 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -74,9 +74,27 @@ class SalarySlip(TransactionBase): if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: self.email_salary_slip() + self.update_payment_status_for_gratuity() + + def update_payment_status_for_gratuity(self): + add_salary = frappe.db.get_all("Additional Salary", + filters = { + "payroll_date": ("BETWEEN", [self.start_date, self.end_date]), + "employee": self.employee, + "ref_doctype": "Gratuity", + "docstatus": 1, + }, fields = ["ref_docname", "name"])[0] + + status = "Paid" if self.docstatus == 1 else "Unpaid" + + + if add_salary and add_salary.name in [data.additional_salary for data in self.earnings]: + frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status) + def on_cancel(self): self.set_status() self.update_status() + self.update_payment_status_for_gratuity() self.cancel_loan_repayment_entry() def on_trash(self): @@ -566,6 +584,7 @@ class SalarySlip(TransactionBase): for d in self.get(key): if d.salary_component == struct_row.salary_component: component_row = d + if not component_row or (struct_row.get("is_additional_component") and not overwrite): if amount: self.append(key, { From b88af3a3f79e4cfd3d8db321b34c48ae276f5087 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 2 Nov 2020 18:35:03 +0530 Subject: [PATCH 16/64] feat: Accrural Entry for Gratuity beafore Payment --- .../doctype/payment_entry/payment_entry.py | 2 +- erpnext/payroll/doctype/gratuity/gratuity.js | 10 ++++ .../payroll/doctype/gratuity/gratuity.json | 20 +++++++- erpnext/payroll/doctype/gratuity/gratuity.py | 50 ++++++++++++++++++- 4 files changed, 78 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index df49667ed2d..123db7ee9c5 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1182,7 +1182,7 @@ def set_party_account(dt, dn, doc, party_type): elif dt == "Expense Claim": party_account = doc.payable_account elif dt == "Gratuity": - party_account = doc.expense_account + party_account = doc.payable_account else: party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company) return party_account diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js index dfdf08bdea0..9118ccc99d8 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.js +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -19,6 +19,16 @@ frappe.ui.form.on('Gratuity', { } }; }); + + 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") { diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index b81ae588eaa..5cffd7eebf9 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -19,8 +19,10 @@ "pay_via_salary_slip", "payroll_date", "salary_component", + "payable_account", "expense_account", "mode_of_payment", + "cost_center", "column_break_15", "current_work_experience", "amount", @@ -173,12 +175,28 @@ "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-10-27 14:04:41.886934", + "modified": "2020-11-02 18:21:11.971488", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index db353e9d71a..b09419116d3 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -7,9 +7,11 @@ import frappe from frappe import _, bold from frappe.model.document import Document 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(Document): +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"] @@ -18,7 +20,51 @@ class Gratuity(Document): self.status = "Unpaid" def on_submit(self): - self.create_additional_salary() + 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.employee, + "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: From 550e60a69d135d8804353abe159d22c912d0e3e1 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 2 Nov 2020 19:14:20 +0530 Subject: [PATCH 17/64] feat: fix test for Gratuity --- erpnext/payroll/doctype/gratuity/gratuity.py | 1 + .../payroll/doctype/gratuity/test_gratuity.py | 74 +++++++++---------- .../doctype/salary_slip/salary_slip.py | 11 ++- 3 files changed, 39 insertions(+), 47 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index b09419116d3..e9e577c1257 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -34,6 +34,7 @@ class Gratuity(AccountsController): make_gl_entries(gl_entries, cancel) def get_gl_entries(self): + print(self.payable_account, self.expense_account) gl_entry = [] # payable entry if self.amount: diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 680ecbcfc1b..5053f886cbb 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -9,27 +9,19 @@ from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip 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 class TestGratuity(unittest.TestCase): def setUp(self): - frappe.db.sql("DELETE FROM `tabgratuity`") + 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 = frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)") - if not rule: - create_gratuity_rule() - else: - rule = frappe.get_doc("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)") - rule.applicable_earnings_component = [] - rule.append("applicable_earnings_component", { - "salary_component": "Basic Salary" - }) - rule.save() - rule.reload() + + rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") gratuity = frappe.new_doc("Gratuity") gratuity.employee = employee @@ -72,31 +64,12 @@ class TestGratuity(unittest.TestCase): #additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) - self.assertEqual(gratuity.status, "Paid") def test_check_gratuity_amount_based_on_all_previous_slabs(self): employee, sal_slip = create_employee_and_get_last_salary_slip() - rule = frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)") - if not rule: - create_gratuity_rule() - else: - rule = frappe.get_doc("Gratuity Rule", rule) - rule.applicable_earnings_component = [] - rule = frappe.get_doc("Gratuity Rule", "Rule Under Limited Contract (UAE)") - rule.append("applicable_earnings_component", { - "salary_component": "Basic Salary" - }) - rule.save() - rule.reload() - - mof = frappe.get_doc("Mode of Payment", "Cheque") - mof.accounts = [] - mof.append("accounts", { - "company": "_Test Company", - "default_account": "_Test Bank - _TC" - }) - - mof.save() + rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") + set_mode_of_payment_account() + payable_account = get_payable_account("_Test Company") gratuity = frappe.new_doc("Gratuity") gratuity.employee = employee @@ -105,6 +78,7 @@ class TestGratuity(unittest.TestCase): gratuity.pay_via_salary_slip = 0 gratuity.payroll_date = getdate() gratuity.expense_account = "Payment Account - _TC" + gratuity.payable_account = payable_account gratuity.mode_of_payment = "Cheque" gratuity.save() @@ -132,39 +106,57 @@ class TestGratuity(unittest.TestCase): }, 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(gratuity.paid_amount, flt(gratuity.amount, 2)) def tearDown(self): - frappe.db.sql("DELETE FROM `tabgratuity`") + 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() + else: + 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 set_mode_of_payment_account(): + mode_of_payment = frappe.get_doc("Mode of Payment", "Cheque") + mode_of_payment.accounts = [] + mode_of_payment.append("accounts", { + "company": "_Test Company", + "default_account": "_Test Bank - _TC" + }) + mode_of_payment.save() + def create_employee_and_get_last_salary_slip(): employee = make_employee("test_employee@salary.com") frappe.db.set_value("Employee", employee, "relieving_date", getdate()) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 68147269b37..c7b83b9aeb3 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -83,13 +83,12 @@ class SalarySlip(TransactionBase): "employee": self.employee, "ref_doctype": "Gratuity", "docstatus": 1, - }, fields = ["ref_docname", "name"])[0] + }, fields = ["ref_docname", "name"], limit=1) - status = "Paid" if self.docstatus == 1 else "Unpaid" - - - if add_salary and add_salary.name in [data.additional_salary for data in self.earnings]: - frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status) + if len(add_salary): + status = "Paid" if self.docstatus == 1 else "Unpaid" + if add_salary[0].name in [data.additional_salary for data in self.earnings]: + frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status) def on_cancel(self): self.set_status() From 66b697cd054b71604a560e8670642496d9e819b2 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 3 Nov 2020 12:33:00 +0530 Subject: [PATCH 18/64] feat: fix test for Gratuity --- erpnext/hr/doctype/employee/test_employee.py | 1 + erpnext/payroll/doctype/gratuity/gratuity.py | 1 - erpnext/payroll/doctype/gratuity/test_gratuity.py | 14 ++++++++++++-- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index f4b214adc3c..ed8222aca13 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -46,6 +46,7 @@ class TestEmployee(unittest.TestCase): self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) def make_employee(user, company=None, **kwargs): + "" if not frappe.db.get_value("User", user): frappe.get_doc({ "doctype": "User", diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index e9e577c1257..d2fc2f726f1 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -5,7 +5,6 @@ from __future__ import unicode_literals import frappe from frappe import _, bold -from frappe.model.document import Document 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 diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 5053f886cbb..569c89b270e 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -158,7 +158,7 @@ def set_mode_of_payment_account(): mode_of_payment.save() def create_employee_and_get_last_salary_slip(): - employee = make_employee("test_employee@salary.com") + 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}): @@ -168,5 +168,15 @@ def create_employee_and_get_last_salary_slip(): else: salary_slip = get_last_salary_slip(employee) - return employee, salary_slip + #just to see what going on travis will remove this + print(frappe.db.get_value("Employee", "test_employee@salary.com", "company")) + print(frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list")) + 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") + + print(frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list")) + + return employee, salary_slip From af9f172be8a077f704a69a641f0ba44a32cfd6e2 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 24 Nov 2020 18:36:26 +0530 Subject: [PATCH 19/64] fix: requested changes and sider --- erpnext/payroll/doctype/gratuity/gratuity.js | 30 +++++++++---------- .../payroll/doctype/gratuity/test_gratuity.py | 11 ++----- .../doctype/gratuity_rule/gratuity_rule.js | 9 +++--- .../doctype/salary_slip/test_salary_slip.py | 1 + .../regional/united_arab_emirates/setup.py | 4 --- 5 files changed, 23 insertions(+), 32 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js index 9118ccc99d8..565d2c49f94 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.js +++ b/erpnext/payroll/doctype/gratuity/gratuity.js @@ -2,15 +2,15 @@ // For license information, please see license.txt frappe.ui.form.on('Gratuity', { - setup: function(frm){ - frm.set_query('salary_component', function() { + setup: function (frm) { + frm.set_query('salary_component', function () { return { filters: { type: "Earning" } }; }); - frm.set_query("expense_account", function() { + frm.set_query("expense_account", function () { return { filters: { "root_type": "Expense", @@ -20,7 +20,7 @@ frappe.ui.form.on('Gratuity', { }; }); - frm.set_query("payable_account", function() { + frm.set_query("payable_account", function () { return { filters: { "root_type": "Liability", @@ -30,16 +30,16 @@ frappe.ui.form.on('Gratuity', { }; }); }, - 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() { + 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) { + callback: function (r) { var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } @@ -47,18 +47,18 @@ frappe.ui.form.on('Gratuity', { }); } }, - employee: function(frm) { + employee: function (frm) { frm.events.calculate_work_experience_and_amount(frm); }, - gratuity_rule: function(frm){ + gratuity_rule: function (frm) { frm.events.calculate_work_experience_and_amount(frm); }, - calculate_work_experience_and_amount: function(frm) { + calculate_work_experience_and_amount: function (frm) { - if(frm.doc.employee && frm.doc.gratuity_rule){ + if (frm.doc.employee && frm.doc.gratuity_rule) { frappe.call({ - method:"erpnext.payroll.doctype.gratuity.gratuity.calculate_work_experience_and_amount", - args:{ + method: "erpnext.payroll.doctype.gratuity.gratuity.calculate_work_experience_and_amount", + args: { employee: frm.doc.employee, gratuity_rule: frm.doc.gratuity_rule } @@ -69,4 +69,4 @@ frappe.ui.form.on('Gratuity', { } } -}); +}); \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 569c89b270e..0e485cc8302 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -6,15 +6,16 @@ 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 +from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, make_earning_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"] class TestGratuity(unittest.TestCase): def setUp(self): + make_earning_salary_component() frappe.db.sql("DELETE FROM `tabGratuity`") frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") @@ -168,15 +169,9 @@ def create_employee_and_get_last_salary_slip(): else: salary_slip = get_last_salary_slip(employee) - #just to see what going on travis will remove this - print(frappe.db.get_value("Employee", "test_employee@salary.com", "company")) - print(frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list")) - 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") - print(frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list")) - return employee, salary_slip diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js index 1a5347e792d..ee6c5df7371 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js @@ -25,17 +25,16 @@ frappe.ui.form.on('Gratuity Rule Slab', { 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; + 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){ + if (row.to_year <= row.from_year && row.to_year === 0) { frappe.throw(__("To(Year) year can not be less than From(year) ")); } } -}); - +}); \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 5daf1d439d1..634500fc47a 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -21,6 +21,7 @@ from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_ta class TestSalarySlip(unittest.TestCase): def setUp(self): setup_test() + def tearDown(self): frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py index 2a45c225906..c26633675fa 100644 --- a/erpnext/regional/united_arab_emirates/setup.py +++ b/erpnext/regional/united_arab_emirates/setup.py @@ -12,13 +12,9 @@ from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rul def setup(company=None, patch=True): make_custom_fields() add_print_formats() -<<<<<<< HEAD add_custom_roles_for_reports() add_permissions() - create_standard_documents() -======= create_gratuity_rule() ->>>>>>> test: gratuity if company: create_sales_tax(company) From 708065cb851423311c3eebaa3e9a65a93e5275d0 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 1 Dec 2020 18:54:31 +0530 Subject: [PATCH 20/64] fix: test cases --- .../payroll/doctype/gratuity/test_gratuity.py | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 0e485cc8302..571eef4a6cf 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -6,16 +6,18 @@ 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 +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"] +test_dependencies = ["Salary Component", "Salary Slip", "Account"] class TestGratuity(unittest.TestCase): def setUp(self): - make_earning_salary_component() + 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'") @@ -39,14 +41,11 @@ class TestGratuity(unittest.TestCase): 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 6 + #amount Calculation component_amount = frappe.get_list("Salary Detail", filters={ "docstatus": 1, @@ -80,7 +79,7 @@ class TestGratuity(unittest.TestCase): gratuity.payroll_date = getdate() gratuity.expense_account = "Payment Account - _TC" gratuity.payable_account = payable_account - gratuity.mode_of_payment = "Cheque" + gratuity.mode_of_payment = "Cash" gratuity.save() gratuity.submit() @@ -97,7 +96,7 @@ class TestGratuity(unittest.TestCase): self.assertEqual(floor(experience), gratuity.current_work_experience) - #amount Calculation 6 + #amount Calculation component_amount = frappe.get_list("Salary Detail", filters={ "docstatus": 1, @@ -150,7 +149,11 @@ def get_gratuity_rule(name): return rule def set_mode_of_payment_account(): - mode_of_payment = frappe.get_doc("Mode of Payment", "Cheque") + if not frappe.db.exists("Account", "Payment Account - _TC"): + mode_of_payment = create_account() + else: + mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") + mode_of_payment.accounts = [] mode_of_payment.append("accounts", { "company": "_Test Company", @@ -158,6 +161,18 @@ def set_mode_of_payment_account(): }) 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()) From ba6ff6e2270302c83416d74ec13a3513ef3cc892 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 2 Dec 2020 13:32:02 +0530 Subject: [PATCH 21/64] fix: conflict --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 +- erpnext/payroll/doctype/gratuity/test_gratuity.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 123db7ee9c5..8d2907e0fbc 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -1036,7 +1036,7 @@ def get_total_amount_exchange_rate_base_on_currency(party_account_currency, comp def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency): outstanding_amount, bill_no = None -if reference_doctype in ("Sales Invoice", "Purchase Invoice"): + if reference_doctype in ("Sales Invoice", "Purchase Invoice"): outstanding_amount = ref_doc.get("outstanding_amount") bill_no = ref_doc.get("bill_no") elif reference_doctype == "Expense Claim": diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 571eef4a6cf..f32e0eb74e4 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -137,8 +137,7 @@ def get_gratuity_rule(name): rule = frappe.db.exists("Gratuity Rule", name) if not rule: create_gratuity_rule() - else: - rule = frappe.get_doc("Gratuity Rule", name) + rule = frappe.get_doc("Gratuity Rule", name) rule.applicable_earnings_component = [] rule.append("applicable_earnings_component", { "salary_component": "Basic Salary" From 4cc333996c1e2b4516f2c03a6a52c829bd09d297 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 3 Dec 2020 11:47:23 +0530 Subject: [PATCH 22/64] fix: test --- .../payroll/doctype/gratuity/test_gratuity.py | 51 ++++++++++--------- .../doctype/gratuity_rule/gratuity_rule.json | 6 +-- .../doctype/payroll_entry/payroll_entry.py | 2 +- 3 files changed, 30 insertions(+), 29 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index f32e0eb74e4..e89e3dd077a 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -26,15 +26,7 @@ class TestGratuity(unittest.TestCase): rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") - gratuity = frappe.new_doc("Gratuity") - gratuity.employee = employee - gratuity.posting_date = getdate() - gratuity.gratuity_rule = rule.name - gratuity.pay_via_salary_slip = 1 - gratuity.salary_component = "Performance Bonus" - gratuity.payroll_date = getdate() - gratuity.save() - gratuity.submit() + 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']) @@ -69,20 +61,8 @@ class TestGratuity(unittest.TestCase): employee, sal_slip = create_employee_and_get_last_salary_slip() rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") set_mode_of_payment_account() - payable_account = get_payable_account("_Test Company") - gratuity = frappe.new_doc("Gratuity") - gratuity.employee = employee - gratuity.posting_date = getdate() - gratuity.gratuity_rule = rule.name - gratuity.pay_via_salary_slip = 0 - gratuity.payroll_date = getdate() - gratuity.expense_account = "Payment Account - _TC" - gratuity.payable_account = payable_account - gratuity.mode_of_payment = "Cash" - - gratuity.save() - gratuity.submit() + 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']) @@ -127,7 +107,7 @@ class TestGratuity(unittest.TestCase): gratuity.reload() self.assertEqual(gratuity.status, "Paid") - self.assertEqual(gratuity.paid_amount, flt(gratuity.amount, 2)) + self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2)) def tearDown(self): frappe.db.sql("DELETE FROM `tabGratuity`") @@ -147,11 +127,32 @@ def get_gratuity_rule(name): 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() - else: - mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") + + mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") mode_of_payment.accounts = [] mode_of_payment.append("accounts", { diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json index 7d24e41f485..84cdcf50386 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json @@ -27,7 +27,7 @@ "fieldname": "calculate_gratuity_amount_based_on", "fieldtype": "Select", "in_list_view": 1, - "label": "Calculate Gratuity Amount Based on", + "label": "Calculate Gratuity Amount Based On", "options": "Current Slab\nSum of all previous slabs", "reqd": 1 }, @@ -67,7 +67,7 @@ "default": "365", "fieldname": "total_working_days_per_year", "fieldtype": "Int", - "label": "Total Working Days Per Year" + "label": "Total working Days Per Year" }, { "fieldname": "minimum_year_for_gratuity", @@ -77,7 +77,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-27 14:04:31.617621", + "modified": "2020-12-03 17:08:27.891535", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity Rule", diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 8c2d9740ece..4cf4542a4f9 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -542,7 +542,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): title = _("Creating Salary Slips...")) else: salary_slip_name = frappe.db.sql( - '''SELECT + '''SELECT name FROM `tabSalary Slip` WHERE company=%s From b71611dd3da99c18563cd32d317566cc37e0d820 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 4 Dec 2020 15:05:57 +0530 Subject: [PATCH 23/64] feat: Added Form dashboard --- .../doctype/gratuity/gratuity_dashboard.py | 20 +++++++++++++++++++ .../gratuity_rule/gratuity_rule_dashboard.py | 13 ++++++++++++ 2 files changed, 33 insertions(+) create mode 100644 erpnext/payroll/doctype/gratuity/gratuity_dashboard.py create mode 100644 erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py new file mode 100644 index 00000000000..5b2489f22cd --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py @@ -0,0 +1,20 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'reference_name', + 'non_standard_fieldnames': { + 'Additional Salary': 'ref_docname', + }, + 'transactions': [ + { + 'label': _('Payment'), + 'items': ['Payment Entry'] + }, + { + 'label': _('Additional Salary'), + 'items': ['Additional Salary'] + } + ] + } \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py new file mode 100644 index 00000000000..0d70163495a --- /dev/null +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py @@ -0,0 +1,13 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'gratuity_rule', + 'transactions': [ + { + 'label': _('Gratuity'), + 'items': ['Gratuity'] + } + ] + } \ No newline at end of file From 4da7a15ac048f7592351891e150be628727ae8fd Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 11 Dec 2020 09:45:58 +0530 Subject: [PATCH 24/64] fix: Fixed typo --- .../patches/v13_0/setup_gratuity_rule_for_india_and_uae.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py index a71f33c233e..01fd6a158e9 100644 --- a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py +++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py @@ -8,9 +8,9 @@ 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"}): + 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"}): + if frappe.db.exists("Company", {"country": "United Arab Emirates"}): from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule - create_gratuity_rule() \ No newline at end of file + create_gratuity_rule() From eb065d6d40d3af79d8802264db9af1658d8a0553 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 11 Dec 2020 09:50:50 +0530 Subject: [PATCH 25/64] fix: fixed gl entries --- erpnext/payroll/doctype/gratuity/gratuity.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index d2fc2f726f1..5a08f6a24c1 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -33,7 +33,6 @@ class Gratuity(AccountsController): make_gl_entries(gl_entries, cancel) def get_gl_entries(self): - print(self.payable_account, self.expense_account) gl_entry = [] # payable entry if self.amount: @@ -57,7 +56,7 @@ class Gratuity(AccountsController): "account": self.expense_account, "debit": self.amount, "debit_in_account_currency": self.amount, - "against": self.employee, + "against": self.payable_account, "cost_center": self.cost_center }, item=self) ) From 1446749b63be791d309de2ecb29208135e2db2f7 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 17 Dec 2020 14:24:05 +0530 Subject: [PATCH 26/64] fix: changes requested --- erpnext/payroll/doctype/gratuity/gratuity.py | 53 ++++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 5a08f6a24c1..7160bf0ef63 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -113,7 +113,13 @@ def calculate_work_experience(employee, gratuity_rule): 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) + 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" @@ -124,10 +130,9 @@ def calculate_work_experience(employee, gratuity_rule): total_absents = get_non_working_days(employee, relieving_date, "Absent") employee_total_workings_days -= total_absents - # current_work_experience = time_difference.years - - current_work_experience = employee_total_workings_days/total_working_days_per_year or 1 + return employee_total_workings_days +def get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity): if method == "Round off Work Experience": current_work_experience = round(current_work_experience) else: @@ -135,7 +140,6 @@ def calculate_work_experience(employee, gratuity_rule): 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): @@ -157,27 +161,22 @@ def get_non_working_days(employee, relieving_date, status): return record[0].total_lwp if len(record) else 0 def calculate_gratuity_amount(employee, gratuity_rule, experience): - 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] - - slabs = get_gratuity_rule_slabs(gratuity_rule) - + 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": - if experience >= slab.from_year and (slab.to_year == 0 or experience < slab.to_year): - gratuity_amount = total_applicable_components_amount * experience * slab.fraction_of_applicable_earnings - if slab.fraction_of_applicable_earnings: - slab_found = True + 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 == True: 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 @@ -194,16 +193,20 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): 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, @@ -217,9 +220,17 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen 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") From 3278d02cd2c3a910cbe2ca0b032047ada198cdfa Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Wed, 23 Dec 2020 18:06:46 +0530 Subject: [PATCH 27/64] fix: sider --- erpnext/payroll/doctype/gratuity/gratuity.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 7160bf0ef63..1acd6e342fd 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -116,7 +116,7 @@ def calculate_work_experience(employee, gratuity_rule): 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) + 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 ): @@ -132,7 +132,7 @@ def calculate_employee_total_workings_days(employee, date_of_joining, relieving_ return employee_total_workings_days -def get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity): +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: @@ -174,7 +174,7 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): 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 == True: + if slab_found: break elif calculate_gratuity_amount_based_on == "Sum of all previous slabs": From e1e4810a466f1146fafde434216a043f8857be07 Mon Sep 17 00:00:00 2001 From: David Angulo Date: Mon, 25 Jan 2021 08:29:11 -0600 Subject: [PATCH 28/64] feat: Make patient age translateable --- .../doctype/clinical_procedure/clinical_procedure.js | 2 +- erpnext/healthcare/doctype/lab_test/lab_test.js | 2 +- erpnext/healthcare/doctype/patient/patient.js | 4 ++-- erpnext/healthcare/doctype/patient/patient.py | 2 +- .../doctype/patient_appointment/patient_appointment.js | 2 +- .../healthcare/doctype/patient_encounter/patient_encounter.js | 2 +- .../healthcare/doctype/sample_collection/sample_collection.js | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js index 1d4411d73de..f68da9b9ba1 100644 --- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js +++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js @@ -364,7 +364,7 @@ let calculate_age = function(birth) { let age = new Date(); age.setTime(ageMS); let years = age.getFullYear() - 1970; - return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; + return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`; }; // List Stock items diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js index f1634c12949..bb7976ccfac 100644 --- a/erpnext/healthcare/doctype/lab_test/lab_test.js +++ b/erpnext/healthcare/doctype/lab_test/lab_test.js @@ -258,5 +258,5 @@ var calculate_age = function (dob) { var age = new Date(); age.setTime(ageMS); var years = age.getFullYear() - 1970; - return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; + return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`; }; diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js index 490f2475001..15e203a824f 100644 --- a/erpnext/healthcare/doctype/patient/patient.js +++ b/erpnext/healthcare/doctype/patient/patient.js @@ -50,7 +50,7 @@ frappe.ui.form.on('Patient', { $(frm.fields_dict['age_html'].wrapper).html(''); } 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 { let age_str = get_age(frm.doc.dob); - $(frm.fields_dict['age_html'].wrapper).html('AGE : ' + age_str); + $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`); } } else { diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 63dd8d4793a..8603f974c39 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -108,7 +108,7 @@ class Patient(Document): if self.dob: dob = getdate(self.dob) age = dateutil.relativedelta.relativedelta(getdate(), dob) - age_str = str(age.years) + ' year(s) ' + str(age.months) + ' month(s) ' + str(age.days) + ' day(s)' + age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") return age_str def invoice_patient_registration(self): diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js index 3d5073b13e7..750ecdb3546 100644 --- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js +++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js @@ -596,5 +596,5 @@ let calculate_age = function(birth) { let age = new Date(); age.setTime(ageMS); let years = age.getFullYear() - 1970; - return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; + return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`; }; diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js index e960f0a9c40..aaeaa692e63 100644 --- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js +++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js @@ -358,5 +358,5 @@ let calculate_age = function(birth) { let age = new Date(); age.setTime(ageMS); let years = age.getFullYear() - 1970; - return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; + return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`; }; diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js index 03903912358..ddf8285bc6d 100644 --- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js +++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js @@ -36,5 +36,5 @@ var calculate_age = function(birth) { var age = new Date(); age.setTime(ageMS); var years = age.getFullYear() - 1970; - return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)'; + return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`; }; From f5d6bd77e5a0203657e17b0192cb076b0b342655 Mon Sep 17 00:00:00 2001 From: shariquerik Date: Mon, 22 Feb 2021 21:27:14 +0530 Subject: [PATCH 29/64] fix: item attributes not editable until refresh --- erpnext/stock/doctype/item/item.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index f851aafd9ad..55391235cb0 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -85,7 +85,7 @@ frappe.ui.form.on("Item", { } if (frm.doc.variant_of) { frm.set_intro(__('This Item is a Variant of {0} (Template).', - [`${frm.doc.variant_of}`]), true); + [`${frm.doc.variant_of}`]), true); } if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) { From 56f6dbb66662a50b7cf3d3d35be17312ff49bfe3 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 26 Feb 2021 21:11:56 -0600 Subject: [PATCH 30/64] Remove obsolete section titles --- .../doctype/quality_meeting/quality_meeting.json | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json index ead403d453a..36e56e5c144 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json @@ -33,8 +33,7 @@ }, { "fieldname": "sb_00", - "fieldtype": "Section Break", - "label": "Agenda" + "fieldtype": "Section Break" }, { "fieldname": "agenda", @@ -44,13 +43,12 @@ }, { "fieldname": "sb_01", - "fieldtype": "Section Break", - "label": "Minutes" + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-10-27 16:36:45.657883", + "modified": "2021-02-26 16:36:45.657883", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Meeting", @@ -85,4 +83,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} From 28da945ecdb2084ba65a0932ef0fb0fb49e6f895 Mon Sep 17 00:00:00 2001 From: Daniel Chalmers Date: Fri, 26 Feb 2021 21:14:12 -0600 Subject: [PATCH 31/64] Update quality_meeting.json --- .../doctype/quality_meeting/quality_meeting.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json index 36e56e5c144..e2125c3933a 100644 --- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json +++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json @@ -48,7 +48,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-02-26 16:36:45.657883", + "modified": "2021-02-27 16:36:45.657883", "modified_by": "Administrator", "module": "Quality Management", "name": "Quality Meeting", From 1521b31795aa79dff94ac44f17ee61a55fee5639 Mon Sep 17 00:00:00 2001 From: barredterra <14891507+barredterra@users.noreply.github.com> Date: Wed, 3 Mar 2021 12:33:48 +0100 Subject: [PATCH 32/64] fix: use set_value instead of sql --- erpnext/regional/india/setup.py | 5 +++-- erpnext/regional/italy/setup.py | 4 +--- erpnext/regional/united_states/setup.py | 3 +-- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 526198424f3..ee46a52f1ce 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -105,8 +105,9 @@ def add_print_formats(): frappe.reload_doc("accounts", "print_format", "gst_pos_invoice") frappe.reload_doc("accounts", "print_format", "GST E-Invoice") - frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """) + frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0) + frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) + frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) def make_custom_fields(update=True): hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py index 217d623a8d5..95b92e76a69 100644 --- a/erpnext/regional/italy/setup.py +++ b/erpnext/regional/italy/setup.py @@ -189,9 +189,7 @@ def make_custom_fields(update=True): def setup_report(): report_name = 'Electronic Invoice Register' - - frappe.db.sql(""" update `tabReport` set disabled = 0 where - name = %s """, report_name) + frappe.db.set_value("Report", report_name, "disabled", 0) if not frappe.db.get_value('Custom Role', dict(report=report_name)): frappe.get_doc(dict( diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py index 2b0ecafebc5..24ab1cf049f 100644 --- a/erpnext/regional/united_states/setup.py +++ b/erpnext/regional/united_states/setup.py @@ -36,5 +36,4 @@ def make_custom_fields(update=True): def add_print_formats(): frappe.reload_doc("regional", "print_format", "irs_1099_form") - frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where - name in('IRS 1099 Form') """) + frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0) From fa932e2a3e238a8ba137187e493040151f9e40c7 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 8 Mar 2021 14:36:40 +0530 Subject: [PATCH 33/64] fix: sending proper arguments for report --- .../tds_computation_summary/tds_computation_summary.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index c7cfee74cb0..f60e409997b 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt +from frappe.utils import flt, getdate from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category \ import get_advance_vouchers, get_debit_note_amount @@ -55,7 +55,7 @@ def get_result(filters): except IndexError: account = [] total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account, - filters.company, filters.from_date, filters.to_date) + filters.company, filters.from_date, filters.to_date, filters.fiscal_year) if total_invoiced_amount or tds_deducted: row = [supplier.pan, supplier.name] @@ -68,7 +68,7 @@ def get_result(filters): return out -def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): +def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year): ''' calculate total invoice amount and total tds deducted for given supplier ''' entries = frappe.db.sql(""" @@ -94,7 +94,9 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date): """.format(', '.join(["'%s'" % d for d in vouchers])), (account, from_date, to_date, company))[0][0]) - debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, company=company) + date_range_filter = [fiscal_year, from_date, to_date] + + debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company) total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount From 1002e36c532243f490a6e282c7bb1dbce2495468 Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 8 Mar 2021 14:39:19 +0530 Subject: [PATCH 34/64] fix: removed unused import --- .../report/tds_computation_summary/tds_computation_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py index f60e409997b..a8280c1b18e 100644 --- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py +++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py @@ -1,7 +1,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, getdate +from frappe.utils import flt from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category \ import get_advance_vouchers, get_debit_note_amount From cc46866cd912e2b6170bb1392fbabcb3f8981357 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 4 Mar 2021 18:56:00 +0530 Subject: [PATCH 35/64] fix: do not update PE title during data import Related issue: ISS-20-21-10132 --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 31a4c8a3879..61858b33393 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -455,6 +455,10 @@ class PaymentEntry(AccountsController): .format(total_negative_outstanding), InvalidPaymentEntry) def set_title(self): + if frappe.flags.in_import and self.title: + # do not set title dynamically if title exists during data import. + return + if self.payment_type in ("Receive", "Pay"): self.title = self.party else: @@ -996,7 +1000,7 @@ def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_curre total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) elif ref_doc.doctype == "Employee Advance": total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc) - + if not total_amount: total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency( party_account_currency, company_currency, ref_doc) @@ -1326,4 +1330,4 @@ def make_payment_order(source_name, target_doc=None): }, target_doc, set_missing_values) - return doclist \ No newline at end of file + return doclist From 5571b988dda24a20cb7329c63e6eff889fe624bc Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 8 Mar 2021 15:36:46 +0530 Subject: [PATCH 36/64] fix: make title field read only --- erpnext/accounts/doctype/payment_entry/payment_entry.json | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 2e1f201e253..328584a61a0 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -536,7 +536,8 @@ "fieldtype": "Data", "hidden": 1, "label": "Title", - "print_hide": 1 + "print_hide": 1, + "read_only": 1 }, { "depends_on": "party", @@ -588,7 +589,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-10-30 13:56:20.007336", + "modified": "2021-03-08 13:05:16.958866", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", @@ -632,4 +633,4 @@ "sort_order": "DESC", "title_field": "title", "track_changes": 1 -} \ No newline at end of file +} From d95b59e90c4d1b0a58a8e2ddb0f2ae908070a6bd Mon Sep 17 00:00:00 2001 From: Afshan Date: Mon, 8 Mar 2021 18:19:53 +0530 Subject: [PATCH 37/64] fix: call function when arguments available --- .../public/js/controllers/taxes_and_totals.js | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index d81321b2916..3a3ee3858bf 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -158,16 +158,18 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ let me = this; frappe.flags.round_off_applicable_accounts = []; - return frappe.call({ - "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", - "args": { - "company": me.frm.doc.company, - "account_list": frappe.flags.round_off_applicable_accounts - }, - callback: function(r) { - frappe.flags.round_off_applicable_accounts.push(...r.message); - } - }); + if (me.frm.doc.company) { + return frappe.call({ + "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts", + "args": { + "company": me.frm.doc.company, + "account_list": frappe.flags.round_off_applicable_accounts + }, + callback: function(r) { + frappe.flags.round_off_applicable_accounts.push(...r.message); + } + }); + } }, determine_exclusive_rate: function() { From be98ee26cdecb6101b1222ec86d4de08cb5b1873 Mon Sep 17 00:00:00 2001 From: walstanb Date: Tue, 9 Mar 2021 13:03:52 +0530 Subject: [PATCH 38/64] fix: breadcrumbs for company --- erpnext/setup/doctype/company/company_list.js | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/erpnext/setup/doctype/company/company_list.js b/erpnext/setup/doctype/company/company_list.js index 017286560fe..1d1184f04d3 100644 --- a/erpnext/setup/doctype/company/company_list.js +++ b/erpnext/setup/doctype/company/company_list.js @@ -1,10 +1,5 @@ frappe.listview_settings['Company'] = { - onload: () => { - frappe.breadcrumbs.add({ - type: 'Custom', - module: __('Accounts'), - label: __('Accounts'), - route: '#modules/Accounts' - }); - } -} \ No newline at end of file + onload() { + frappe.breadcrumbs.add('Accounts'); + }, +}; From 9b4e5bfa87786ccf1e8815b270066db20237875a Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Mon, 1 Mar 2021 10:44:21 +0530 Subject: [PATCH 39/64] chore: improve error message for missing fields. --- erpnext/payroll/doctype/salary_slip/salary_slip.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index 60aff02b38d..deaa2096587 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -504,7 +504,8 @@ class SalarySlip(TransactionBase): return amount except NameError as err: - frappe.throw(_("Name error: {0}").format(err)) + frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err), + title=_("Name error")) except SyntaxError as err: frappe.throw(_("Syntax error in formula or condition: {0}").format(err)) except Exception as e: @@ -928,7 +929,8 @@ class SalarySlip(TransactionBase): if condition: return frappe.safe_eval(condition, self.whitelisted_globals, data) except NameError as err: - frappe.throw(_("Name error: {0}").format(err)) + frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err), + title=_("Name error")) except SyntaxError as err: frappe.throw(_("Syntax error in condition: {0}").format(err)) except Exception as e: @@ -1223,4 +1225,4 @@ def unlink_ref_doc_from_salary_slip(ref_no): def generate_password_for_pdf(policy_template, employee): employee = frappe.get_doc("Employee", employee) - return policy_template.format(**employee.as_dict()) \ No newline at end of file + return policy_template.format(**employee.as_dict()) From 06a6ee37cbf62e848794f4a5076245e7b029dccd Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:23:26 +0530 Subject: [PATCH 40/64] fix: Ignore mandatory fields in opening invoice via Opening Invoice Creation tool (#24821) --- .../opening_invoice_creation_tool.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 76027a301f3..e6449b78316 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -198,6 +198,7 @@ def start_import(invoices): try: publish(idx, len(invoices), d.doctype) doc = frappe.get_doc(d) + doc.flags.ignore_mandatory = True doc.insert() doc.submit() frappe.db.commit() From 79b5be18305b8d5d8ff89e815b87d6b311754e04 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Tue, 9 Mar 2021 15:56:43 +0100 Subject: [PATCH 41/64] fix: Group German VAT Accounts (#24804) * fix: group account for tax assets and liabilities (germany) * fix: spacing --- .../verified/de_kontenplan_SKR03_gnucash.json | 27 ++++---- .../verified/de_kontenplan_SKR04.json | 64 ++++++++++--------- ..._kontenplan_SKR04_with_account_number.json | 1 + 3 files changed, 52 insertions(+), 40 deletions(-) diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json index 89465eedf0e..ee501f664b6 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json @@ -63,17 +63,21 @@ "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": { "account_number": "1371" }, - "Abziehbare VSt. 7%": { - "account_number": "1571" - }, - "Abziehbare VSt. 19%": { - "account_number": "1576" - }, - "Abziehbare VStr. nach \u00a713b UStG 19%": { - "account_number": "1577" - }, - "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { - "account_number": "3120" + "Abziehbare Vorsteuer": { + "account_type": "Tax", + "is_group": 1, + "Abziehbare Vorsteuer 7%": { + "account_number": "1571" + }, + "Abziehbare Vorsteuer 19%": { + "account_number": "1576" + }, + "Abziehbare Vorsteuer nach \u00a713b UStG 19%": { + "account_number": "1577" + }, + "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": { + "account_number": "3120" + } } }, "III. Wertpapiere": { @@ -196,6 +200,7 @@ }, "Umsatzsteuer": { "is_group": 1, + "account_type": "Tax", "Umsatzsteuer 7%": { "account_number": "1771" }, diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json index 7fa67081341..57e8bdd9dc7 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json @@ -292,18 +292,21 @@ "Umsatzsteuerforderungen fr\u00fchere Jahre": {} }, "Sonstige Verm\u00f6gensgegenst\u00e4nde oder sonstige Verbindlichkeiten": { - "Abziehbare Vorsteuer": {}, - "Abziehbare Vorsteuer 16%": {}, - "Abziehbare Vorsteuer 19%": {}, - "Abziehbare Vorsteuer 7%": {}, - "Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {}, - "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {}, - "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {}, - "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {}, - "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {}, - "Abziehbare Vorsteuer nach \u00a7 13b UStG ": {}, - "Abziehbare Vorsteuer nach \u00a7 13b UStG 16%": {}, - "Abziehbare Vorsteuer nach \u00a7 13b UStG 19%": {}, + "Abziehbare Vorsteuer": { + "account_type": "Tax", + "is_group": 1, + "Abziehbare Vorsteuer 16%": {}, + "Abziehbare Vorsteuer 19%": {}, + "Abziehbare Vorsteuer 7%": {}, + "Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {}, + "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {}, + "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {}, + "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {}, + "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {}, + "Abziehbare Vorsteuer nach \u00a7 13b UStG ": {}, + "Abziehbare Vorsteuer nach \u00a7 13b UStG 16%": {}, + "Abziehbare Vorsteuer nach \u00a7 13b UStG 19%": {} + }, "Aufl\u00f6sung Vorsteuer aus Vorjahr \u00a7 4/3 EStG": {}, "Aufzuteilende Vorsteuer": {}, "Aufzuteilende Vorsteuer 16%": {}, @@ -673,23 +676,26 @@ "Sonstige Verrechnungskonten (Interimskonto)": { "account_type": "Stock Received But Not Billed" }, - "Umsatzsteuer": {}, - "Umsatzsteuer 16%": {}, - "Umsatzsteuer 19%": {}, - "Umsatzsteuer 7%": {}, - "Umsatzsteuer Vorjahr": {}, - "Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {}, - "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {}, - "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {}, - "Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {}, - "Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {}, - "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {}, - "Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {}, - "Umsatzsteuer fr\u00fchere Jahre": {}, - "Umsatzsteuer laufendes Jahr": {}, - "Umsatzsteuer nach \u00a713b UStG": {}, - "Umsatzsteuer nach \u00a713b UStG 16%": {}, - "Umsatzsteuer nach \u00a713b UStG 19%": {}, + "Umsatzsteuer": { + "account_type": "Tax", + "is_group": 1, + "Umsatzsteuer 16%": {}, + "Umsatzsteuer 19%": {}, + "Umsatzsteuer 7%": {}, + "Umsatzsteuer Vorjahr": {}, + "Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {}, + "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {}, + "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {}, + "Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {}, + "Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {}, + "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {}, + "Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {}, + "Umsatzsteuer fr\u00fchere Jahre": {}, + "Umsatzsteuer laufendes Jahr": {}, + "Umsatzsteuer nach \u00a713b UStG": {}, + "Umsatzsteuer nach \u00a713b UStG 16%": {}, + "Umsatzsteuer nach \u00a713b UStG 19%": {} + }, "Umsatzsteuer- Vorauszahlungen": {}, "Umsatzsteuer- Vorauszahlungen 1/11": {}, "Verbindlichkeiten aus Lohn- und Kirchensteuer": {} diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json index 849df18c6f9..2bf55cfcd04 100644 --- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json +++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json @@ -659,6 +659,7 @@ }, "Abziehbare Vorsteuer (Gruppe)": { "is_group": 1, + "account_type": "Tax", "Abziehbare Vorsteuer": { "account_number": "1400" }, From fa7307425008c4e15c78552ed24516cfdcdeb9f2 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:27:29 +0530 Subject: [PATCH 42/64] fix: salary structure assign to employee (#24798) * fix: salary structure assign to employee * fix: pass data from dialog --- erpnext/payroll/doctype/salary_structure/salary_structure.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js index 1378bf0b913..6aa13873633 100755 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.js +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js @@ -142,6 +142,8 @@ frappe.ui.form.on('Salary Structure', { ], primary_action: function() { var data = d.get_values(); + delete data.company + delete data.currency frappe.call({ doc: frm.doc, method: "assign_salary_structure", From 261c42186fb27e73e191640e5aab85734f716f6b Mon Sep 17 00:00:00 2001 From: Shariq Ansari <30859809+shariquerik@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:28:00 +0530 Subject: [PATCH 43/64] fix: updating phone icon setup logic for readonly fields (#24787) * fix: updating phone icon setup logic for readonly fields * fix: sider fix --- erpnext/public/js/telephony.js | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js index b66126c2b8c..9548d6c5f36 100644 --- a/erpnext/public/js/telephony.js +++ b/erpnext/public/js/telephony.js @@ -1,11 +1,18 @@ frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( { make_input() { - if (!this.df.read_only) { - this._super(); - } + this._super(); if (this.df.options == 'Phone') { this.setup_phone(); } + if (this.frm && this.frm.fields_dict) { + Object.values(this.frm.fields_dict).forEach(function(field) { + if (field.df.read_only === 1 && field.df.options === 'Phone' + && field.disp_area.style[0] != 'display' && !field.has_icon) { + field.setup_phone(); + field.has_icon = true; + } + }); + } }, setup_phone() { if (frappe.phone_call.handler) { From 9f6015a4e982e634e0fecc816d42cc472f8c01b4 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:31:14 +0530 Subject: [PATCH 44/64] fix: batch no in POS (#24771) * fix: batch no in list * fix: check for batch no to be list --- erpnext/stock/doctype/serial_no/serial_no.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py index 6bacf1f8a33..c8d8ca9e17e 100644 --- a/erpnext/stock/doctype/serial_no/serial_no.py +++ b/erpnext/stock/doctype/serial_no/serial_no.py @@ -554,7 +554,7 @@ def auto_fetch_serial_number(qty, item_code, warehouse, posting_date=None, batch if batch_nos: try: - filters["batch_no"] = json.loads(batch_nos) + filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)] except: filters["batch_no"] = [batch_nos] @@ -626,4 +626,4 @@ def fetch_serial_numbers(filters, qty, do_not_include=[]): batch_no_condition=batch_no_condition ), filters, as_dict=1) - return serial_numbers \ No newline at end of file + return serial_numbers From e314b8b209ab0a990b861c4013e66a2d549bf57f Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:31:57 +0530 Subject: [PATCH 45/64] fix: general ledger report (#24770) --- .../report/general_ledger/general_ledger.py | 31 ++++++++++++------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index f735d87a764..b5d7992604f 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -129,6 +129,9 @@ def get_gl_entries(filters, accounting_dimensions): order_by_statement = "order by posting_date, account, creation" + if filters.get("include_dimensions"): + order_by_statement = "order by posting_date, creation" + if filters.get("group_by") == _("Group by Voucher"): order_by_statement = "order by posting_date, voucher_type, voucher_no" @@ -142,7 +145,9 @@ def get_gl_entries(filters, accounting_dimensions): distributed_cost_center_query = "" if filters and filters.get('cost_center'): - select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, + select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, + credit*(DCC_allocation.percentage_allocation/100) as credit, + debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """ distributed_cost_center_query = """ @@ -200,7 +205,7 @@ def get_gl_entries(filters, accounting_dimensions): def get_conditions(filters): conditions = [] - if filters.get("account"): + if filters.get("account") and not filters.get("include_dimensions"): lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) conditions.append("""account in (select name from tabAccount where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) @@ -245,17 +250,19 @@ def get_conditions(filters): if match_conditions: conditions.append(match_conditions) - accounting_dimensions = get_accounting_dimensions(as_list=False) + if filters.get("include_dimensions"): + accounting_dimensions = get_accounting_dimensions(as_list=False) - if accounting_dimensions: - for dimension in accounting_dimensions: - if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) - conditions.append("{0} in %({0})s".format(dimension.fieldname)) - else: - conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) + if accounting_dimensions: + for dimension in accounting_dimensions: + if not dimension.disabled: + if filters.get(dimension.fieldname): + if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): + filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, + filters.get(dimension.fieldname)) + conditions.append("{0} in %({0})s".format(dimension.fieldname)) + else: + conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) return "and {}".format(" and ".join(conditions)) if conditions else "" From 78777d6ed4b2da74cda83e77516435482e5b3014 Mon Sep 17 00:00:00 2001 From: Sagar Vora Date: Tue, 9 Mar 2021 20:35:08 +0530 Subject: [PATCH 46/64] fix: fetch Material Requests of type Customer Provided in Stock Entry using Get Items From (#24756) Co-authored-by: walstanb --- erpnext/public/js/utils.js | 40 ++++++++++--------- .../stock/doctype/stock_entry/stock_entry.js | 28 +++++++++++-- 2 files changed, 47 insertions(+), 21 deletions(-) diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index e5bd4d7e050..e5b50d86eda 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -595,21 +595,7 @@ erpnext.utils.update_child_items = function(opts) { } erpnext.utils.map_current_doc = function(opts) { - let query_args = {}; - if (opts.get_query_filters) { - query_args.filters = opts.get_query_filters; - } - - if (opts.get_query_method) { - query_args.query = opts.get_query_method; - } - - if (query_args.filters || query_args.query) { - opts.get_query = () => { - return query_args; - } - } - var _map = function() { + function _map() { if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) { // remove first item row if empty if(!cur_frm.doc.items[0].item_code) { @@ -683,8 +669,22 @@ erpnext.utils.map_current_doc = function(opts) { } }); } - if(opts.source_doctype) { - var d = new frappe.ui.form.MultiSelectDialog({ + + let query_args = {}; + if (opts.get_query_filters) { + query_args.filters = opts.get_query_filters; + } + + if (opts.get_query_method) { + query_args.query = opts.get_query_method; + } + + if (query_args.filters || query_args.query) { + opts.get_query = () => query_args; + } + + if (opts.source_doctype) { + const d = new frappe.ui.form.MultiSelectDialog({ doctype: opts.source_doctype, target: opts.target, date_field: opts.date_field || undefined, @@ -703,7 +703,11 @@ erpnext.utils.map_current_doc = function(opts) { _map(); }, }); - } else if(opts.source_name) { + + return d; + } + + if (opts.source_name) { opts.source_name = [opts.source_name]; _map(); } diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 726118d06d1..dfb530b22a0 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -231,15 +231,37 @@ frappe.ui.form.on('Stock Entry', { }, __("Get Items From")); frm.add_custom_button(__('Material Request'), function() { - erpnext.utils.map_current_doc({ + const allowed_request_types = ["Material Transfer", "Material Issue", "Customer Provided"]; + const depends_on_condition = "eval:doc.material_request_type==='Customer Provided'"; + const d = erpnext.utils.map_current_doc({ method: "erpnext.stock.doctype.material_request.material_request.make_stock_entry", source_doctype: "Material Request", target: frm, date_field: "schedule_date", - setters: {}, + setters: [{ + fieldtype: 'Select', + label: __('Purpose'), + options: allowed_request_types.join("\n"), + fieldname: 'material_request_type', + default: "Material Transfer", + mandatory: 1, + change() { + if (this.value === 'Customer Provided') { + d.dialog.get_field("customer").set_focus(); + } + }, + }, + { + fieldtype: 'Link', + label: __('Customer'), + options: 'Customer', + fieldname: 'customer', + depends_on: depends_on_condition, + mandatory_depends_on: depends_on_condition, + }], get_query_filters: { docstatus: 1, - material_request_type: ["in", ["Material Transfer", "Material Issue"]], + material_request_type: ["in", allowed_request_types], status: ["not in", ["Transferred", "Issued"]] } }) From d7ac2394e8287406eb83271830f3952dfd83b506 Mon Sep 17 00:00:00 2001 From: Jannat Patel <31363128+pateljannat@users.noreply.github.com> Date: Tue, 9 Mar 2021 20:41:58 +0530 Subject: [PATCH 47/64] fix: error in bulk attendance (#24806) * fix: error in bulk attendance * fix: no date selected scenario * fix: translation and sider --- erpnext/hr/doctype/attendance/attendance.py | 4 ++ .../hr/doctype/attendance/attendance_list.js | 46 +++++++++++-------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 373b94008e7..18a4fe53c4b 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -131,6 +131,10 @@ def mark_bulk_attendance(data): data = json.loads(data) data = frappe._dict(data) company = frappe.get_value('Employee', data.employee, 'company') + if not data.unmarked_days: + frappe.throw(_("Please select a date.")) + return + for date in data.unmarked_days: doc_dict = { 'doctype': 'Attendance', diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 6df3dbd7845..0c7eafe9c61 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -12,7 +12,7 @@ frappe.listview_settings['Attendance'] = { onload: function(list_view) { let me = this; const months = moment.months() - list_view.page.add_inner_button( __("Mark Attendance"), function(){ + list_view.page.add_inner_button( __("Mark Attendance"), function() { let dialog = new frappe.ui.Dialog({ title: __("Mark Attendance"), fields: [ @@ -22,11 +22,12 @@ frappe.listview_settings['Attendance'] = { fieldtype: 'Link', options: 'Employee', reqd: 1, - onchange: function(){ + onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); dialog.set_df_property("status", "hidden", 1); dialog.set_df_property("month", "value", ''); dialog.set_df_property("unmarked_days", "options", []); + dialog.no_unmarked_days_left = false; } }, { @@ -35,13 +36,18 @@ frappe.listview_settings['Attendance'] = { fieldname: "month", options: months, reqd: 1, - onchange: function(){ + onchange: function() { if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { dialog.set_df_property("status", "hidden", 0); dialog.set_df_property("unmarked_days", "options", []); + dialog.no_unmarked_days_left = false; me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{ - dialog.set_df_property("unmarked_days", "hidden", 0); - dialog.set_df_property("unmarked_days", "options", options); + if (options.length > 0) { + dialog.set_df_property("unmarked_days", "hidden", 0); + dialog.set_df_property("unmarked_days", "options", options); + } else { + dialog.no_unmarked_days_left = true; + } }); } } @@ -64,21 +70,25 @@ frappe.listview_settings['Attendance'] = { hidden: 1 }, ], - primary_action(data){ - frappe.confirm(__('Mark attendance as ' + data.status + ' for ' + data.month +'' + ' on selected dates?'), () => { - frappe.call({ - method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance", - args: { - data : data - }, - callback: function(r) { - if(r.message === 1) { - frappe.show_alert({message:__("Attendance Marked"), indicator:'blue'}); - cur_dialog.hide(); + primary_action(data) { + if (cur_dialog.no_unmarked_days_left) { + frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value])); + } else { + frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => { + frappe.call({ + method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance", + args: { + data: data + }, + callback: function(r) { + if (r.message === 1) { + frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'}); + cur_dialog.hide(); + } } - } + }); }); - }); + } dialog.hide(); list_view.refresh(); }, From db2d1962960bf0cbf85e05fd240826083f33bd7a Mon Sep 17 00:00:00 2001 From: Rohan Date: Tue, 9 Mar 2021 21:03:45 +0530 Subject: [PATCH 48/64] fix: do not send emails to disabled users from Employee Onboarding (#24795) --- erpnext/hr/utils.py | 50 +++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index d57ef5955dc..0c4c1cafb07 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -1,16 +1,19 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals -import frappe, erpnext -from frappe import _ -from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today -from frappe.model.document import Document -from frappe.desk.form import assign_to +import erpnext +import frappe from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee +from frappe import _ +from frappe.desk.form import assign_to +from frappe.model.document import Document +from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate, + get_datetime, getdate, nowdate, today, unique) + class DuplicateDeclarationError(frappe.ValidationError): pass + class EmployeeBoardingController(Document): ''' Create the project and the task for the boarding process @@ -48,27 +51,38 @@ class EmployeeBoardingController(Document): continue task = frappe.get_doc({ - "doctype": "Task", - "project": self.project, - "subject": activity.activity_name + " : " + self.employee_name, - "description": activity.description, - "department": self.department, - "company": self.company, - "task_weight": activity.task_weight - }).insert(ignore_permissions=True) + "doctype": "Task", + "project": self.project, + "subject": activity.activity_name + " : " + self.employee_name, + "description": activity.description, + "department": self.department, + "company": self.company, + "task_weight": activity.task_weight + }).insert(ignore_permissions=True) activity.db_set("task", task.name) + users = [activity.user] if activity.user else [] if activity.role: - user_list = frappe.db.sql_list('''select distinct(parent) from `tabHas Role` - where parenttype='User' and role=%s''', activity.role) - users = users + user_list + user_list = frappe.db.sql_list(''' + SELECT + DISTINCT(has_role.parent) + FROM + `tabHas Role` has_role + LEFT JOIN `tabUser` user + ON has_role.parent = user.name + WHERE + has_role.parenttype = 'User' + AND user.enabled = 1 + AND has_role.role = %s + ''', activity.role) + users = unique(users + user_list) if "Administrator" in users: users.remove("Administrator") # assign the task the users if users: - self.assign_task_to_users(task, set(users)) + self.assign_task_to_users(task, users) def assign_task_to_users(self, task, users): for user in users: From ec829786e909bfe87cdfcd07e7d67bea8e7e4c12 Mon Sep 17 00:00:00 2001 From: Syed Mujeer Hashmi Date: Tue, 9 Mar 2021 21:04:55 +0530 Subject: [PATCH 49/64] fix: Add student category to student applicant (#24779) * fix: Add student category to student applicant Student category will be automatically fetched during program enrollment while importing students into the system. Signed-off-by: Syed Mujeer Hashmi * fix: Allow student category during program enrollment Signed-off-by: Syed Mujeer Hashmi --- erpnext/education/api.py | 1 + .../program_enrollment_tool/program_enrollment_tool.py | 3 ++- .../doctype/student_applicant/student_applicant.json | 9 ++++++++- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/education/api.py b/erpnext/education/api.py index 948e7cc1aed..afa0be9b9f3 100644 --- a/erpnext/education/api.py +++ b/erpnext/education/api.py @@ -36,6 +36,7 @@ def enroll_student(source_name): student.save() program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment.student = student.name + program_enrollment.student_category = student.student_category program_enrollment.student_name = student.title program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) diff --git a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py index 9f8f9f4dc00..8180102c582 100644 --- a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py +++ b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py @@ -30,7 +30,7 @@ class ProgramEnrollmentTool(Document): .format(condition), self.as_dict(), as_dict=1) elif self.get_students_from == "Program Enrollment": condition2 = 'and student_batch_name=%(student_batch)s' if self.student_batch else " " - students = frappe.db.sql('''select student, student_name, student_batch_name from `tabProgram Enrollment` + students = frappe.db.sql('''select student, student_name, student_batch_name, student_category from `tabProgram Enrollment` where program=%(program)s and academic_year=%(academic_year)s {0} {1} and docstatus != 2''' .format(condition, condition2), self.as_dict(), as_dict=1) @@ -57,6 +57,7 @@ class ProgramEnrollmentTool(Document): prog_enrollment = frappe.new_doc("Program Enrollment") prog_enrollment.student = stud.student prog_enrollment.student_name = stud.student_name + prog_enrollment.student_category = stud.student_category prog_enrollment.program = self.new_program prog_enrollment.academic_year = self.new_academic_year prog_enrollment.academic_term = self.new_academic_term diff --git a/erpnext/education/doctype/student_applicant/student_applicant.json b/erpnext/education/doctype/student_applicant/student_applicant.json index 6df9b9a84f9..95f9224a73c 100644 --- a/erpnext/education/doctype/student_applicant/student_applicant.json +++ b/erpnext/education/doctype/student_applicant/student_applicant.json @@ -11,6 +11,7 @@ "middle_name", "last_name", "program", + "student_category", "lms_only", "paid", "column_break_8", @@ -257,12 +258,18 @@ "options": "Student Applicant", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "student_category", + "fieldtype": "Link", + "label": "Student Category", + "options": "Student Category" } ], "image_field": "image", "is_submittable": 1, "links": [], - "modified": "2020-10-05 13:59:45.631647", + "modified": "2021-03-01 23:00:25.119241", "modified_by": "Administrator", "module": "Education", "name": "Student Applicant", From 832098f042bc56b6623ca498e3861f16e3697d58 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Tue, 9 Mar 2021 21:14:25 +0530 Subject: [PATCH 50/64] fix: In-Transit Feature enhancements (#24652) --- erpnext/setup/doctype/company/company.json | 4 ++-- erpnext/stock/doctype/stock_entry/stock_entry.js | 12 +++++++++++- erpnext/stock/doctype/warehouse/warehouse.js | 12 ++++++++++++ erpnext/stock/doctype/warehouse/warehouse.json | 10 +++++++++- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index d49ae7ce8ac..56f60dfcff0 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -725,7 +725,7 @@ { "fieldname": "default_in_transit_warehouse", "fieldtype": "Link", - "label": "Default In Transit Warehouse", + "label": "Default In-Transit Warehouse", "options": "Warehouse" }, { @@ -740,7 +740,7 @@ "image_field": "company_logo", "is_tree": 1, "links": [], - "modified": "2020-12-03 12:27:27.085094", + "modified": "2021-02-16 15:53:37.167589", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index dfb530b22a0..64dcbed1d85 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -591,6 +591,7 @@ frappe.ui.form.on('Stock Entry', { add_to_transit: function(frm) { if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') { + frm.set_value('to_warehouse', ''); frm.set_value('stock_entry_type', 'Material Transfer'); frm.fields_dict.to_warehouse.get_query = function() { return { @@ -601,7 +602,15 @@ frappe.ui.form.on('Stock Entry', { } }; }; - frappe.db.get_value('Company', frm.doc.company, 'default_in_transit_warehouse', (r) => { + frm.trigger('set_tansit_warehouse'); + } + }, + + set_tansit_warehouse: function(frm) { + if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse) { + let dt = frm.doc.from_warehouse ? 'Warehouse' : 'Company'; + let dn = frm.doc.from_warehouse ? frm.doc.from_warehouse : frm.doc.company; + frappe.db.get_value(dt, dn, 'default_in_transit_warehouse', (r) => { if (r.default_in_transit_warehouse) { frm.set_value('to_warehouse', r.default_in_transit_warehouse); } @@ -968,6 +977,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({ }, from_warehouse: function(doc) { + this.frm.trigger('set_tansit_warehouse'); this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse); }, diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js index 1bea00e2632..1f172504a7f 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.js +++ b/erpnext/stock/doctype/warehouse/warehouse.js @@ -3,6 +3,18 @@ frappe.ui.form.on("Warehouse", { + onload: function(frm) { + frm.set_query("default_in_transit_warehouse", function() { + return { + filters:{ + 'warehouse_type' : 'Transit', + 'is_group': 0, + 'company': frm.doc.company + } + }; + }); + }, + refresh: function(frm) { frm.toggle_display('warehouse_name', frm.doc.__islocal); frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json index 1cc600b9ca7..bddb114c9de 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.json +++ b/erpnext/stock/doctype/warehouse/warehouse.json @@ -13,6 +13,7 @@ "column_break_3", "warehouse_type", "parent_warehouse", + "default_in_transit_warehouse", "is_group", "column_break_4", "account", @@ -230,13 +231,20 @@ { "fieldname": "column_break_3", "fieldtype": "Section Break" + }, + { + "depends_on": "eval: doc.warehouse_type !== 'Transit';", + "fieldname": "default_in_transit_warehouse", + "fieldtype": "Link", + "label": "Default In-Transit Warehouse", + "options": "Warehouse" } ], "icon": "fa fa-building", "idx": 1, "is_tree": 1, "links": [], - "modified": "2020-08-03 18:41:52.442502", + "modified": "2021-02-16 17:21:52.380098", "modified_by": "Administrator", "module": "Stock", "name": "Warehouse", From 27bcb2a064b0bbdc284667ba2c2178a16ca32465 Mon Sep 17 00:00:00 2001 From: Afshan <33727827+AfshanKhan@users.noreply.github.com> Date: Tue, 9 Mar 2021 22:25:48 +0530 Subject: [PATCH 51/64] fix: renamed "supplier_warehouse" to "set_from_warehouse" (#24816) * fix: reverting and adding new field for supplier warehouse --- .../purchase_invoice/purchase_invoice.json | 16 ++++++++++++++-- .../purchase_invoice/test_purchase_invoice.py | 4 ++-- erpnext/controllers/buying_controller.py | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index 451c9368816..18b66375e99 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -58,6 +58,7 @@ "rejected_warehouse", "col_break_warehouse", "set_from_warehouse", + "supplier_warehouse", "is_subcontracted", "items_section", "update_stock", @@ -1350,7 +1351,7 @@ "options": "Company" }, { - "depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)", + "depends_on": "eval:doc.update_stock && doc.is_internal_supplier", "description": "Sets 'From Warehouse' in each row of the items table.", "fieldname": "set_from_warehouse", "fieldtype": "Link", @@ -1360,13 +1361,24 @@ "print_hide": 1, "print_width": "50px", "width": "50px" + }, + { + "depends_on": "eval:doc.update_stock && doc.is_subcontracted==\"Yes\"", + "fieldname": "supplier_warehouse", + "fieldtype": "Link", + "label": "Supplier Warehouse", + "no_copy": 1, + "options": "Warehouse", + "print_hide": 1, + "print_width": "50px", + "width": "50px" } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2020-12-26 20:49:03.305063", + "modified": "2021-03-09 21:12:30.422084", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 2c088ce2b20..ded293b88d5 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -456,7 +456,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") - return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, + return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") @@ -1031,7 +1031,7 @@ def make_purchase_invoice_against_cost_center(**args): pi.is_return = args.is_return pi.credit_to = args.return_against or "Creditors - _TC" pi.is_subcontracted = args.is_subcontracted or "No" - if args.supplier_warehouse: + if args.supplier_warehouse: pi.supplier_warehouse = "_Test Warehouse 1 - _TC" pi.append("items", { diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index e4698389963..219d5295c38 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -278,7 +278,7 @@ class BuyingController(StockController): if self.is_subcontracted == "Yes": if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse: - frappe.throw(_("Supplier Warehouse mandatory for sub-contracted Purchase Receipt")) + frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype)) for item in self.get("items"): if item in self.sub_contracted_items and not item.bom: From 894cff5aa47efab72d29bd18905a087a15b50f85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Rold=C3=A1n?= Date: Tue, 9 Mar 2021 15:05:24 -0300 Subject: [PATCH 52/64] fix: lms program and index missing context variables when is no data available (#24828) * fix: program and index * fix: moved default value to get_course_progress --- erpnext/www/lms/index.py | 2 +- erpnext/www/lms/program.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py index 00f66e72c3e..26f59a2395e 100644 --- a/erpnext/www/lms/index.py +++ b/erpnext/www/lms/index.py @@ -13,4 +13,4 @@ def get_context(context): def get_featured_programs(): - return utils.get_portal_programs() \ No newline at end of file + return utils.get_portal_programs() or [] \ No newline at end of file diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py index d3b04c2f8f6..104d3fa315a 100644 --- a/erpnext/www/lms/program.py +++ b/erpnext/www/lms/program.py @@ -26,4 +26,4 @@ def get_program(program_name): def get_course_progress(courses, program): progress = {course.name: utils.get_course_progress(course, program) for course in courses} - return progress \ No newline at end of file + return progress or {} \ No newline at end of file From 358153b04b81e1fbf3fc92bae06369a31509a7f3 Mon Sep 17 00:00:00 2001 From: David Angulo Date: Tue, 9 Mar 2021 13:19:12 -0600 Subject: [PATCH 53/64] fix sider --- erpnext/healthcare/doctype/patient/patient.js | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js index 15e203a824f..49478cc3608 100644 --- a/erpnext/healthcare/doctype/patient/patient.js +++ b/erpnext/healthcare/doctype/patient/patient.js @@ -49,6 +49,7 @@ frappe.ui.form.on('Patient', { if(!frm.doc.dob){ $(frm.fields_dict['age_html'].wrapper).html(''); } + if(frm.doc.dob){ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`); } From 06e99dfe7b1619aaced3157cc24785a0b845d5b5 Mon Sep 17 00:00:00 2001 From: David Angulo Date: Tue, 9 Mar 2021 13:23:48 -0600 Subject: [PATCH 54/64] fix sider --- erpnext/healthcare/doctype/patient/patient.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js index 49478cc3608..ff237d38243 100644 --- a/erpnext/healthcare/doctype/patient/patient.js +++ b/erpnext/healthcare/doctype/patient/patient.js @@ -46,11 +46,10 @@ frappe.ui.form.on('Patient', { } }, onload: function (frm) { - if(!frm.doc.dob){ + if(!frm.doc.dob) { $(frm.fields_dict['age_html'].wrapper).html(''); } - - if(frm.doc.dob){ + if(frm.doc.dob) { $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`); } } From fe0f46c5b3cfd6175872e0764c247beb25e3db4e Mon Sep 17 00:00:00 2001 From: David Angulo Date: Tue, 9 Mar 2021 13:28:08 -0600 Subject: [PATCH 55/64] fix sider --- erpnext/healthcare/doctype/patient/patient.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js index ff237d38243..bce42e51d07 100644 --- a/erpnext/healthcare/doctype/patient/patient.js +++ b/erpnext/healthcare/doctype/patient/patient.js @@ -46,10 +46,10 @@ frappe.ui.form.on('Patient', { } }, onload: function (frm) { - if(!frm.doc.dob) { + if (!frm.doc.dob) { $(frm.fields_dict['age_html'].wrapper).html(''); } - if(frm.doc.dob) { + if (frm.doc.dob) { $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`); } } From 20cc2bb0d84ba1f3d8f8101bdb6182511987ff27 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Wed, 10 Mar 2021 05:54:43 +0100 Subject: [PATCH 56/64] fix: autoname for Item Tax Template * fix: autoname for Item Tax Template Use title + company abbreviation instead of just title. * fix: test records of Item Tax Template * fix: rename item tax templates in test records * Revert "fix: test records of Item Tax Template" This reverts commit 53875c54ffe059c32d52b9ee60795d56f95473b1. * fix: rename hardcoded item tax templates in tests * fix: rename hardcoded Item Tax Templates in tests (2) * fix: delete Item Tax Template with company --- .../item_tax_template/item_tax_template.json | 10 ++++--- .../item_tax_template/item_tax_template.py | 5 ++++ .../doctype/pos_invoice/test_pos_invoice.py | 8 +++--- .../purchase_invoice/test_records.json | 2 +- .../doctype/sales_invoice/test_records.json | 4 +-- .../sales_invoice/test_sales_invoice.py | 12 ++++----- .../purchase_order/test_purchase_order.py | 6 ++--- .../doctype/sales_order/test_sales_order.py | 6 ++--- erpnext/setup/doctype/company/company.py | 2 ++ .../doctype/item_group/test_records.json | 6 ++--- erpnext/stock/doctype/item/test_item.py | 26 +++++++++---------- erpnext/stock/doctype/item/test_records.json | 10 +++---- 12 files changed, 53 insertions(+), 44 deletions(-) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index 8915f79b926..77c9e95b759 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -1,7 +1,7 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, - "autoname": "field:title", "creation": "2018-11-22 22:45:00.370913", "doctype": "DocType", "document_type": "Setup", @@ -20,8 +20,7 @@ "in_list_view": 1, "label": "Title", "no_copy": 1, - "reqd": 1, - "unique": 1 + "reqd": 1 }, { "fieldname": "taxes", @@ -33,12 +32,14 @@ { "fieldname": "company", "fieldtype": "Link", + "in_list_view": 1, "label": "Company", "options": "Company", "reqd": 1 } ], - "modified": "2020-09-18 17:26:09.703215", + "links": [], + "modified": "2021-03-08 19:50:21.416513", "modified_by": "Administrator", "module": "Accounts", "name": "Item Tax Template", @@ -81,5 +82,6 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "title_field": "title", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py index e77481d44f5..d9155cbab4a 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py @@ -11,6 +11,11 @@ class ItemTaxTemplate(Document): def validate(self): self.validate_tax_accounts() + def autoname(self): + if self.company and self.title: + abbr = frappe.get_cached_value('Company', self.company, 'abbr') + self.name = '{0} - {1}'.format(self.title, abbr) + def validate_tax_accounts(self): """Check whether Tax Rate is not entered twice for same Tax Type""" check_list = [] diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 15875afe878..eb52fd62759 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -99,10 +99,10 @@ class TestPOSInvoice(unittest.TestCase): item_row = inv.get("items")[0] add_items = [ - (54, '_Test Account Excise Duty @ 12'), - (288, '_Test Account Excise Duty @ 15'), - (144, '_Test Account Excise Duty @ 20'), - (430, '_Test Item Tax Template 1') + (54, '_Test Account Excise Duty @ 12 - _TC'), + (288, '_Test Account Excise Duty @ 15 - _TC'), + (144, '_Test Account Excise Duty @ 20 - _TC'), + (430, '_Test Item Tax Template 1 - _TC') ] for qty, item_tax_template in add_items: item_row_copy = copy.deepcopy(item_row) diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json index 7030faf2b73..e7166c5a12d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_records.json +++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json @@ -18,7 +18,7 @@ "expense_account": "_Test Account Cost for Goods Sold - _TC", "item_code": "_Test Item Home Desktop 100", "item_name": "_Test Item Home Desktop 100", - "item_tax_template": "_Test Account Excise Duty @ 10", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", "parentfield": "items", "qty": 10, "rate": 50, diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json index ee6419db20a..e00a58f8641 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_records.json +++ b/erpnext/accounts/doctype/sales_invoice/test_records.json @@ -148,7 +148,7 @@ "expense_account": "_Test Account Cost for Goods Sold - _TC", "item_code": "_Test Item Home Desktop 100", "item_name": "_Test Item Home Desktop 100", - "item_tax_template": "_Test Account Excise Duty @ 10", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", "parentfield": "items", "price_list_rate": 50, "qty": 10, @@ -276,7 +276,7 @@ "expense_account": "_Test Account Cost for Goods Sold - _TC", "item_code": "_Test Item Home Desktop 100", "item_name": "_Test Item Home Desktop 100", - "item_tax_template": "_Test Account Excise Duty @ 10", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", "parentfield": "items", "price_list_rate": 62.5, "qty": 10, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 7cd1828343b..1b9557839fe 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -405,10 +405,10 @@ class TestSalesInvoice(unittest.TestCase): item_row = si.get("items")[0] add_items = [ - (54, '_Test Account Excise Duty @ 12'), - (288, '_Test Account Excise Duty @ 15'), - (144, '_Test Account Excise Duty @ 20'), - (430, '_Test Item Tax Template 1') + (54, '_Test Account Excise Duty @ 12 - _TC'), + (288, '_Test Account Excise Duty @ 15 - _TC'), + (144, '_Test Account Excise Duty @ 20 - _TC'), + (430, '_Test Item Tax Template 1 - _TC') ] for qty, item_tax_template in add_items: item_row_copy = copy.deepcopy(item_row) @@ -2077,14 +2077,14 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): item.save() item.append("taxes", { - "item_tax_template": "_Test Item Tax Template 1", + "item_tax_template": "_Test Item Tax Template 1 - _TC", "valid_from": add_days(nowdate(), 1) }) item.save() sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1) - sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1" + sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1 - _TC" self.assertRaises(frappe.ValidationError, sales_invoice.save) item.taxes = [] diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index d568ef1ceda..02d48653203 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -231,12 +231,12 @@ class TestPurchaseOrder(unittest.TestCase): new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template", + "item_tax_template": "Test Update Items Template - _TC", "valid_from": nowdate() }) new_item_with_tax.save() - tax_template = "_Test Account Excise Duty @ 10" + tax_template = "_Test Account Excise Duty @ 10 - _TC" item = "_Test Item Home Desktop 100" if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): item_doc = frappe.get_doc("Item", item) @@ -287,7 +287,7 @@ class TestPurchaseOrder(unittest.TestCase): po.cancel() po.delete() new_item_with_tax.delete() - frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() + frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete() def test_update_child_uom_conv_factor_change(self): po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py index 52a0174798e..ee16f441715 100644 --- a/erpnext/selling/doctype/sales_order/test_sales_order.py +++ b/erpnext/selling/doctype/sales_order/test_sales_order.py @@ -555,12 +555,12 @@ class TestSalesOrder(unittest.TestCase): new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") new_item_with_tax.append("taxes", { - "item_tax_template": "Test Update Items Template", + "item_tax_template": "Test Update Items Template - _TC", "valid_from": nowdate() }) new_item_with_tax.save() - tax_template = "_Test Account Excise Duty @ 10" + tax_template = "_Test Account Excise Duty @ 10 - _TC" item = "_Test Item Home Desktop 100" if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): item_doc = frappe.get_doc("Item", item) @@ -614,7 +614,7 @@ class TestSalesOrder(unittest.TestCase): so.cancel() so.delete() new_item_with_tax.delete() - frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() + frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete() frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value) def test_warehouse_user(self): diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 819ba78e666..433851cde53 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -390,8 +390,10 @@ class Company(NestedSet): frappe.db.sql("delete from tabDepartment where company=%s", self.name) frappe.db.sql("delete from `tabTax Withholding Account` where company=%s", self.name) + # delete tax templates frappe.db.sql("delete from `tabSales Taxes and Charges Template` where company=%s", self.name) frappe.db.sql("delete from `tabPurchase Taxes and Charges Template` where company=%s", self.name) + frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name) @frappe.whitelist() def enqueue_replace_abbr(company, old, new): diff --git a/erpnext/setup/doctype/item_group/test_records.json b/erpnext/setup/doctype/item_group/test_records.json index 71159643209..146da87bddc 100644 --- a/erpnext/setup/doctype/item_group/test_records.json +++ b/erpnext/setup/doctype/item_group/test_records.json @@ -79,13 +79,13 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10", + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", "tax_category": "" }, { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 12", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", "tax_category": "_Test Tax Category 1" } ] @@ -99,7 +99,7 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 15", + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC", "tax_category": "" } ] diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py index 109731abb53..36d0de1e5df 100644 --- a/erpnext/stock/doctype/item/test_item.py +++ b/erpnext/stock/doctype/item/test_item.py @@ -104,41 +104,41 @@ class TestItem(unittest.TestCase): def test_item_tax_template(self): expected_item_tax_template = [ {"item_code": "_Test Item With Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10"}, + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12"}, + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, {"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 10"}, + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12"}, + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 15"}, + "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Account Excise Duty @ 12"}, + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"}, {"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "", - "item_tax_template": "_Test Account Excise Duty @ 20"}, + "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"}, {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Item Tax Template 1"}, + "item_tax_template": "_Test Item Tax Template 1 - _TC"}, {"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2", "item_tax_template": None}, ] expected_item_tax_map = { None: {}, - "_Test Account Excise Duty @ 10": {"_Test Account Excise Duty - _TC": 10}, - "_Test Account Excise Duty @ 12": {"_Test Account Excise Duty - _TC": 12}, - "_Test Account Excise Duty @ 15": {"_Test Account Excise Duty - _TC": 15}, - "_Test Account Excise Duty @ 20": {"_Test Account Excise Duty - _TC": 20}, - "_Test Item Tax Template 1": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10, + "_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10}, + "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12}, + "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15}, + "_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20}, + "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10, "_Test Account S&H Education Cess - _TC": 15} } diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json index 8f437b13f0d..909c4eeb906 100644 --- a/erpnext/stock/doctype/item/test_records.json +++ b/erpnext/stock/doctype/item/test_records.json @@ -92,7 +92,7 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10" + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC" } ], "stock_uom": "_Test UOM 1" @@ -370,12 +370,12 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 10" + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC" }, { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 12", + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", "tax_category": "_Test Tax Category 1" } ] @@ -449,13 +449,13 @@ { "doctype": "Item Tax", "parentfield": "taxes", - "item_tax_template": "_Test Account Excise Duty @ 20" + "item_tax_template": "_Test Account Excise Duty @ 20 - _TC" }, { "doctype": "Item Tax", "parentfield": "taxes", "tax_category": "_Test Tax Category 1", - "item_tax_template": "_Test Item Tax Template 1" + "item_tax_template": "_Test Item Tax Template 1 - _TC" } ] }, From 77eaf2939ab6f0a16ff9010011e22fd3db8e34d9 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 10 Mar 2021 12:04:54 +0530 Subject: [PATCH 57/64] fix: rate of stock uom division by zero error (#24829) --- erpnext/controllers/stock_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index e0031c9c699..11ac703311b 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -406,7 +406,8 @@ class StockController(AccountsController): def set_rate_of_stock_uom(self): if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: for d in self.get("items"): - d.stock_uom_rate = d.rate / d.conversion_factor + if d.conversion_factor: + d.stock_uom_rate = d.rate / d.conversion_factor def validate_internal_transfer(self): if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ From a0e7787f7b73f433018e3091097d1cb83765c974 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Wed, 10 Mar 2021 13:46:42 +0530 Subject: [PATCH 58/64] fix: removed show cancelled entries checkbox from the stock ledger report (#24826) --- erpnext/stock/report/stock_ledger/stock_ledger.js | 5 ----- erpnext/stock/report/stock_ledger/stock_ledger.py | 5 +---- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js index 6f12c2731bb..fe2417bba7e 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.js +++ b/erpnext/stock/report/stock_ledger/stock_ledger.js @@ -82,11 +82,6 @@ frappe.query_reports["Stock Ledger"] = { "label": __("Include UOM"), "fieldtype": "Link", "options": "UOM" - }, - { - "fieldname": "show_cancelled_entries", - "label": __("Show Cancelled Entries"), - "fieldtype": "Check" } ], "formatter": function (value, row, column, data, default_formatter) { diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index 7b5701a9932..36996e96745 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -138,7 +138,7 @@ def get_stock_ledger_entries(filters, items): `tabStock Ledger Entry` sle WHERE company = %(company)s - AND posting_date BETWEEN %(from_date)s AND %(to_date)s + AND is_cancelled = 0 AND posting_date BETWEEN %(from_date)s AND %(to_date)s {sle_conditions} {item_conditions_sql} ORDER BY @@ -209,9 +209,6 @@ def get_sle_conditions(filters): if filters.get("project"): conditions.append("project=%(project)s") - if not filters.get("show_cancelled_entries"): - conditions.append("is_cancelled = 0") - return "and {}".format(" and ".join(conditions)) if conditions else "" From 000fa824d3092aa851be73a3ef9116e7af71cbe3 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Wed, 10 Mar 2021 13:31:05 +0530 Subject: [PATCH 59/64] fix: use account_name only in consolidated report Don't use account_number and only rely on account_name for preparing consolidated financial statement. Related issue: ISS-20-21-10217 --- .../consolidated_financial_statement.py | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 76f3c50578e..0c4a4224407 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -240,8 +240,7 @@ def get_company_currency(filters=None): def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters): for entries in gl_entries_by_account.values(): for entry in entries: - key = entry.account_number or entry.account_name - d = accounts_by_name.get(key) + d = accounts_by_name.get(entry.account_name) if d: for company in companies: # check if posting date is within the period @@ -256,7 +255,8 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - account = d.parent_account.split(' - ')[0].strip() + account = d.parent_account_name + if not accounts_by_name.get(account): continue @@ -267,16 +267,34 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): accounts_by_name[account]["opening_balance"] = \ accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) + def get_account_heads(root_type, companies, filters): accounts = get_accounts(root_type, filters) if not accounts: return None, None + accounts = update_parent_account_names(accounts) + accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) return accounts, accounts_by_name +def update_parent_account_names(accounts): + """Update parent_account_name in accounts list. + + parent_name is `name` of parent account which could have other prefix + of account_number and suffix of company abbr. This function adds key called + `parent_account_name` which does not have such prefix/suffix. + """ + name_to_account_map = { d.name : d.account_name for d in accounts } + + for account in accounts: + if account.parent_account: + account["parent_account_name"] = name_to_account_map[account.parent_account] + + return accounts + def get_companies(filters): companies = {} all_companies = get_subsidiary_companies(filters.get('company')) @@ -381,9 +399,9 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) for entry in gl_entries: - key = entry.account_number or entry.account_name - validate_entries(key, entry, accounts_by_name, accounts) - gl_entries_by_account.setdefault(key, []).append(entry) + account_name = entry.account_name + validate_entries(account_name, entry, accounts_by_name, accounts) + gl_entries_by_account.setdefault(account_name, []).append(entry) return gl_entries_by_account @@ -452,8 +470,7 @@ def filter_accounts(accounts, depth=10): parent_children_map = {} accounts_by_name = {} for d in accounts: - key = d.account_number or d.account_name - accounts_by_name[key] = d + accounts_by_name[d.account_name] = d parent_children_map.setdefault(d.parent_account or None, []).append(d) filtered_accounts = [] From c7fff34cf5930f5b81f9eb6734e429e162ba4555 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 10 Mar 2021 15:51:44 +0530 Subject: [PATCH 60/64] fix(travis): Customer Testcase Failing (#24833) --- erpnext/selling/doctype/customer/test_customer.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index 87fdaa366f1..7761aa70fb2 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -54,7 +54,11 @@ class TestCustomer(unittest.TestCase): details = get_party_details("_Test Customer") for key, value in iteritems(to_check): - self.assertEqual(value, details.get(key)) + val = details.get(key) + if not val and not isinstance(val, list): + val = None + + self.assertEqual(value, val) def test_party_details_tax_category(self): from erpnext.accounts.party import get_party_details From 6f0fabc54e09d09206b210a639f6434d1d920da9 Mon Sep 17 00:00:00 2001 From: Anupam Kumar Date: Wed, 10 Mar 2021 18:36:46 +0530 Subject: [PATCH 61/64] fix: gle issue in internal transfer(Purchase) (#24830) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index dacd50a3e24..5c4e32e493e 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -968,7 +968,7 @@ class PurchaseInvoice(BuyingController): # base_rounding_adjustment may become zero due to small precision # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 # then base_rounding_adjustment becomes zero and error is thrown in GL Entry - if self.rounding_adjustment and self.base_rounding_adjustment: + if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment: round_off_account, round_off_cost_center = \ get_round_off_account_and_cost_center(self.company) From a780306ec765312f393f914f0adfdcaf0949e572 Mon Sep 17 00:00:00 2001 From: casesolved-co-uk Date: Tue, 16 Feb 2021 05:48:50 +0000 Subject: [PATCH 62/64] fix: Don't throw exception on invoice lines when there is no item_code (fixes #24640) --- erpnext/public/js/controllers/transaction.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index e5f90490176..9351f6d2066 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1885,7 +1885,6 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ frappe.throw(__("Please enter Item Code to get batch no")); } else if (doc.doctype == "Purchase Receipt" || (doc.doctype == "Purchase Invoice" && doc.update_stock)) { - return { filters: {'item': item.item_code} } @@ -1911,9 +1910,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ set_query_for_item_tax_template: function(doc, cdt, cdn) { var item = frappe.get_doc(cdt, cdn); if(!item.item_code) { - frappe.throw(__("Please enter Item Code to get item taxes")); + return doc.company ? {filters: {company: doc.company}} : {}; } else { - let filters = { 'item_code': item.item_code, 'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date], @@ -2124,4 +2122,4 @@ erpnext.apply_putaway_rule = (frm, purpose=null) => { } } }); -}; \ No newline at end of file +}; From be2c1fca7b97a3edb2d98fbdaee7b5b5d9d81d62 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 11 Mar 2021 13:19:44 +0530 Subject: [PATCH 63/64] feat(Non Profit): 80G Certificates and Donations (#24657) * feat: 80G Certificates * feat: add non-profit custom fields for India * feat: 80G Certificate print format for memberships * feat: Donation doctype and API endpoint to capture donations via razorpay * chore: Rename Membership Settings to Non Profit Settings * chore: clean up Non Profit Settings - Rename fields, better labels - patch for renaming * feat: Webhook secret generation for Razorpay donations in Non-Profit Settings * feat: Payment Entry for donations - added Donor as Party Type - setting for automating payment entries for donations created via web form * fix: linter and sider issues * fix: translation syntax * feat: PAN Details custom field for Indian donors * feat: 80G certificates for Donations with Print Format * fix: sider * feat: validations for donor and donation - create donor for website user from donations - validate donor email * feat: extract member name from subscription notes * test: Donation * test: Tax Exemption 80G Certificate * chore: styling fixes * fix: tests * fix: sider * feat: extract PAN number from additional subscription notes * feat: Add creation user field in Non Profit Settings fix: Payment Entry not generating for memberships and donations * feat: update desk page * fix: tests --- .../doctype/payment_entry/payment_entry.js | 26 +- .../doctype/payment_entry/payment_entry.py | 23 +- .../__init__.py | 0 .../non_profit/doctype/donation/donation.js | 26 ++ .../non_profit/doctype/donation/donation.json | 156 +++++++++ .../non_profit/doctype/donation/donation.py | 215 +++++++++++++ .../doctype/donation/donation_dashboard.py | 16 + .../doctype/donation/test_donation.py | 76 +++++ erpnext/non_profit/doctype/donor/donor.json | 9 +- erpnext/non_profit/doctype/donor/donor.py | 5 + erpnext/non_profit/doctype/member/member.js | 2 +- erpnext/non_profit/doctype/member/member.py | 11 +- .../doctype/membership/membership.js | 4 +- .../doctype/membership/membership.json | 10 +- .../doctype/membership/membership.py | 95 ++++-- .../doctype/membership/test_membership.py | 63 ++-- .../membership_settings.json | 192 ----------- .../membership_settings.py | 33 -- .../membership_type/membership_type.js | 4 +- .../doctype/non_profit_settings/__init__.py | 0 .../non_profit_settings.js} | 73 +++-- .../non_profit_settings.json | 273 ++++++++++++++++ .../non_profit_settings.py | 36 +++ .../test_non_profit_settings.py} | 2 +- .../workspace/non_profit/non_profit.json | 37 ++- erpnext/patches.txt | 2 + ...bership_settings_to_non_profit_settings.py | 22 ++ ...fields_for_80g_certificate_and_donation.py | 16 + .../tax_exemption_80g_certificate/__init__.py | 0 .../tax_exemption_80g_certificate.js | 67 ++++ .../tax_exemption_80g_certificate.json | 297 ++++++++++++++++++ .../tax_exemption_80g_certificate.py | 89 ++++++ .../test_tax_exemption_80g_certificate.py | 101 ++++++ .../__init__.py | 0 .../tax_exemption_80g_certificate_detail.json | 66 ++++ .../tax_exemption_80g_certificate_detail.py | 10 + erpnext/regional/india/setup.py | 26 +- .../80g_certificate_for_donation.json | 26 ++ .../80g_certificate_for_donation/__init__.py | 0 .../80g_certificate_for_membership.json | 26 ++ .../__init__.py | 0 .../operations/install_fixtures.py | 1 + 42 files changed, 1801 insertions(+), 335 deletions(-) rename erpnext/non_profit/doctype/{membership_settings => donation}/__init__.py (100%) create mode 100644 erpnext/non_profit/doctype/donation/donation.js create mode 100644 erpnext/non_profit/doctype/donation/donation.json create mode 100644 erpnext/non_profit/doctype/donation/donation.py create mode 100644 erpnext/non_profit/doctype/donation/donation_dashboard.py create mode 100644 erpnext/non_profit/doctype/donation/test_donation.py delete mode 100644 erpnext/non_profit/doctype/membership_settings/membership_settings.json delete mode 100644 erpnext/non_profit/doctype/membership_settings/membership_settings.py create mode 100644 erpnext/non_profit/doctype/non_profit_settings/__init__.py rename erpnext/non_profit/doctype/{membership_settings/membership_settings.js => non_profit_settings/non_profit_settings.js} (50%) create mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json create mode 100644 erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py rename erpnext/non_profit/doctype/{membership_settings/test_membership_settings.py => non_profit_settings/test_non_profit_settings.py} (79%) create mode 100644 erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py create mode 100644 erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json create mode 100644 erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py create mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json create mode 100644 erpnext/regional/print_format/80g_certificate_for_donation/__init__.py create mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json create mode 100644 erpnext/regional/print_format/80g_certificate_for_membership/__init__.py diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index f5c488d0f97..6412772073c 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -92,14 +92,16 @@ frappe.ui.form.on('Payment Entry', { }); frm.set_query("reference_doctype", "references", function() { - if (frm.doc.party_type=="Customer") { + if (frm.doc.party_type == "Customer") { var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; - } else if (frm.doc.party_type=="Supplier") { + } else if (frm.doc.party_type == "Supplier") { var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; - } else if (frm.doc.party_type=="Employee") { + } else if (frm.doc.party_type == "Employee") { var doctypes = ["Expense Claim", "Journal Entry"]; - } else if (frm.doc.party_type=="Student") { + } else if (frm.doc.party_type == "Student") { var doctypes = ["Fees"]; + } else if (frm.doc.party_type == "Donor") { + var doctypes = ["Donation"]; } else { var doctypes = ["Journal Entry"]; } @@ -128,7 +130,7 @@ frappe.ui.form.on('Payment Entry', { const child = locals[cdt][cdn]; const filters = {"docstatus": 1, "company": doc.company}; const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', - 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; + 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation']; if (in_list(party_type_doctypes, child.reference_doctype)) { filters[doc.party_type.toLowerCase()] = doc.party; @@ -281,7 +283,7 @@ frappe.ui.form.on('Payment Entry', { let party_types = Object.keys(frappe.boot.party_account_types); if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){ frm.set_value("party_type", ""); - frappe.throw(__("Party can only be one of "+ party_types.join(", "))); + frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")])); } frm.set_query("party", function() { @@ -705,7 +707,8 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") ) { if(total_positive_outstanding > total_negative_outstanding) if (!frm.doc.paid_amount) @@ -748,7 +751,8 @@ frappe.ui.form.on('Payment Entry', { (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") || (frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") || - (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") || + (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor") ) { if(total_positive_outstanding_including_order > paid_amount) { var remaining_outstanding = total_positive_outstanding_including_order - paid_amount; @@ -905,6 +909,12 @@ frappe.ui.form.on('Payment Entry', { frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx])); return false; } + + if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") { + frappe.model.set_value(row.doctype, row.name, "reference_doctype", null); + frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx])); + return false; + } } if (row) { diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 61858b33393..97a89b66c73 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -72,6 +72,7 @@ class PaymentEntry(AccountsController): self.update_outstanding_amounts() self.update_advance_paid() self.update_expense_claim() + self.update_donation() self.update_payment_schedule() self.set_status() @@ -82,6 +83,7 @@ class PaymentEntry(AccountsController): self.update_outstanding_amounts() self.update_advance_paid() self.update_expense_claim() + self.update_donation(cancel=1) self.delink_advance_entry_references() self.update_payment_schedule(cancel=1) self.set_payment_req_status() @@ -245,6 +247,8 @@ class PaymentEntry(AccountsController): valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") elif self.party_type == "Shareholder": valid_reference_doctypes = ("Journal Entry") + elif self.party_type == "Donor": + valid_reference_doctypes = ("Donation") for d in self.get("references"): if not d.allocated_amount: @@ -618,6 +622,13 @@ class PaymentEntry(AccountsController): doc = frappe.get_doc("Expense Claim", d.reference_name) update_reimbursed_amount(doc, self.name) + def update_donation(self, cancel=0): + if self.payment_type == "Receive" and self.party_type == "Donor" and self.party: + for d in self.get("references"): + if d.reference_doctype=="Donation" and d.reference_name: + is_paid = 0 if cancel else 1 + frappe.db.set_value("Donation", d.reference_name, "paid", is_paid) + def on_recurring(self, reference_doc, auto_repeat_doc): self.reference_no = reference_doc.name self.reference_date = nowdate() @@ -917,6 +928,9 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre total_amount = ref_doc.get("grand_total") exchange_rate = 1 outstanding_amount = ref_doc.get("outstanding_amount") + elif reference_doctype == "Donation": + total_amount = ref_doc.get("amount") + exchange_rate = 1 elif reference_doctype == "Dunning": total_amount = ref_doc.get("dunning_amount") exchange_rate = 1 @@ -1166,8 +1180,10 @@ def set_party_type(dt): party_type = "Supplier" elif dt in ("Expense Claim", "Employee Advance"): party_type = "Employee" - elif dt in ("Fees"): + elif dt == "Fees": party_type = "Student" + elif dt == "Donation": + party_type = "Donor" return party_type def set_party_account(dt, dn, doc, party_type): @@ -1193,7 +1209,7 @@ def set_party_account_currency(dt, party_account, doc): return party_account_currency def set_payment_type(dt, doc): - if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ + if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): payment_type = "Receive" else: @@ -1226,6 +1242,9 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre elif dt == "Dunning": grand_total = doc.grand_total outstanding_amount = doc.grand_total + elif dt == "Donation": + grand_total = doc.amount + outstanding_amount = doc.amount else: if party_account_currency == doc.company_currency: grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) diff --git a/erpnext/non_profit/doctype/membership_settings/__init__.py b/erpnext/non_profit/doctype/donation/__init__.py similarity index 100% rename from erpnext/non_profit/doctype/membership_settings/__init__.py rename to erpnext/non_profit/doctype/donation/__init__.py diff --git a/erpnext/non_profit/doctype/donation/donation.js b/erpnext/non_profit/doctype/donation/donation.js new file mode 100644 index 00000000000..10e82201440 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation.js @@ -0,0 +1,26 @@ +// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Donation', { + refresh: function(frm) { + if (frm.doc.docstatus === 1 && !frm.doc.paid) { + frm.add_custom_button(__('Create Payment Entry'), function() { + frm.events.make_payment_entry(frm); + }); + } + }, + + make_payment_entry: function(frm) { + return frappe.call({ + method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry', + args: { + 'dt': frm.doc.doctype, + 'dn': frm.doc.name + }, + callback: function(r) { + var doc = frappe.model.sync(r.message); + frappe.set_route('Form', doc[0].doctype, doc[0].name); + } + }); + }, +}); diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json new file mode 100644 index 00000000000..6759569d54d --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation.json @@ -0,0 +1,156 @@ +{ + "actions": [], + "autoname": "naming_series:", + "creation": "2021-02-17 10:28:52.645731", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "naming_series", + "donor", + "donor_name", + "email", + "column_break_4", + "company", + "date", + "payment_details_section", + "paid", + "amount", + "mode_of_payment", + "razorpay_payment_id", + "amended_from" + ], + "fields": [ + { + "fieldname": "donor", + "fieldtype": "Link", + "label": "Donor", + "options": "Donor", + "reqd": 1 + }, + { + "fetch_from": "donor.donor_name", + "fieldname": "donor_name", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Donor Name", + "read_only": 1 + }, + { + "fetch_from": "donor.email", + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Email", + "read_only": 1 + }, + { + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, + { + "fieldname": "date", + "fieldtype": "Date", + "label": "Date", + "reqd": 1 + }, + { + "fieldname": "payment_details_section", + "fieldtype": "Section Break", + "label": "Payment Details" + }, + { + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "reqd": 1 + }, + { + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "label": "Mode of Payment", + "options": "Mode of Payment" + }, + { + "fieldname": "razorpay_payment_id", + "fieldtype": "Data", + "label": "Razorpay Payment ID", + "read_only": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Naming Series", + "options": "NPO-DTN-.YYYY.-" + }, + { + "default": "0", + "fieldname": "paid", + "fieldtype": "Check", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Paid" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "label": "Amended From", + "no_copy": 1, + "options": "Donation", + "print_hide": 1, + "read_only": 1 + } + ], + "index_web_pages_for_search": 1, + "is_submittable": 1, + "links": [], + "modified": "2021-03-11 10:53:11.269005", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Donation", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Non Profit Manager", + "select": 1, + "share": 1, + "submit": 1, + "write": 1 + } + ], + "search_fields": "donor_name, email", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "donor_name", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py new file mode 100644 index 00000000000..e947588482d --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +import six +import json +from frappe.model.document import Document +from frappe import _ +from frappe.utils import getdate, flt, get_link_to_form +from frappe.email import sendmail_to_system_managers +from erpnext.non_profit.doctype.membership.membership import verify_signature + +class Donation(Document): + def validate(self): + if not self.donor or not frappe.db.exists('Donor', self.donor): + # for web forms + user_type = frappe.db.get_value('User', frappe.session.user, 'user_type') + if user_type == 'Website User': + self.create_donor_for_website_user() + else: + frappe.throw(_('Please select a Member')) + + def create_donor_for_website_user(self): + donor_name = frappe.get_value('Donor', dict(email=frappe.session.user)) + + if not donor_name: + user = frappe.get_doc('User', frappe.session.user) + donor = frappe.get_doc(dict( + doctype='Donor', + donor_type=self.get('donor_type'), + email=frappe.session.user, + member_name=user.get_fullname() + )).insert(ignore_permissions=True) + donor_name = donor.name + + if self.get('__islocal'): + self.donor = donor_name + + def on_payment_authorized(self, *args, **kwargs): + self.load_from_db() + self.create_payment_entry() + + def create_payment_entry(self): + settings = frappe.get_doc('Non Profit Settings') + if not settings.automate_donation_payment_entries: + return + + if not settings.donation_payment_account: + frappe.throw(_('You need to set Payment Account for Donation in {0}').format( + get_link_to_form('Non Profit Settings', 'Non Profit Settings'))) + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + frappe.flags.ignore_account_permission = True + pe = get_payment_entry(dt=self.doctype, dn=self.name) + frappe.flags.ignore_account_permission = False + pe.paid_from = settings.donation_debit_account + pe.paid_to = settings.donation_payment_account + pe.reference_no = self.name + pe.reference_date = getdate() + pe.flags.ignore_mandatory = True + pe.insert() + pe.submit() + + +@frappe.whitelist(allow_guest=True) +def capture_razorpay_donations(*args, **kwargs): + """ + Creates Donation from Razorpay Webhook Request Data on payment.captured event + Creates Donor from email if not found + """ + data = frappe.request.get_data(as_text=True) + + try: + verify_signature(data, endpoint='Donation') + except Exception as e: + log = frappe.log_error(e, 'Donation Webhook Verification Error') + notify_failure(log) + return { 'status': 'Failed', 'reason': e } + + if isinstance(data, six.string_types): + data = json.loads(data) + data = frappe._dict(data) + + payment = data.payload.get('payment', {}).get('entity', {}) + payment = frappe._dict(payment) + + try: + if not data.event == 'payment.captured': + return + + donor = get_donor(payment.email) + if not donor: + donor = create_donor(payment) + + donation = create_donation(donor, payment) + donation.run_method('create_payment_entry') + + except Exception as e: + message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id) + log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name)) + notify_failure(log) + return { 'status': 'Failed', 'reason': e } + + return { 'status': 'Success' } + + +def create_donation(donor, payment): + if not frappe.db.exists('Mode of Payment', payment.method): + create_mode_of_payment(payment.method) + + company = get_company_for_donations() + donation = frappe.get_doc({ + 'doctype': 'Donation', + 'company': company, + 'donor': donor.name, + 'donor_name': donor.donor_name, + 'email': donor.email, + 'date': getdate(), + 'amount': flt(payment.amount), + 'mode_of_payment': payment.method, + 'razorpay_payment_id': payment.id + }).insert(ignore_mandatory=True) + + donation.submit() + return donation + + +def get_donor(email): + donors = frappe.get_all('Donor', + filters={'email': email}, + order_by='creation desc') + + try: + return frappe.get_doc('Donor', donors[0]['name']) + except Exception: + return None + + +@frappe.whitelist() +def create_donor(payment): + donor_details = frappe._dict(payment) + donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type') + + donor = frappe.new_doc('Donor') + donor.update({ + 'donor_name': donor_details.email, + 'donor_type': donor_type, + 'email': donor_details.email, + 'contact': donor_details.contact + }) + + if donor_details.get('notes'): + donor = get_additional_notes(donor, donor_details) + + donor.insert(ignore_mandatory=True) + return donor + + +def get_company_for_donations(): + company = frappe.db.get_single_value('Non Profit Settings', 'donation_company') + if not company: + from erpnext.healthcare.setup import get_company + company = get_company() + return company + + +def get_additional_notes(donor, donor_details): + if type(donor_details.notes) == dict: + for k, v in donor_details.notes.items(): + notes = '\n'.join('{}: {}'.format(k, v)) + + # extract donor name from notes + if 'name' in k.lower(): + donor.update({ + 'donor_name': donor_details.notes.get(k) + }) + + # extract pan from notes + if 'pan' in k.lower(): + donor.update({ + 'pan_number': donor_details.notes.get(k) + }) + + donor.add_comment('Comment', notes) + + elif type(donor_details.notes) == str: + donor.add_comment('Comment', donor_details.notes) + + return donor + + +def create_mode_of_payment(method): + frappe.get_doc({ + 'doctype': 'Mode of Payment', + 'mode_of_payment': method + }).insert(ignore_mandatory=True) + + +def notify_failure(log): + try: + content = ''' + Dear System Manager, + Razorpay webhook for creating donation failed due to some reason. + Please check the error log linked below + Error Log: {0} + Regards, Administrator + '''.format(get_link_to_form('Error Log', log.name)) + + sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content) + except Exception: + pass + diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py new file mode 100644 index 00000000000..7e25c8d2173 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py @@ -0,0 +1,16 @@ +from __future__ import unicode_literals +from frappe import _ + +def get_data(): + return { + 'fieldname': 'donation', + 'non_standard_fieldnames': { + 'Payment Entry': 'reference_name' + }, + 'transactions': [ + { + 'label': _('Payment'), + 'items': ['Payment Entry'] + } + ] + } \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py new file mode 100644 index 00000000000..c6a534dac34 --- /dev/null +++ b/erpnext/non_profit/doctype/donation/test_donation.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +import frappe +import unittest +from erpnext.non_profit.doctype.donation.donation import create_donation + +class TestDonation(unittest.TestCase): + def setUp(self): + create_donor_type() + settings = frappe.get_doc('Non Profit Settings') + settings.company = '_Test Company' + settings.donation_company = '_Test Company' + settings.default_donor_type = '_Test Donor' + settings.automate_donation_payment_entries = 1 + settings.donation_debit_account = 'Debtors - _TC' + settings.donation_payment_account = 'Cash - _TC' + settings.creation_user = 'Administrator' + settings.flags.ignore_permissions = True + settings.save() + + def test_payment_entry_for_donations(self): + donor = create_donor() + create_mode_of_payment() + payment = frappe._dict({ + 'amount': 100, + 'method': 'Debit Card', + 'id': 'pay_MeXAmsgeKOhq7O' + }) + donation = create_donation(donor, payment) + + self.assertTrue(donation.name) + + # Naive test to check if at all payment entry is generated + # This method is actually triggered from Payment Gateway + # In any case if details were missing, this would throw an error + donation.on_payment_authorized() + donation.reload() + + self.assertEquals(donation.paid, 1) + self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name})) + + +def create_donor_type(): + if not frappe.db.exists('Donor Type', '_Test Donor'): + frappe.get_doc({ + 'doctype': 'Donor Type', + 'donor_type': '_Test Donor' + }).insert() + + +def create_donor(): + donor = frappe.db.exists('Donor', 'donor@test.com') + if donor: + return frappe.get_doc('Donor', 'donor@test.com') + else: + return frappe.get_doc({ + 'doctype': 'Donor', + 'donor_name': '_Test Donor', + 'donor_type': '_Test Donor', + 'email': 'donor@test.com' + }).insert() + + +def create_mode_of_payment(): + if not frappe.db.exists('Mode of Payment', 'Debit Card'): + frappe.get_doc({ + 'doctype': 'Mode of Payment', + 'mode_of_payment': 'Debit Card', + 'accounts': [{ + 'company': '_Test Company', + 'default_account': 'Cash - _TC' + }] + }).insert() \ No newline at end of file diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json index 96392658f1a..72f24ef9226 100644 --- a/erpnext/non_profit/doctype/donor/donor.json +++ b/erpnext/non_profit/doctype/donor/donor.json @@ -76,8 +76,13 @@ } ], "image_field": "image", - "links": [], - "modified": "2020-09-16 23:46:04.083274", + "links": [ + { + "link_doctype": "Donation", + "link_fieldname": "donor" + } + ], + "modified": "2021-02-17 16:36:33.470731", "modified_by": "Administrator", "module": "Non Profit", "name": "Donor", diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py index 9121d0cdfc8..fb70e59575b 100644 --- a/erpnext/non_profit/doctype/donor/donor.py +++ b/erpnext/non_profit/doctype/donor/donor.py @@ -11,3 +11,8 @@ class Donor(Document): """Load address and contacts in `__onload`""" load_address_and_contact(self) + def validate(self): + from frappe.utils import validate_email_address + if self.email: + validate_email_address(self.email.strip(), True) + diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js index 199dcfc04f5..6b8f1b1deb6 100644 --- a/erpnext/non_profit/doctype/member/member.js +++ b/erpnext/non_profit/doctype/member/member.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Member', { setup: function(frm) { - frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { + frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => { if (val && (frm.doc.subscription_id || frm.doc.customer_id)) { frm.set_df_property('razorpay_details_section', 'hidden', false); } diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py index 04b99f93f21..3ba2ee71c67 100644 --- a/erpnext/non_profit/doctype/member/member.py +++ b/erpnext/non_profit/doctype/member/member.py @@ -7,7 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from frappe.integrations.utils import get_payment_gateway_controller from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type @@ -26,9 +26,10 @@ class Member(Document): validate_email_address(email.strip(), True) def setup_subscription(self): - membership_settings = frappe.get_doc("Membership Settings") - if not membership_settings.enable_razorpay: - frappe.throw("Please enable Razorpay to setup subscription") + non_profit_settings = frappe.get_doc('Non Profit Settings') + if not non_profit_settings.enable_razorpay_for_memberships: + frappe.throw('Please check Enable Razorpay for Memberships in {0} to setup subscription').format( + get_link_to_form('Non Profit Settings', 'Non Profit Settings')) controller = get_payment_gateway_controller("Razorpay") settings = controller.get_settings({}) @@ -40,7 +41,7 @@ class Member(Document): subscription_details = { "plan_id": plan_id, - "billing_frequency": cint(membership_settings.billing_frequency), + "billing_frequency": cint(non_profit_settings.billing_frequency), "customer_notify": 1 } diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js index 573ac3319a4..31872048a06 100644 --- a/erpnext/non_profit/doctype/membership/membership.js +++ b/erpnext/non_profit/doctype/membership/membership.js @@ -3,7 +3,7 @@ frappe.ui.form.on('Membership', { setup: function(frm) { - frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => { + frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => { if (val) frm.set_df_property("razorpay_details_section", "hidden", false); }) }, @@ -26,7 +26,7 @@ frappe.ui.form.on('Membership', { }); }); - frappe.db.get_single_value("Membership Settings", "send_email").then(val => { + frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => { if (val) frm.add_custom_button("Send Acknowledgement", () => { frm.call("send_acknowlement").then(() => { frm.reload_doc(); diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 6da053f9fc4..11d32f9c2b4 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -10,6 +10,7 @@ "member_name", "membership_type", "column_break_3", + "company", "membership_status", "membership_validity_section", "from_date", @@ -132,11 +133,18 @@ "fieldtype": "Data", "label": "Member Name", "read_only": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "reqd": 1 } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-21 16:31:20.032656", + "modified": "2021-02-19 14:33:44.925122", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index c113b80d56f..57f787f07f7 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import json import frappe import six +import os from datetime import datetime from frappe.model.document import Document from frappe.email import sendmail_to_system_managers @@ -58,7 +59,7 @@ class Membership(Document): else: self.from_date = nowdate() - if frappe.db.get_single_value("Membership Settings", "billing_cycle") == "Yearly": + if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly": self.to_date = add_years(self.from_date, 1) else: self.to_date = add_months(self.from_date, 1) @@ -68,9 +69,9 @@ class Membership(Document): return self.load_from_db() self.db_set("paid", 1) - settings = frappe.get_doc("Membership Settings") - if settings.enable_invoicing and settings.create_for_web_forms: - self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True) + settings = frappe.get_doc("Non Profit Settings") + if settings.allow_invoicing and settings.automate_membership_invoicing: + self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) def generate_invoice(self, save=True, with_payment_entry=False): @@ -85,7 +86,7 @@ class Membership(Document): frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member))) plan = frappe.get_doc("Membership Type", self.membership_type) - settings = frappe.get_doc("Membership Settings") + settings = frappe.get_doc("Non Profit Settings") self.validate_membership_type_and_settings(plan, settings) invoice = make_invoice(self, member, plan, settings) @@ -102,7 +103,7 @@ class Membership(Document): def validate_membership_type_and_settings(self, plan, settings): settings_link = get_link_to_form("Membership Type", self.membership_type) - if not settings.debit_account: + if not settings.membership_debit_account: frappe.throw(_("You need to set Debit Account in {0}").format(settings_link)) if not settings.company: @@ -113,25 +114,26 @@ class Membership(Document): get_link_to_form("Membership Type", self.membership_type))) def make_payment_entry(self, settings, invoice): - if not settings.payment_account: - frappe.throw(_("You need to set Payment Account in {0}").format( - get_link_to_form("Membership Type", self.membership_type))) + if not settings.membership_payment_account: + frappe.throw(_("You need to set Payment Account for Membership in {0}").format( + get_link_to_form("Non Profit Settings", "Non Profit Settings"))) from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry frappe.flags.ignore_account_permission = True pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total) frappe.flags.ignore_account_permission=False - pe.paid_to = settings.payment_account + pe.paid_to = settings.membership_payment_account pe.reference_no = self.name pe.reference_date = getdate() - pe.save(ignore_permissions=True) + pe.flags.ignore_mandatory = True + pe.save() pe.submit() def send_acknowlement(self): - settings = frappe.get_doc("Membership Settings") + settings = frappe.get_doc("Non Profit Settings") if not settings.send_email: frappe.throw(_("You need to enable Send Acknowledge Email in {0}").format( - get_link_to_form("Membership Settings", "Membership Settings"))) + get_link_to_form("Non Profit Settings", "Non Profit Settings"))) member = frappe.get_doc("Member", self.member) if not member.email_id: @@ -170,7 +172,7 @@ def make_invoice(membership, member, plan, settings): invoice = frappe.get_doc({ "doctype": "Sales Invoice", "customer": member.customer, - "debit_to": settings.debit_account, + "debit_to": settings.membership_debit_account, "currency": membership.currency, "company": settings.company, "is_pos": 0, @@ -183,7 +185,7 @@ def make_invoice(membership, member, plan, settings): ] }) invoice.set_missing_values() - invoice.insert(ignore_permissions=True) + invoice.insert() invoice.submit() frappe.msgprint(_("Sales Invoice created successfully")) @@ -203,17 +205,18 @@ def get_member_based_on_subscription(subscription_id, email): return None -def verify_signature(data): - if frappe.flags.in_test: +def verify_signature(data, endpoint="Membership"): + if frappe.flags.in_test or os.environ.get("CI"): return True signature = frappe.request.headers.get("X-Razorpay-Signature") - settings = frappe.get_doc("Membership Settings") - key = settings.get_webhook_secret() + settings = frappe.get_doc("Non Profit Settings") + key = settings.get_webhook_secret(endpoint) controller = frappe.get_doc("Razorpay Settings") controller.verify_signature(data, signature, key) + frappe.set_user(settings.creation_user) @frappe.whitelist(allow_guest=True) @@ -222,7 +225,7 @@ def trigger_razorpay_subscription(*args, **kwargs): try: verify_signature(data) except Exception as e: - log = frappe.log_error(e, "Webhook Verification Error") + log = frappe.log_error(e, "Membership Webhook Verification Error") notify_failure(log) return { "status": "Failed", "reason": e} @@ -250,16 +253,15 @@ def trigger_razorpay_subscription(*args, **kwargs): member.subscription_id = subscription.id member.customer_id = payment.customer_id - if subscription.notes and type(subscription.notes) == dict: - notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items()) - member.add_comment("Comment", notes) - elif subscription.notes and type(subscription.notes) == str: - member.add_comment("Comment", subscription.notes) + if subscription.get("notes"): + member = get_additional_notes(member, subscription) + company = get_company_for_memberships() # Update Membership membership = frappe.new_doc("Membership") membership.update({ + "company": company, "member": member.name, "membership_status": "Current", "membership_type": member.membership_type, @@ -270,13 +272,19 @@ def trigger_razorpay_subscription(*args, **kwargs): "to_date": datetime.fromtimestamp(subscription.current_end), "amount": payment.amount / 100 # Convert to rupees from paise }) - membership.insert(ignore_permissions=True) + membership.flags.ignore_mandatory = True + membership.insert() # Update membership values member.subscription_start = datetime.fromtimestamp(subscription.start_at) member.subscription_end = datetime.fromtimestamp(subscription.end_at) member.subscription_activated = 1 - member.save(ignore_permissions=True) + member.flags.ignore_mandatory = True + member.save() + + automate_payment = frappe.db.get_single_value("Membership Settings", "automate_membership_payment_entries") + membership.generate_invoice(with_payment_entry=automate_payment, save=True) + except Exception as e: message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name)) @@ -286,6 +294,39 @@ def trigger_razorpay_subscription(*args, **kwargs): return { "status": "Success" } +def get_company_for_memberships(): + company = frappe.db.get_single_value("Non Profit Settings", "company") + if not company: + from erpnext.healthcare.setup import get_company + company = get_company() + return company + + +def get_additional_notes(member, subscription): + if type(subscription.notes) == dict: + for k, v in subscription.notes.items(): + notes = "\n".join("{}: {}".format(k, v)) + + # extract member name from notes + if "name" in k.lower(): + member.update({ + "member_name": subscription.notes.get(k) + }) + + # extract pan number from notes + if "pan" in k.lower(): + member.update({ + "pan_number": subscription.notes.get(k) + }) + + member.add_comment("Comment", notes) + + elif type(subscription.notes) == str: + member.add_comment("Comment", subscription.notes) + + return member + + def notify_failure(log): try: content = """ diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py index ff7e6c473c5..31da792e534 100644 --- a/erpnext/non_profit/doctype/membership/test_membership.py +++ b/erpnext/non_profit/doctype/membership/test_membership.py @@ -10,33 +10,7 @@ from frappe.utils import nowdate, add_months class TestMembership(unittest.TestCase): def setUp(self): - # Get default company - company = frappe.get_doc("Company", erpnext.get_default_company()) - - # update membership settings - settings = frappe.get_doc("Membership Settings") - # Enable razorpay - settings.enable_razorpay = 1 - settings.billing_cycle = "Monthly" - settings.billing_frequency = 24 - # Enable invoicing - settings.enable_invoicing = 1 - settings.make_payment_entry = 1 - settings.company = company.name - settings.payment_account = company.default_cash_account - settings.debit_account = company.default_receivable_account - settings.save() - - # make test plan - if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): - plan = frappe.new_doc("Membership Type") - plan.membership_type = "_rzpy_test_milythm" - plan.amount = 100 - plan.razorpay_plan_id = "_rzpy_test_milythm" - plan.linked_item = create_item("_Test Item for Non Profit Membership").name - plan.insert() - else: - plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") + plan = setup_membership() # make test member self.member_doc = create_member(frappe._dict({ @@ -78,7 +52,7 @@ class TestMembership(unittest.TestCase): }) def set_config(key, value): - frappe.db.set_value("Membership Settings", None, key, value) + frappe.db.set_value("Non Profit Settings", None, key, value) def make_membership(member, payload={}): data = { @@ -109,3 +83,36 @@ def create_item(item_code): else: item = frappe.get_doc("Item", item_code) return item + +def setup_membership(): + # Get default company + company = frappe.get_doc("Company", erpnext.get_default_company()) + + # update non profit settings + settings = frappe.get_doc("Non Profit Settings") + # Enable razorpay + settings.enable_razorpay_for_memberships = 1 + settings.billing_cycle = "Monthly" + settings.billing_frequency = 24 + # Enable invoicing + settings.allow_invoicing = 1 + settings.automate_membership_payment_entries = 1 + settings.company = company.name + settings.donation_company = company.name + settings.membership_payment_account = company.default_cash_account + settings.membership_debit_account = company.default_receivable_account + settings.flags.ignore_mandatory = True + settings.save() + + # make test plan + if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"): + plan = frappe.new_doc("Membership Type") + plan.membership_type = "_rzpy_test_milythm" + plan.amount = 100 + plan.razorpay_plan_id = "_rzpy_test_milythm" + plan.linked_item = create_item("_Test Item for Non Profit Membership").name + plan.insert() + else: + plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm") + + return plan \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json deleted file mode 100644 index 3887b0a2bea..00000000000 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json +++ /dev/null @@ -1,192 +0,0 @@ -{ - "actions": [], - "creation": "2020-03-29 12:57:03.005120", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "enable_razorpay", - "razorpay_settings_section", - "billing_cycle", - "billing_frequency", - "webhook_secret", - "column_break_6", - "enable_invoicing", - "create_for_web_forms", - "make_payment_entry", - "company", - "debit_account", - "payment_account", - "column_break_9", - "send_email", - "send_invoice", - "membership_print_format", - "inv_print_format", - "email_template" - ], - "fields": [ - { - "fieldname": "billing_cycle", - "fieldtype": "Select", - "label": "Billing Cycle", - "options": "Monthly\nYearly" - }, - { - "default": "0", - "fieldname": "enable_razorpay", - "fieldtype": "Check", - "label": "Enable RazorPay For Memberships" - }, - { - "depends_on": "eval:doc.enable_razorpay", - "fieldname": "razorpay_settings_section", - "fieldtype": "Section Break", - "label": "RazorPay Settings" - }, - { - "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.", - "fieldname": "billing_frequency", - "fieldtype": "Int", - "label": "Billing Frequency" - }, - { - "fieldname": "webhook_secret", - "fieldtype": "Password", - "label": "Webhook Secret", - "read_only": 1 - }, - { - "fieldname": "column_break_6", - "fieldtype": "Section Break", - "label": "Invoicing" - }, - { - "depends_on": "eval:doc.enable_invoicing", - "fieldname": "debit_account", - "fieldtype": "Link", - "label": "Debit Account", - "mandatory_depends_on": "eval:doc.enable_auto_invoicing", - "options": "Account" - }, - { - "fieldname": "column_break_9", - "fieldtype": "Column Break" - }, - { - "depends_on": "eval:doc.enable_invoicing", - "fieldname": "company", - "fieldtype": "Link", - "label": "Company", - "mandatory_depends_on": "eval:doc.enable_auto_invoicing", - "options": "Company" - }, - { - "default": "0", - "depends_on": "eval:doc.enable_invoicing && doc.send_email", - "fieldname": "send_invoice", - "fieldtype": "Check", - "label": "Send Invoice with Email" - }, - { - "default": "0", - "fieldname": "send_email", - "fieldtype": "Check", - "label": "Send Membership Acknowledgement" - }, - { - "depends_on": "eval: doc.send_invoice", - "fieldname": "inv_print_format", - "fieldtype": "Link", - "label": "Invoice Print Format", - "mandatory_depends_on": "eval: doc.send_invoice", - "options": "Print Format" - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "membership_print_format", - "fieldtype": "Link", - "label": "Membership Print Format", - "options": "Print Format" - }, - { - "depends_on": "eval:doc.send_email", - "fieldname": "email_template", - "fieldtype": "Link", - "label": "Email Template", - "mandatory_depends_on": "eval:doc.send_email", - "options": "Email Template" - }, - { - "default": "0", - "fieldname": "enable_invoicing", - "fieldtype": "Check", - "label": "Enable Invoicing", - "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry" - }, - { - "default": "0", - "depends_on": "eval:doc.enable_invoicing", - "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.", - "fieldname": "make_payment_entry", - "fieldtype": "Check", - "label": "Make Payment Entry" - }, - { - "depends_on": "eval:doc.make_payment_entry", - "fieldname": "payment_account", - "fieldtype": "Link", - "label": "Payment To", - "mandatory_depends_on": "eval:doc.make_payment_entry", - "options": "Account" - }, - { - "default": "0", - "depends_on": "eval:doc.enable_invoicing", - "description": "Automatically create an invoice when payment is authorized from a web form entry", - "fieldname": "create_for_web_forms", - "fieldtype": "Check", - "label": "Auto Create Invoice for Web Forms" - } - ], - "index_web_pages_for_search": 1, - "issingle": 1, - "links": [], - "modified": "2021-01-21 19:57:53.213286", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Membership Settings", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "System Manager", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "print": 1, - "read": 1, - "role": "Non Profit Manager", - "share": 1, - "write": 1 - }, - { - "email": 1, - "print": 1, - "read": 1, - "role": "Non Profit Member", - "share": 1 - } - ], - "quick_entry": 1, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1 -} \ No newline at end of file diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py deleted file mode 100644 index f3b2eee6f97..00000000000 --- a/erpnext/non_profit/doctype/membership_settings/membership_settings.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe -from frappe import _ -from frappe.integrations.utils import get_payment_gateway_controller -from frappe.model.document import Document - -class MembershipSettings(Document): - def generate_webhook_key(self): - key = frappe.generate_hash(length=20) - self.webhook_secret = key - self.save() - - frappe.msgprint( - _("Here is your webhook secret, this will be shown to you only once.") + "

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

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

{{ doc.company }} 80G Donor Certificate

\n
\n

\n\n
\n

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

\n

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

\n

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

\n \n

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

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

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

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

\n

Financial Cycle {{ doc.fiscal_year }}

\n
\n

\n\n
\n

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

\n

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

\n

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

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

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

\n\n
\n
\n\n

\n

{{doc.company_address_display }}

\n\n", + "idx": 0, + "line_breaks": 0, + "modified": "2021-02-21 23:29:00.778973", + "modified_by": "Administrator", + "module": "Regional", + "name": "80G Certificate for Membership", + "owner": "Administrator", + "print_format_builder": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} \ No newline at end of file diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py index 72ed00293ed..5053c6a5124 100644 --- a/erpnext/setup/setup_wizard/operations/install_fixtures.py +++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py @@ -195,6 +195,7 @@ def install(country=None): {'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"}, {'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"}, {'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"}, + {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"}, {'doctype': "Opportunity Type", "name": "Hub"}, {'doctype': "Opportunity Type", "name": _("Sales")}, From d1e331f77f701b641af7d23a5960d36e004c9139 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Thu, 11 Mar 2021 15:27:22 +0530 Subject: [PATCH 64/64] fix(Membership): Generate Invoice for membership webhook only if automation is enabled (#24849) --- erpnext/non_profit/doctype/membership/membership.py | 5 +++-- .../doctype/non_profit_settings/non_profit_settings.py | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py index 57f787f07f7..191281f4cea 100644 --- a/erpnext/non_profit/doctype/membership/membership.py +++ b/erpnext/non_profit/doctype/membership/membership.py @@ -282,8 +282,9 @@ def trigger_razorpay_subscription(*args, **kwargs): member.flags.ignore_mandatory = True member.save() - automate_payment = frappe.db.get_single_value("Membership Settings", "automate_membership_payment_entries") - membership.generate_invoice(with_payment_entry=automate_payment, save=True) + settings = frappe.get_doc("Non Profit Settings") + if settings.allow_invoicing and settings.automate_membership_invoicing: + membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True) except Exception as e: message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id) diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py index 95765fd536f..108554c6a08 100644 --- a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py +++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py @@ -9,7 +9,7 @@ from frappe.integrations.utils import get_payment_gateway_controller from frappe.model.document import Document class NonProfitSettings(Document): - def generate_webhook_secret(self, field): + def generate_webhook_secret(self, field="membership_webhook_secret"): key = frappe.generate_hash(length=20) self.set(field, key) self.save()