From 59a0bce3a9967dacc9d053ca85a470bc74bfd76f Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Tue, 7 Jan 2020 14:39:33 +0530 Subject: [PATCH 01/27] feat: Repay advance from salary --- .../employee_advance/employee_advance.js | 34 +++++++++++++++---- .../employee_advance/employee_advance.json | 28 +++++++++++++-- .../employee_advance/employee_advance.py | 20 +++++++++-- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 389660387b..c5d044a187 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -23,6 +23,14 @@ frappe.ui.form.on('Employee Advance', { } }; }); + + frm.set_query('salary_component', function(doc) { + return { + filters: { + "type": "Deduction" + } + }; + }); }, refresh: function(frm) { @@ -47,19 +55,33 @@ frappe.ui.form.on('Employee Advance', { } if (frm.doc.docstatus === 1 - && (flt(frm.doc.claimed_amount) + flt(frm.doc.return_amount) < flt(frm.doc.paid_amount)) - && frappe.model.can_create("Journal Entry")) { + && (flt(frm.doc.claimed_amount) < flt(frm.doc.paid_amount) && flt(frm.doc.paid_amount) != flt(frm.doc.return_amount))) { - frm.add_custom_button(__("Return"), function() { - frm.trigger('make_return_entry'); - }, __('Create')); + if (frm.doc.repay_unclaimed_amount_from_salary == 0 && frappe.model.can_create("Journal Entry")){ + frm.add_custom_button(__("Return"), function() { + frm.trigger('make_return_entry'); + }, __('Create')); + }else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")){ + frm.add_custom_button(__("Deduction from salary"), function() { + frm.events.make_deduction_via_additional_salary(frm) + }, __('Create')); + } } }, + make_deduction_via_additional_salary: function(frm){ + frappe.call({ + method: "erpnext.hr.doctype.employee_advance.employee_advance.create_return_through_additional_salary", + args: { + doc: frm.doc + } + }); + }, + make_payment_entry: function(frm) { var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) { - method = "erpnext.hr.doctype.employee_advance.employee_advance.make_bank_entry" + method = "erpnext.hr.doctype.employee_advance.employee_advance.make_bank_entry"; } return frappe.call({ method: method, diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index d233a2bb93..7f599a74c7 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -10,9 +10,12 @@ "naming_series", "employee", "employee_name", + "department", "column_break_4", "posting_date", - "department", + "repay_unclaimed_amount_from_salary", + "payroll_date", + "salary_component", "section_break_8", "purpose", "column_break_11", @@ -169,11 +172,30 @@ "label": "Returned Amount", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "default": "0", + "fieldname": "repay_unclaimed_amount_from_salary", + "fieldtype": "Check", + "label": "Repay unclaimed amount from salary" + }, + { + "depends_on": "eval:doc.repay_unclaimed_amount_from_salary == 1", + "fieldname": "payroll_date", + "fieldtype": "Date", + "label": "Payroll date" + }, + { + "depends_on": "eval:doc.repay_unclaimed_amount_from_salary == 1", + "fieldname": "salary_component", + "fieldtype": "Link", + "label": "Salary Component", + "options": "Salary Component" } ], "is_submittable": 1, "links": [], - "modified": "2019-12-15 19:04:07.044505", + "modified": "2020-01-03 13:02:32.094099", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -210,4 +232,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index f10e3b6ce2..feedccca12 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -133,8 +133,24 @@ def make_bank_entry(dt, dn): return je.as_dict() @frappe.whitelist() -def make_return_entry(employee, company, employee_advance_name, - return_amount, advance_account, mode_of_payment=None): +def create_return_through_additional_salary(doc): + import json + doc = frappe._dict(json.loads(doc)) + additional_salary = frappe.new_doc('Additional Salary') + additional_salary.employee = doc.employee + additional_salary.salary_component = doc.salary_component + additional_salary.amount = doc.paid_amount - doc.claimed_amount + additional_salary.payroll_date = doc.payroll_date + additional_salary.company = doc.company + + additional_salary.submit() + + frappe.db.set_value("Employee Advance", doc.name, "return_amount", additional_salary.amount) + + return additional_salary.name + +@frappe.whitelist() +def make_return_entry(employee_name, company, employee_advance_name, return_amount, mode_of_payment, advance_account): return_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) je = frappe.new_doc('Journal Entry') je.posting_date = nowdate() From 943457abd39dacd3a8aa632ab7ceaa6fba38da11 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 5 Mar 2020 18:57:52 +0530 Subject: [PATCH 02/27] fix: reverse linking for employee advance --- .../additional_salary/additional_salary.json | 18 ++++++++++++- .../employee_advance/employee_advance.js | 5 ++++ .../employee_advance/employee_advance.json | 17 +----------- .../employee_advance/employee_advance.py | 10 +++---- .../employee_incentive.json | 13 ++-------- .../employee_incentive/employee_incentive.py | 26 ++++++------------- 6 files changed, 36 insertions(+), 53 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.json b/erpnext/hr/doctype/additional_salary/additional_salary.json index 7d69f7e7fc..9819b384ee 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.json +++ b/erpnext/hr/doctype/additional_salary/additional_salary.json @@ -13,6 +13,8 @@ "salary_component", "overwrite_salary_structure_amount", "deduct_full_tax_on_selected_payroll_date", + "ref_doctype", + "ref_docname", "column_break_5", "company", "payroll_date", @@ -127,11 +129,25 @@ "options": "Additional Salary", "print_hide": 1, "read_only": 1 + }, + { + "fieldname": "ref_doctype", + "fieldtype": "Link", + "label": "Reference Document Type", + "options": "DocType", + "read_only": 1 + }, + { + "fieldname": "ref_docname", + "fieldtype": "Dynamic Link", + "label": "Reference Document", + "options": "ref_doctype", + "read_only": 1 } ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 19:07:23.635901", + "modified": "2020-03-05 18:54:17.763244", "modified_by": "Administrator", "module": "HR", "name": "Additional Salary", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index c5d044a187..38561d41a8 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -74,6 +74,11 @@ frappe.ui.form.on('Employee Advance', { method: "erpnext.hr.doctype.employee_advance.employee_advance.create_return_through_additional_salary", args: { doc: frm.doc + }, + callback: function (r){ + console.log("Helloxs") + var doclist = frappe.model.sync(r.message); + frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } }); }, diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 7f599a74c7..1ab2356c8e 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -14,8 +14,6 @@ "column_break_4", "posting_date", "repay_unclaimed_amount_from_salary", - "payroll_date", - "salary_component", "section_break_8", "purpose", "column_break_11", @@ -178,24 +176,11 @@ "fieldname": "repay_unclaimed_amount_from_salary", "fieldtype": "Check", "label": "Repay unclaimed amount from salary" - }, - { - "depends_on": "eval:doc.repay_unclaimed_amount_from_salary == 1", - "fieldname": "payroll_date", - "fieldtype": "Date", - "label": "Payroll date" - }, - { - "depends_on": "eval:doc.repay_unclaimed_amount_from_salary == 1", - "fieldname": "salary_component", - "fieldtype": "Link", - "label": "Salary Component", - "options": "Salary Component" } ], "is_submittable": 1, "links": [], - "modified": "2020-01-03 13:02:32.094099", + "modified": "2020-03-05 16:31:50.417539", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index feedccca12..a9cccd7b28 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -138,16 +138,12 @@ def create_return_through_additional_salary(doc): doc = frappe._dict(json.loads(doc)) additional_salary = frappe.new_doc('Additional Salary') additional_salary.employee = doc.employee - additional_salary.salary_component = doc.salary_component additional_salary.amount = doc.paid_amount - doc.claimed_amount - additional_salary.payroll_date = doc.payroll_date additional_salary.company = doc.company + additional_salary.ref_doctype = doc.doctype + additional_salary.ref_docname = doc.name - additional_salary.submit() - - frappe.db.set_value("Employee Advance", doc.name, "return_amount", additional_salary.amount) - - return additional_salary.name + return additional_salary @frappe.whitelist() def make_return_entry(employee_name, company, employee_advance_name, return_amount, mode_of_payment, advance_account): diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.json b/erpnext/hr/doctype/employee_incentive/employee_incentive.json index ce8e1ea230..e2d8a11f47 100644 --- a/erpnext/hr/doctype/employee_incentive/employee_incentive.json +++ b/erpnext/hr/doctype/employee_incentive/employee_incentive.json @@ -9,10 +9,9 @@ "employee", "incentive_amount", "employee_name", - "additional_salary", + "salary_component", "column_break_5", "payroll_date", - "salary_component", "department", "amended_from" ], @@ -65,14 +64,6 @@ "options": "Department", "read_only": 1 }, - { - "fieldname": "additional_salary", - "fieldtype": "Link", - "label": "Additional Salary", - "no_copy": 1, - "options": "Additional Salary", - "read_only": 1 - }, { "fieldname": "salary_component", "fieldtype": "Link", @@ -83,7 +74,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2019-12-12 13:24:44.761540", + "modified": "2020-03-05 18:59:40.526014", "modified_by": "Administrator", "module": "HR", "name": "Employee Incentive", diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.py b/erpnext/hr/doctype/employee_incentive/employee_incentive.py index 2e138f8ef5..1cc52e1112 100644 --- a/erpnext/hr/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/hr/doctype/employee_incentive/employee_incentive.py @@ -10,11 +10,13 @@ class EmployeeIncentive(Document): def on_submit(self): company = frappe.db.get_value('Employee', self.employee, 'company') additional_salary = frappe.db.exists('Additional Salary', { - 'employee': self.employee, + 'employee': self.employee, + 'ref_doctype': self.doctype, + 'ref_docname': self.name, 'salary_component': self.salary_component, - 'payroll_date': self.payroll_date, + 'payroll_date': self.payroll_date, 'company': company, - 'docstatus': 1 + 'docstatus': ["!=", 2] }) if not additional_salary: @@ -24,22 +26,10 @@ class EmployeeIncentive(Document): additional_salary.amount = self.incentive_amount additional_salary.payroll_date = self.payroll_date additional_salary.company = company + additional_salary.ref_doctype = self.doctype + additional_salary.ref_docname = self.name additional_salary.submit() - self.db_set('additional_salary', additional_salary.name) - else: incentive_added = frappe.db.get_value('Additional Salary', additional_salary, 'amount') + self.incentive_amount - frappe.db.set_value('Additional Salary', additional_salary, 'amount', incentive_added) - self.db_set('additional_salary', additional_salary) + frappe.db.set_value('Additional Salary', additional_salary, {'amount', incentive_added}) - def on_cancel(self): - if self.additional_salary: - incentive_removed = frappe.db.get_value('Additional Salary', self.additional_salary, 'amount') - self.incentive_amount - if incentive_removed == 0: - frappe.get_doc('Additional Salary', self.additional_salary).cancel() - else: - frappe.db.set_value('Additional Salary', self.additional_salary, 'amount', incentive_removed) - - self.db_set('additional_salary', '') - - From 2d7fcab2c96e82d4f812a8f8355660d457a7b593 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 13 Jan 2020 19:10:48 +0530 Subject: [PATCH 03/27] feat: Recurring Addtional Salary --- .../additional_salary/additional_salary.js | 2 +- .../additional_salary/additional_salary.json | 34 ++++++++++++++----- .../additional_salary/additional_salary.py | 27 ++++++++++++--- erpnext/hr/doctype/salary_slip/salary_slip.py | 1 + 4 files changed, 49 insertions(+), 15 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.js b/erpnext/hr/doctype/additional_salary/additional_salary.js index 18f6b8b52d..fb42b6f410 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.js +++ b/erpnext/hr/doctype/additional_salary/additional_salary.js @@ -13,5 +13,5 @@ frappe.ui.form.on('Additional Salary', { } }; }); - } + }, }); diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.json b/erpnext/hr/doctype/additional_salary/additional_salary.json index 9819b384ee..91bcdc394a 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.json +++ b/erpnext/hr/doctype/additional_salary/additional_salary.json @@ -17,8 +17,10 @@ "ref_docname", "column_break_5", "company", + "is_recurring", + "from_date", + "to_date", "payroll_date", - "salary_slip", "type", "department", "amount", @@ -76,12 +78,13 @@ "fieldtype": "Column Break" }, { + "depends_on": "eval:(doc.is_recurring==0)", "description": "Date on which this component is applied", "fieldname": "payroll_date", "fieldtype": "Date", "in_list_view": 1, "label": "Payroll Date", - "reqd": 1, + "mandatory_depends_on": "eval:(doc.is_recurring==0)", "search_index": 1 }, { @@ -107,13 +110,6 @@ "options": "Company", "reqd": 1 }, - { - "fieldname": "salary_slip", - "fieldtype": "Link", - "label": "Salary Slip", - "options": "Salary Slip", - "read_only": 1 - }, { "fetch_from": "salary_component.type", "fieldname": "type", @@ -131,6 +127,26 @@ "read_only": 1 }, { + "default": "0", + "fieldname": "is_recurring", + "fieldtype": "Check", + "label": "Is Recurring" + }, + { + "depends_on": "eval:(doc.is_recurring==1)", + "fieldname": "from_date", + "fieldtype": "Date", + "label": "From Date", + "mandatory_depends_on": "eval:(doc.is_recurring==1)" + }, + { + "depends_on": "eval:(doc.is_recurring==1)", + "fieldname": "to_date", + "fieldtype": "Date", + "label": "To Date", + "mandatory_depends_on": "eval:(doc.is_recurring==1)" + }, + { "fieldname": "ref_doctype", "fieldtype": "Link", "label": "Reference Document Type", diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/hr/doctype/additional_salary/additional_salary.py index bc7dcee55e..a6f34cc759 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/additional_salary.py @@ -21,10 +21,23 @@ class AdditionalSalary(Document): frappe.throw(_("Amount should not be less than zero.")) def validate_dates(self): - date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, + date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) - if date_of_joining and getdate(self.payroll_date) < getdate(date_of_joining): - frappe.throw(_("Payroll date can not be less than employee's joining date")) + + if not self.is_recurring and not self.payroll_date: + frappe.msgprint(_("Please enter Payroll Date."), indicator='blue', raise_exception=1) + if self.is_recurring and not self.from_date and not self.to_date: + frappe.msgprint(_("Please enter From Date and To Date."), indicator='blue', raise_exception=1) + if getdate(self.from_date) > getdate(self.to_date): + frappe.throw(_("From Date can not be greater than To Date.")) + + if date_of_joining: + if getdate(self.payroll_date) < getdate(date_of_joining): + frappe.throw(_("Payroll date can not be less than employee's joining date.")) + elif getdate(self.from_date) < getdate(date_of_joining): + frappe.throw(_("From date can not be less than employee's joining date.")) + elif getdate(self.to_date) > getdate(relieving_date): + frappe.throw(_("To date can not be greater than employee's relieving date.")) def get_amount(self, sal_start_date, sal_end_date): start_date = getdate(sal_start_date) @@ -45,8 +58,12 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty from `tabAdditional Salary` where employee=%(employee)s and docstatus = 1 - and payroll_date between %(from_date)s and %(to_date)s - and type = %(component_type)s + and (payroll_date between %(from_date)s and %(to_date)s) + or ( + (from_date between %(from_date)s and %(to_date)s) + or(to_date between %(from_date)s and %(to_date)s) + ) + and type = %(component_type)s group by salary_component, overwrite_salary_structure_amount order by salary_component, overwrite_salary_structure_amount """, { diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index d03a3dd9a3..a78f55ca32 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -410,6 +410,7 @@ class SalarySlip(TransactionBase): if additional_components: for additional_component in additional_components: amount = additional_component.amount + print("-------------[>>>]", amount) overwrite = additional_component.overwrite self.update_component_row(frappe._dict(additional_component.struct_row), amount, component_type, overwrite=overwrite) From 885ab5dc663e5b04f72cbe1a1d4ce8b46861061f Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 20 Feb 2020 12:41:42 +0530 Subject: [PATCH 04/27] fix: requested changes --- erpnext/hr/doctype/additional_salary/additional_salary.py | 4 ---- erpnext/hr/doctype/salary_slip/salary_slip.py | 1 - 2 files changed, 5 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/hr/doctype/additional_salary/additional_salary.py index a6f34cc759..c37c1b4d2d 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/additional_salary.py @@ -24,10 +24,6 @@ class AdditionalSalary(Document): date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, ["date_of_joining", "relieving_date"]) - if not self.is_recurring and not self.payroll_date: - frappe.msgprint(_("Please enter Payroll Date."), indicator='blue', raise_exception=1) - if self.is_recurring and not self.from_date and not self.to_date: - frappe.msgprint(_("Please enter From Date and To Date."), indicator='blue', raise_exception=1) if getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("From Date can not be greater than To Date.")) diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index a78f55ca32..d03a3dd9a3 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -410,7 +410,6 @@ class SalarySlip(TransactionBase): if additional_components: for additional_component in additional_components: amount = additional_component.amount - print("-------------[>>>]", amount) overwrite = additional_component.overwrite self.update_component_row(frappe._dict(additional_component.struct_row), amount, component_type, overwrite=overwrite) From 1663f66e6d2e069241d0edab7cd7c04d5f933b65 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Thu, 20 Feb 2020 19:33:27 +0530 Subject: [PATCH 05/27] test: Recurring Additional salary --- .../additional_salary/additional_salary.py | 10 ++--- .../test_additional_salary.py | 41 ++++++++++++++++++- erpnext/hr/doctype/employee/test_employee.py | 2 +- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/hr/doctype/additional_salary/additional_salary.py index c37c1b4d2d..d1b602d243 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/additional_salary.py @@ -54,11 +54,11 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty from `tabAdditional Salary` where employee=%(employee)s and docstatus = 1 - and (payroll_date between %(from_date)s and %(to_date)s) - or ( - (from_date between %(from_date)s and %(to_date)s) - or(to_date between %(from_date)s and %(to_date)s) - ) + and ( + payroll_date between %(from_date)s and %(to_date)s + or + from_date <= %(to_date)s and to_date >= %(to_date)s + ) and type = %(component_type)s group by salary_component, overwrite_salary_structure_amount order by salary_component, overwrite_salary_structure_amount diff --git a/erpnext/hr/doctype/additional_salary/test_additional_salary.py b/erpnext/hr/doctype/additional_salary/test_additional_salary.py index 949ba20335..ebe6b20d81 100644 --- a/erpnext/hr/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/test_additional_salary.py @@ -3,6 +3,45 @@ # See license.txt from __future__ import unicode_literals import unittest +import frappe, erpnext +from frappe.utils import nowdate, add_days +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.salary_component.test_salary_component import create_salary_component +from erpnext.hr.doctype.salary_slip.test_salary_slip import make_employee_salary_slip + class TestAdditionalSalary(unittest.TestCase): - pass + + def setUp(self): + from erpnext.hr.doctype.salary_slip.test_salary_slip import TestSalarySlip + TestSalarySlip().setUp() + + def test_recurring_additional_salary(self): + emp_id = make_employee("test_additional@salary.com") + frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) + add_sal = get_additional_salary(emp_id) + + ss = make_employee_salary_slip("test_additional@salary.com", "Monthly") + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + + self.assertEqual(amount, add_sal.amount) + self.assertEqual(salary_component, add_sal.salary_component) + + + +def get_additional_salary(emp_id): + create_salary_component("Recurring Salary Component") + add_sal = frappe.new_doc("Additional Salary") + add_sal.employee = emp_id + add_sal.salary_component = "Recurring Salary Component" + add_sal.is_recurring = 1 + add_sal.from_date = add_days(nowdate(), -50) + add_sal.to_date = add_days(nowdate(), 180) + add_sal.amount = 5000 + add_sal.save() + add_sal.submit() + + return add_sal diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index d3410de2eb..906386db11 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -51,7 +51,7 @@ def make_employee(user, company=None): "doctype": "User", "email": user, "first_name": user, - "new_password": "password", + "new_password": "qwerty123@12435", "roles": [{"doctype": "Has Role", "role": "Employee"}] }).insert() From d319feee9e3bfc42e7515ca4828aaa18e027d867 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 2 Mar 2020 18:24:55 +0530 Subject: [PATCH 06/27] fix: reverse linking --- .../doctype/additional_salary/additional_salary.json | 2 +- .../hr/doctype/additional_salary/additional_salary.py | 8 ++++++-- erpnext/hr/doctype/employee/test_employee.py | 2 +- erpnext/hr/doctype/salary_detail/salary_detail.json | 10 +++++++++- erpnext/hr/doctype/salary_slip/salary_slip.py | 9 +-------- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.json b/erpnext/hr/doctype/additional_salary/additional_salary.json index 91bcdc394a..bf9d8192d0 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.json +++ b/erpnext/hr/doctype/additional_salary/additional_salary.json @@ -163,7 +163,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-05 18:54:17.763244", + "modified": "2020-03-02 18:06:29.170878", "modified_by": "Administrator", "module": "HR", "name": "Additional Salary", diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/hr/doctype/additional_salary/additional_salary.py index d1b602d243..d8f7444beb 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/additional_salary.py @@ -50,7 +50,7 @@ class AdditionalSalary(Document): @frappe.whitelist() def get_additional_salary_component(employee, start_date, end_date, component_type): additional_components = frappe.db.sql(""" - select salary_component, sum(amount) as amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date + select name, salary_component, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date from `tabAdditional Salary` where employee=%(employee)s and docstatus = 1 @@ -60,7 +60,6 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty from_date <= %(to_date)s and to_date >= %(to_date)s ) and type = %(component_type)s - group by salary_component, overwrite_salary_structure_amount order by salary_component, overwrite_salary_structure_amount """, { 'employee': employee, @@ -70,8 +69,11 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty }, as_dict=1) additional_components_list = [] + existing_salary_components = [] component_fields = ["depends_on_payment_days", "salary_component_abbr", "is_tax_applicable", "variable_based_on_taxable_salary", 'type'] for d in additional_components: + if d.salary_component in existing_salary_components: + frappe.throw(_('Multiple additional Salary is created for Salary Component {0}'.format(d.salary_component))) struct_row = frappe._dict({'salary_component': d.salary_component}) component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields) if component: @@ -79,6 +81,7 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date struct_row['is_additional_component'] = 1 + struct_row['additional_salary'] = d.name additional_components_list.append(frappe._dict({ 'amount': d.amount, @@ -86,4 +89,5 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty 'struct_row': struct_row, 'overwrite': d.overwrite_salary_structure_amount, })) + existing_salary_components.append(d.salary_component) return additional_components_list \ No newline at end of file diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py index 906386db11..d3410de2eb 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -51,7 +51,7 @@ def make_employee(user, company=None): "doctype": "User", "email": user, "first_name": user, - "new_password": "qwerty123@12435", + "new_password": "password", "roles": [{"doctype": "Has Role", "role": "Employee"}] }).insert() diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json index bde735d3bc..5a55593506 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/hr/doctype/salary_detail/salary_detail.json @@ -25,6 +25,7 @@ "tax_on_flexible_benefit", "tax_on_additional_salary", "section_break_11", + "additional_salary", "condition_and_formula_help" ], "fields": [ @@ -187,11 +188,18 @@ "fieldtype": "HTML", "label": "Condition and Formula Help", "options": "

Condition and Formula Help

\n\n

Notes:

\n\n
    \n
  1. Use field base for using base salary of the Employee
  2. \n
  3. Use Salary Component abbreviations in conditions and formulas. BS = Basic Salary
  4. \n
  5. Use field name for employee details in conditions and formulas. Employment Type = employment_typeBranch = branch
  6. \n
  7. Use field name from Salary Slip in conditions and formulas. Payment Days = payment_daysLeave without pay = leave_without_pay
  8. \n
  9. Direct Amount can also be entered based on Condtion. See example 3
\n\n

Examples

\n
    \n
  1. Calculating Basic Salary based on base\n
    Condition: base < 10000
    \n
    Formula: base * .2
  2. \n
  3. Calculating HRA based on Basic SalaryBS \n
    Condition: BS > 2000
    \n
    Formula: BS * .1
  4. \n
  5. Calculating TDS based on Employment Typeemployment_type \n
    Condition: employment_type==\"Intern\"
    \n
    Amount: 1000
  6. \n
" + }, + { + "fieldname": "additional_salary", + "fieldtype": "Link", + "label": "Additional Salary ", + "options": "Additional Salary", + "read_only": 1 } ], "istable": 1, "links": [], - "modified": "2019-12-31 17:15:25.646689", + "modified": "2020-03-02 18:16:38.571005", "modified_by": "Administrator", "module": "HR", "name": "Salary Detail", diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index d03a3dd9a3..ac55c679a6 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -442,6 +442,7 @@ class SalarySlip(TransactionBase): if amount: self.append(key, { 'amount': amount, + 'additional_salary': '' if not struct_row.get("additional_salary") else struct_row.get("additional_salary"), 'default_amount': amount if not struct_row.get("is_additional_component") else 0, 'depends_on_payment_days' : struct_row.depends_on_payment_days, 'salary_component' : struct_row.salary_component, @@ -790,14 +791,6 @@ class SalarySlip(TransactionBase): "repay_from_salary": 1, }) - - def update_salary_slip_in_additional_salary(self): - salary_slip = self.name if self.docstatus==1 else None - frappe.db.sql(""" - update `tabAdditional Salary` set salary_slip=%s - where employee=%s and payroll_date between %s and %s and docstatus=1 - """, (salary_slip, self.employee, self.start_date, self.end_date)) - def make_loan_repayment_entry(self): for loan in self.loans: repayment_entry = create_repayment_entry(loan.loan, self.employee, From 3ced42f302fcae2983b8b4022805b8c2d684bc14 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Fri, 13 Mar 2020 19:14:56 +0530 Subject: [PATCH 07/27] feat: reverse linking and multiple additonal salaries in salary slip --- .../additional_salary/additional_salary.py | 63 ++++++++++++------- .../employee_advance/employee_advance.json | 3 +- .../employee_incentive/employee_incentive.py | 32 +++------- .../leave_encashment/leave_encashment.py | 11 ++-- erpnext/hr/doctype/salary_slip/salary_slip.py | 26 ++++---- 5 files changed, 72 insertions(+), 63 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/hr/doctype/additional_salary/additional_salary.py index d8f7444beb..4076ffef18 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/additional_salary.py @@ -9,6 +9,13 @@ from frappe import _ from frappe.utils import getdate, date_diff class AdditionalSalary(Document): + + def on_submit(self): + if self.ref_doctype == "Employee Advance" and self.ref_docname: + emp_adv = frappe.get_doc(self.ref_doctype, self.ref_docname) + emp_adv.return_amount = self.amount + emp_adv.save() + def before_insert(self): if frappe.db.exists("Additional Salary", {"employee": self.employee, "salary_component": self.salary_component, "amount": self.amount, "payroll_date": self.payroll_date, "company": self.company, "docstatus": 1}): @@ -49,8 +56,8 @@ class AdditionalSalary(Document): @frappe.whitelist() def get_additional_salary_component(employee, start_date, end_date, component_type): - additional_components = frappe.db.sql(""" - select name, salary_component, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date + additional_salaries = frappe.db.sql(""" + select name, salary_component, type, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date from `tabAdditional Salary` where employee=%(employee)s and docstatus = 1 @@ -60,7 +67,7 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty from_date <= %(to_date)s and to_date >= %(to_date)s ) and type = %(component_type)s - order by salary_component, overwrite_salary_structure_amount + order by salary_component, overwrite_salary_structure_amount DESC """, { 'employee': employee, 'from_date': start_date, @@ -68,26 +75,38 @@ def get_additional_salary_component(employee, start_date, end_date, component_ty 'component_type': "Earning" if component_type == "earnings" else "Deduction" }, as_dict=1) - additional_components_list = [] - existing_salary_components = [] + existing_salary_components= [] + salary_components_details = {} + additional_salary_details = [] + + overwrites_components = [ele.salary_component for ele in additional_salaries if ele.overwrite_salary_structure_amount == 1] + component_fields = ["depends_on_payment_days", "salary_component_abbr", "is_tax_applicable", "variable_based_on_taxable_salary", 'type'] - for d in additional_components: - if d.salary_component in existing_salary_components: - frappe.throw(_('Multiple additional Salary is created for Salary Component {0}'.format(d.salary_component))) - struct_row = frappe._dict({'salary_component': d.salary_component}) - component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields) - if component: - struct_row.update(component[0]) + for d in additional_salaries: - struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date - struct_row['is_additional_component'] = 1 - struct_row['additional_salary'] = d.name + if d.salary_component not in existing_salary_components: + component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields) + struct_row = frappe._dict({'salary_component': d.salary_component}) + if component: + struct_row.update(component[0]) + + struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date + struct_row['is_additional_component'] = 1 + + salary_components_details[d.salary_component] = struct_row + + + if overwrites_components.count(d.salary_component) > 1: + frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component: {0} between {1} and {2}.".format(d.salary_component, start_date, end_date)), title=_("Error")) + else: + additional_salary_details.append({ + 'name': d.name, + 'component': d.salary_component, + 'amount': d.amount, + 'type': d.type, + 'overwrite': d.overwrite_salary_structure_amount, + }) - additional_components_list.append(frappe._dict({ - 'amount': d.amount, - 'type': component[0].type, - 'struct_row': struct_row, - 'overwrite': d.overwrite_salary_structure_amount, - })) existing_salary_components.append(d.salary_component) - return additional_components_list \ No newline at end of file + + return salary_components_details, additional_salary_details \ No newline at end of file diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 1ab2356c8e..8c5ce42d87 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -165,6 +165,7 @@ "options": "Mode of Payment" }, { + "allow_on_submit": 1, "fieldname": "return_amount", "fieldtype": "Currency", "label": "Returned Amount", @@ -180,7 +181,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-05 16:31:50.417539", + "modified": "2020-03-06 15:11:33.747535", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", diff --git a/erpnext/hr/doctype/employee_incentive/employee_incentive.py b/erpnext/hr/doctype/employee_incentive/employee_incentive.py index 1cc52e1112..44763fc077 100644 --- a/erpnext/hr/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/hr/doctype/employee_incentive/employee_incentive.py @@ -9,27 +9,13 @@ from frappe.model.document import Document class EmployeeIncentive(Document): def on_submit(self): company = frappe.db.get_value('Employee', self.employee, 'company') - additional_salary = frappe.db.exists('Additional Salary', { - 'employee': self.employee, - 'ref_doctype': self.doctype, - 'ref_docname': self.name, - 'salary_component': self.salary_component, - 'payroll_date': self.payroll_date, - 'company': company, - 'docstatus': ["!=", 2] - }) - - if not additional_salary: - additional_salary = frappe.new_doc('Additional Salary') - additional_salary.employee = self.employee - additional_salary.salary_component = self.salary_component - additional_salary.amount = self.incentive_amount - additional_salary.payroll_date = self.payroll_date - additional_salary.company = company - additional_salary.ref_doctype = self.doctype - additional_salary.ref_docname = self.name - additional_salary.submit() - else: - incentive_added = frappe.db.get_value('Additional Salary', additional_salary, 'amount') + self.incentive_amount - frappe.db.set_value('Additional Salary', additional_salary, {'amount', incentive_added}) + additional_salary = frappe.new_doc('Additional Salary') + additional_salary.employee = self.employee + additional_salary.salary_component = self.salary_component + additional_salary.amount = self.incentive_amount + additional_salary.payroll_date = self.payroll_date + additional_salary.company = company + additional_salary.ref_doctype = self.doctype + additional_salary.ref_docname = self.name + additional_salary.submit() diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py index ad2cc02fd7..56f2f4ca08 100644 --- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py @@ -31,13 +31,16 @@ class LeaveEncashment(Document): additional_salary = frappe.new_doc("Additional Salary") additional_salary.company = frappe.get_value("Employee", self.employee, "company") additional_salary.employee = self.employee - additional_salary.salary_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") + earning_component = frappe.get_value("Leave Type", self.leave_type, "earning_component") + if not earning_component: + frappe.throw(_("Please set Earning Component for Leave type: {0}.".format(self.leave_type))) + additional_salary.salary_component = earning_component additional_salary.payroll_date = self.encashment_date additional_salary.amount = self.encashment_amount + additional_salary.ref_doctype = self.doctype + additional_salary.ref_docname = self.name additional_salary.submit() - self.db_set("additional_salary", additional_salary.name) - # Set encashed leaves in Allocation frappe.db.set_value("Leave Allocation", self.leave_allocation, "total_leaves_encashed", frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') + self.encashable_days) @@ -119,4 +122,4 @@ def create_leave_encashment(leave_allocation): leave_type=allocation.leave_type, encashment_date=allocation.to_date )) - leave_encashment.insert(ignore_permissions=True) + leave_encashment.insert(ignore_permissions=True) \ No newline at end of file diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index ac55c679a6..1d19427dc4 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -74,7 +74,6 @@ class SalarySlip(TransactionBase): def on_cancel(self): self.set_status() self.update_status() - self.update_salary_slip_in_additional_salary() self.cancel_loan_repayment_entry() def on_trash(self): @@ -405,14 +404,15 @@ class SalarySlip(TransactionBase): self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings") def add_additional_salary_components(self, component_type): - additional_components = get_additional_salary_component(self.employee, + salary_components_details, additional_salary_details = get_additional_salary_component(self.employee, self.start_date, self.end_date, component_type) - if additional_components: - for additional_component in additional_components: - amount = additional_component.amount - overwrite = additional_component.overwrite - self.update_component_row(frappe._dict(additional_component.struct_row), amount, - component_type, overwrite=overwrite) + if salary_components_details and additional_salary_details: + for additional_salary in additional_salary_details: + additional_salary =frappe._dict(additional_salary) + amount = additional_salary.amount + overwrite = additional_salary.overwrite + self.update_component_row(frappe._dict(salary_components_details[additional_salary.component]), amount, + component_type, overwrite=overwrite, additional_salary=additional_salary.name) def add_tax_components(self, payroll_period): # Calculate variable_based_on_taxable_salary after all components updated in salary slip @@ -432,21 +432,20 @@ class SalarySlip(TransactionBase): tax_row = self.get_salary_slip_row(d) self.update_component_row(tax_row, tax_amount, "deductions") - def update_component_row(self, struct_row, amount, key, overwrite=1): + def update_component_row(self, struct_row, amount, key, overwrite=1, additional_salary = ''): component_row = None for d in self.get(key): if d.salary_component == struct_row.salary_component: component_row = d - - if not component_row: + if not component_row or (struct_row.get("is_additional_component") and not overwrite): if amount: self.append(key, { 'amount': amount, - 'additional_salary': '' if not struct_row.get("additional_salary") else struct_row.get("additional_salary"), 'default_amount': amount if not struct_row.get("is_additional_component") else 0, 'depends_on_payment_days' : struct_row.depends_on_payment_days, 'salary_component' : struct_row.salary_component, 'abbr' : struct_row.abbr, + 'additional_salary': additional_salary, 'do_not_include_in_total' : struct_row.do_not_include_in_total, 'is_tax_applicable': struct_row.is_tax_applicable, 'is_flexible_benefit': struct_row.is_flexible_benefit, @@ -458,6 +457,7 @@ class SalarySlip(TransactionBase): if struct_row.get("is_additional_component"): if overwrite: component_row.additional_amount = amount - component_row.get("default_amount", 0) + component_row.additional_salary = additional_salary else: component_row.additional_amount = amount @@ -878,4 +878,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()) + return policy_template.format(**employee.as_dict()) \ No newline at end of file From fdacc50e788e810d1ee665d3606d55cf7f7c8ca4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 1 May 2020 19:04:00 +0530 Subject: [PATCH 08/27] Update salary_detail.json --- erpnext/hr/doctype/salary_detail/salary_detail.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json index 5ac7e69f29..92942d8a63 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/hr/doctype/salary_detail/salary_detail.json @@ -197,7 +197,7 @@ "fieldname": "additional_salary", "fieldtype": "Link", "label": "Additional Salary ", - "options": "Additional Salary", + "options": "Additional Salary" }, { "default": "0", @@ -220,4 +220,4 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC" -} \ No newline at end of file +} From ddf90b29f251a4c4b7f86293bb5d3cba9b4423a4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 1 May 2020 22:34:28 +0530 Subject: [PATCH 09/27] Update salary_slip.py --- erpnext/hr/doctype/salary_slip/salary_slip.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/salary_slip/salary_slip.py b/erpnext/hr/doctype/salary_slip/salary_slip.py index f93430229f..4d5c8437c6 100644 --- a/erpnext/hr/doctype/salary_slip/salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/salary_slip.py @@ -66,7 +66,6 @@ class SalarySlip(TransactionBase): else: self.set_status() self.update_status(self.name) - self.update_salary_slip_in_additional_salary() self.make_loan_repayment_entry() if (frappe.db.get_single_value("HR Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry: self.email_salary_slip() @@ -1024,4 +1023,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 f453a1c13f1e4e16e7089da35c5fc5f02167ede4 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 4 May 2020 12:36:01 +0530 Subject: [PATCH 10/27] Fix: requested Changes --- erpnext/hr/doctype/additional_salary/additional_salary.py | 4 +--- erpnext/hr/doctype/employee_advance/employee_advance.js | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.py b/erpnext/hr/doctype/additional_salary/additional_salary.py index 4076ffef18..bab6fb545f 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/additional_salary.py @@ -12,9 +12,7 @@ class AdditionalSalary(Document): def on_submit(self): if self.ref_doctype == "Employee Advance" and self.ref_docname: - emp_adv = frappe.get_doc(self.ref_doctype, self.ref_docname) - emp_adv.return_amount = self.amount - emp_adv.save() + frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount) def before_insert(self): if frappe.db.exists("Additional Salary", {"employee": self.employee, "salary_component": self.salary_component, diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js index 38561d41a8..6cc49cfff2 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.js +++ b/erpnext/hr/doctype/employee_advance/employee_advance.js @@ -76,7 +76,6 @@ frappe.ui.form.on('Employee Advance', { doc: frm.doc }, callback: function (r){ - console.log("Helloxs") var doclist = frappe.model.sync(r.message); frappe.set_route("Form", doclist[0].doctype, doclist[0].name); } From 09572d989272a5f73d18c1cfaa3af238f73dde27 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 4 May 2020 17:07:15 +0530 Subject: [PATCH 11/27] patch: to link additional salary with salary slip and leave encashment, incentive with additional salary --- .../doctype/salary_detail/salary_detail.json | 2 +- erpnext/patches.txt | 1 + ...itional_salary_encashment_and_incentive.py | 35 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py diff --git a/erpnext/hr/doctype/salary_detail/salary_detail.json b/erpnext/hr/doctype/salary_detail/salary_detail.json index 92942d8a63..fe5f83b532 100644 --- a/erpnext/hr/doctype/salary_detail/salary_detail.json +++ b/erpnext/hr/doctype/salary_detail/salary_detail.json @@ -211,7 +211,7 @@ ], "istable": 1, "links": [], - "modified": "2020-04-24 20:00:16.475295", + "modified": "2020-04-04 20:00:16.475295", "modified_by": "Administrator", "module": "HR", "name": "Salary Detail", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5295399695..533a349072 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -677,3 +677,4 @@ erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype +erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive #gyhdghksdhsjsd diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py new file mode 100644 index 0000000000..8b8e60db0b --- /dev/null +++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py @@ -0,0 +1,35 @@ +from __future__ import unicode_literals + +import frappe + +def execute(): + + additional_salaries = frappe.get_all("Additional Salary", fields = ['name', "salary_slip", "type", "salary_component"], group_by = 'salary_slip') + leave_encashments = frappe.get_all("Leave Encashment", fields = ["name","additional_salary"]) + employee_incentives = frappe.get_all("Employee Incentive", fields= ["name", "additional_salary"]) + + for incentive in employee_incentives: + frappe.db.sql(""" UPDATE `tabAdditional Salary` + SET ref_doctype = 'Employee Incentive', ref_docname = %s + WHERE name = %s + """, (incentive['name'], incentive['additional_salary'])) + + + for leave_encashment in leave_encashments: + frappe.db.sql(""" UPDATE `tabAdditional Salary` + SET ref_doctype = 'Leave Encashment', ref_docname = %s + WHERE name = %s + """, (leave_encashment['name'], leave_encashment['additional_salary'])) + + salary_slips = [sal["salary_slip"] for sal in additional_salaries] + + for salary in additional_salaries: + comp_type = "earnings" if salary['type'] == 'Earning' else 'deductions' + if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: + frappe.db.sql(""" UPDATE `tabsalary Detail` + SET additional_salary = %s + WHERE parenttype = 'Salary Slip' + and parentfield = %s + and parent = %s + and salary_component = %s""", (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"])) + From 94719e7e818dd6f5ae6b1fc02a32812bee443a02 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 4 May 2020 18:16:47 +0530 Subject: [PATCH 12/27] fix: tests --- .../hr/doctype/additional_salary/additional_salary.json | 2 +- .../hr/doctype/leave_encashment/test_leave_encashment.py | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/additional_salary.json b/erpnext/hr/doctype/additional_salary/additional_salary.json index bf9d8192d0..bfb543f49a 100644 --- a/erpnext/hr/doctype/additional_salary/additional_salary.json +++ b/erpnext/hr/doctype/additional_salary/additional_salary.json @@ -163,7 +163,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2020-03-02 18:06:29.170878", + "modified": "2020-04-04 18:06:29.170878", "modified_by": "Administrator", "module": "HR", "name": "Additional Salary", diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index e5bd170bc4..2819e1673c 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -53,7 +53,10 @@ class TestLeaveEncashment(unittest.TestCase): self.assertEqual(leave_encashment.encashment_amount, 250) leave_encashment.submit() - self.assertTrue(frappe.db.get_value("Leave Encashment", leave_encashment.name, "additional_salary")) + + # assert links + add_sal = frappe.get_all("Additional salary", filters = {"ref_docname": leave_encashment.name})[0] + self.assertTrue(add_sal) def test_creation_of_leave_ledger_entry_on_submit(self): frappe.db.sql('''delete from `tabLeave Encashment`''') @@ -75,5 +78,8 @@ class TestLeaveEncashment(unittest.TestCase): self.assertEquals(leave_ledger_entry[0].leaves, leave_encashment.encashable_days * -1) # check if leave ledger entry is deleted on cancellation + + frappe.db.sql("Delete from `tabAdditional Salary` WHERE ref_docname = %s", (leave_encashment.name) ) + leave_encashment.cancel() self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_encashment.name})) From 232396494bd36081df275311b7449acc79c61e5c Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Mon, 4 May 2020 19:20:55 +0530 Subject: [PATCH 13/27] fix(patch): reload GSTR 3B report --- erpnext/patches/v11_0/add_permissions_in_gst_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py index d7936110ed..83b2a4cc09 100644 --- a/erpnext/patches/v11_0/add_permissions_in_gst_settings.py +++ b/erpnext/patches/v11_0/add_permissions_in_gst_settings.py @@ -7,4 +7,5 @@ def execute(): return frappe.reload_doc("regional", "doctype", "lower_deduction_certificate") - add_permissions() \ No newline at end of file + frappe.reload_doc("regional", "doctype", "gstr_3b_report") + add_permissions() From 8e08698c158a3be19fdc9da1bba2c578a3d82196 Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Tue, 5 May 2020 12:06:58 +0530 Subject: [PATCH 14/27] test: Fix wrong test for search function (#21588) --- erpnext/tests/test_search.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py index b9665dbd76..566495f1ec 100644 --- a/erpnext/tests/test_search.py +++ b/erpnext/tests/test_search.py @@ -4,11 +4,13 @@ import frappe from frappe.contacts.address_and_contact import filter_dynamic_link_doctypes class TestSearch(unittest.TestCase): - #Search for the word "cond", part of the word "conduire" (Lead) in french. + # Search for the word "cond", part of the word "conduire" (Lead) in french. def test_contact_search_in_foreign_language(self): frappe.local.lang = 'fr' - output = filter_dynamic_link_doctypes("DocType", "prospect", "name", 0, 20, {'fieldtype': 'HTML', 'fieldname': 'contact_html'}) - + output = filter_dynamic_link_doctypes("DocType", "cond", "name", 0, 20, { + 'fieldtype': 'HTML', + 'fieldname': 'contact_html' + }) result = [['found' for x in y if x=="Lead"] for y in output] self.assertTrue(['found'] in result) From 5210761e570f1c9feabddea9a7fcd2c4bc7415bf Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 5 May 2020 12:12:33 +0530 Subject: [PATCH 15/27] fix: heatmap not working for customer and supplier (#21578) Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Co-authored-by: Nabin Hait --- erpnext/accounts/party.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 6e5b33f07d..528fb4e113 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -162,7 +162,7 @@ def get_default_price_list(party): def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list price_list = get_permitted_documents('Price List') - + # if there is only one permitted document based on user permissions, set it if price_list and len(price_list) == 1: price_list = price_list[0] @@ -465,23 +465,25 @@ def get_timeline_data(doctype, name): from frappe.desk.form.load import get_communication_data out = {} - fields = 'date(creation), count(name)' + fields = 'creation, count(*)' after = add_years(None, -1).strftime('%Y-%m-%d') - group_by='group by date(creation)' + group_by='group by Date(creation)' - data = get_communication_data(doctype, name, after=after, group_by='group by date(creation)', - fields='date(C.creation) as creation, count(C.name)',as_dict=False) + data = get_communication_data(doctype, name, after=after, group_by='group by creation', + fields='C.creation as creation, count(C.name)',as_dict=False) # fetch and append data from Activity Log data += frappe.db.sql("""select {fields} from `tabActivity Log` - where (reference_doctype="{doctype}" and reference_name="{name}") - or (timeline_doctype in ("{doctype}") and timeline_name="{name}") - or (reference_doctype in ("Quotation", "Opportunity") and timeline_name="{name}") + where (reference_doctype=%(doctype)s and reference_name=%(name)s) + or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s) + or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s) and status!='Success' and creation > {after} {group_by} order by creation desc - """.format(doctype=frappe.db.escape(doctype), name=frappe.db.escape(name), fields=fields, - group_by=group_by, after=after), as_dict=False) + """.format(fields=fields, group_by=group_by, after=after), { + "doctype": doctype, + "name": name + }, as_dict=False) timeline_items = dict(data) From 5e3d8050aa6c82cbb1862c9bf1ee8b37d9b41f5b Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 5 May 2020 14:37:39 +0530 Subject: [PATCH 16/27] fix: Handle rows with same item code from Purchase Receipt to Invoice. (#20724) * fix: Handle rows with same item code from Purchase Receipt to Invoice. * fix: Added patch, fixed tests, fixed delivery note behaviour * chore: Added comments amd fixed typo * fix: Added patch to patches.txt * fix: Patch fix and simplification, json timestamp updation. Co-authored-by: Nabin Hait --- .../controllers/sales_and_purchase_return.py | 5 +- erpnext/patches.txt | 1 + ...t_purchase_receipt_delivery_note_detail.py | 84 +++++++++++++++++++ .../doctype/delivery_note/delivery_note.py | 11 ++- .../delivery_note/test_delivery_note.py | 5 +- .../delivery_note_item.json | 10 +++ .../purchase_receipt/purchase_receipt.py | 9 +- .../purchase_receipt/test_purchase_receipt.py | 5 +- .../purchase_receipt_item.json | 12 ++- 9 files changed, 127 insertions(+), 15 deletions(-) create mode 100644 erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 81fdbbefc3..90c67f1e52 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -74,7 +74,7 @@ def validate_returned_items(doc): for d in doc.get("items"): if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0): if d.item_code not in valid_items: - frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") + frappe.throw(_("Row # {0}: Returned Item {1} does not exist in {2} {3}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) else: ref = valid_items.get(d.item_code, frappe._dict()) @@ -266,6 +266,8 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.purchase_order = source_doc.purchase_order target_doc.purchase_order_item = source_doc.purchase_order_item target_doc.rejected_warehouse = source_doc.rejected_warehouse + target_doc.purchase_receipt_item = source_doc.name + elif doctype == "Purchase Invoice": target_doc.received_qty = -1* source_doc.received_qty target_doc.rejected_qty = -1* source_doc.rejected_qty @@ -282,6 +284,7 @@ def make_return_doc(doctype, source_name, target_doc=None): target_doc.so_detail = source_doc.so_detail target_doc.si_detail = source_doc.si_detail target_doc.expense_account = source_doc.expense_account + target_doc.dn_detail = source_doc.name if default_warehouse_for_sales_return: target_doc.warehouse = default_warehouse_for_sales_return elif doctype == "Sales Invoice": diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 5295399695..8a31cf33d0 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -662,6 +662,7 @@ erpnext.patches.v12_0.create_irs_1099_field_united_states erpnext.patches.v12_0.move_bank_account_swift_number_to_bank erpnext.patches.v12_0.rename_bank_reconciliation erpnext.patches.v12_0.rename_bank_reconciliation_fields # 2020-01-22 +erpnext.patches.v12_0.set_purchase_receipt_delivery_note_detail erpnext.patches.v12_0.add_permission_in_lower_deduction erpnext.patches.v12_0.set_received_qty_in_material_request_as_per_stock_uom erpnext.patches.v12_0.rename_account_type_doctype diff --git a/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py new file mode 100644 index 0000000000..f5bd8c3aa2 --- /dev/null +++ b/erpnext/patches/v12_0/set_purchase_receipt_delivery_note_detail.py @@ -0,0 +1,84 @@ +from __future__ import unicode_literals +import frappe +from collections import defaultdict + +def execute(): + def map_rows(doc_row, return_doc_row, detail_field, doctype): + """Map rows after identifying similar ones.""" + + frappe.db.sql(""" UPDATE `tab{doctype} Item` set {detail_field} = '{doc_row_name}' + where name = '{return_doc_row_name}'""" \ + .format(doctype=doctype, + detail_field=detail_field, + doc_row_name=doc_row.get('name'), + return_doc_row_name=return_doc_row.get('name'))) #nosec + + def row_is_mappable(doc_row, return_doc_row, detail_field): + """Checks if two rows are similar enough to be mapped.""" + + if doc_row.item_code == return_doc_row.item_code and not return_doc_row.get(detail_field): + if doc_row.get('batch_no') and return_doc_row.get('batch_no') and doc_row.batch_no == return_doc_row.batch_no: + return True + + elif doc_row.get('serial_no') and return_doc_row.get('serial_no'): + doc_sn = doc_row.serial_no.split('\n') + return_doc_sn = return_doc_row.serial_no.split('\n') + + if set(doc_sn) & set(return_doc_sn): + # if two rows have serial nos in common, map them + return True + + elif doc_row.rate == return_doc_row.rate: + return True + else: + return False + + def make_return_document_map(doctype, return_document_map): + """Returns a map of documents and it's return documents. + Format => { 'document' : ['return_document_1','return_document_2'] }""" + + return_against_documents = frappe.db.sql(""" + SELECT + return_against as document, name as return_document + FROM `tab{doctype}` + WHERE + is_return = 1 and docstatus = 1""".format(doctype=doctype),as_dict=1) #nosec + + for entry in return_against_documents: + return_document_map[entry.document].append(entry.return_document) + + return return_document_map + + def set_document_detail_in_return_document(doctype): + """Map each row of the original document in the return document.""" + mapped = [] + return_document_map = defaultdict(list) + detail_field = "purchase_receipt_item" if doctype=="Purchase Receipt" else "dn_detail" + + child_doc = frappe.scrub("{0} Item".format(doctype)) + frappe.reload_doc("stock", "doctype", child_doc) + + return_document_map = make_return_document_map(doctype, return_document_map) + + #iterate through original documents and its return documents + for docname in return_document_map: + doc_items = frappe.get_doc(doctype, docname).get("items") + for return_doc in return_document_map[docname]: + return_doc_items = frappe.get_doc(doctype, return_doc).get("items") + + #iterate through return document items and original document items for mapping + for return_item in return_doc_items: + for doc_item in doc_items: + if row_is_mappable(doc_item, return_item, detail_field) and doc_item.get('name') not in mapped: + map_rows(doc_item, return_item, detail_field, doctype) + mapped.append(doc_item.get('name')) + break + else: + continue + + set_document_detail_in_return_document("Purchase Receipt") + set_document_detail_in_return_document("Delivery Note") + frappe.db.commit() + + + diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index 37f9097937..d04cf785ab 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -388,13 +388,12 @@ def get_invoiced_qty_map(delivery_note): def get_returned_qty_map(delivery_note): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.item_code, sum(abs(dn_item.qty)) as qty + returned_qty_map = frappe._dict(frappe.db.sql("""select dn_item.dn_detail, abs(dn_item.qty) as qty from `tabDelivery Note Item` dn_item, `tabDelivery Note` dn where dn.name = dn_item.parent and dn.docstatus = 1 and dn.is_return = 1 and dn.return_against = %s - group by dn_item.item_code """, delivery_note)) return returned_qty_map @@ -413,7 +412,7 @@ def make_sales_invoice(source_name, target_doc=None): target.run_method("set_po_nos") if len(target.get("items")) == 0: - frappe.throw(_("All these items have already been invoiced")) + frappe.throw(_("All these items have already been Invoiced/Returned")) target.run_method("calculate_taxes_and_totals") @@ -438,9 +437,9 @@ def make_sales_invoice(source_name, target_doc=None): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) returned_qty = 0 - if returned_qty_map.get(item_row.item_code, 0) > 0: - returned_qty = flt(returned_qty_map.get(item_row.item_code, 0)) - returned_qty_map[item_row.item_code] -= pending_qty + if returned_qty_map.get(item_row.name, 0) > 0: + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) + returned_qty_map[item_row.name] -= pending_qty if returned_qty: if returned_qty >= pending_qty: diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py index bf7007abee..a921a56f53 100644 --- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py @@ -612,6 +612,7 @@ class TestDeliveryNote(unittest.TestCase): dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-1, do_not_submit=True) dn1.items[0].against_sales_order = so.name dn1.items[0].so_detail = so.items[0].name + dn1.items[0].dn_detail = dn.items[0].name dn1.submit() si = make_sales_invoice(dn.name) @@ -638,7 +639,9 @@ class TestDeliveryNote(unittest.TestCase): si1.save() si1.submit() - create_delivery_note(is_return=1, return_against=dn.name, qty=-2) + dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, do_not_submit=True) + dn1.items[0].dn_detail = dn.items[0].name + dn1.submit() si2 = make_sales_invoice(dn.name) self.assertEquals(si2.items[0].qty, 2) diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json index 782ac84e57..7ea2de2753 100644 --- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json +++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json @@ -67,6 +67,7 @@ "so_detail", "against_sales_invoice", "si_detail", + "dn_detail", "section_break_40", "batch_no", "serial_no", @@ -699,6 +700,15 @@ { "fieldname": "dimension_col_break", "fieldtype": "Column Break" + }, + { + "fieldname": "dn_detail", + "fieldtype": "Data", + "hidden": 1, + "label": "Against Delivery Note Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8dfe1d1030..e6ab8d634d 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -504,7 +504,7 @@ def make_purchase_invoice(source_name, target_doc=None): def set_missing_values(source, target): if len(target.get("items")) == 0: - frappe.throw(_("All items have already been invoiced")) + frappe.throw(_("All items have already been Invoiced/Returned")) doc = frappe.get_doc(target) doc.ignore_pricing_rule = 1 @@ -514,11 +514,11 @@ def make_purchase_invoice(source_name, target_doc=None): def update_item(source_doc, target_doc, source_parent): target_doc.qty, returned_qty = get_pending_qty(source_doc) - returned_qty_map[source_doc.item_code] = returned_qty + returned_qty_map[source_doc.name] = returned_qty def get_pending_qty(item_row): pending_qty = item_row.qty - invoiced_qty_map.get(item_row.name, 0) - returned_qty = flt(returned_qty_map.get(item_row.item_code, 0)) + returned_qty = flt(returned_qty_map.get(item_row.name, 0)) if returned_qty: if returned_qty >= pending_qty: pending_qty = 0 @@ -576,13 +576,12 @@ def get_invoiced_qty_map(purchase_receipt): def get_returned_qty_map(purchase_receipt): """returns a map: {so_detail: returned_qty}""" - returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.item_code, sum(abs(pr_item.qty)) as qty + returned_qty_map = frappe._dict(frappe.db.sql("""select pr_item.purchase_receipt_item, abs(pr_item.qty) as qty from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr where pr.name = pr_item.parent and pr.docstatus = 1 and pr.is_return = 1 and pr.return_against = %s - group by pr_item.item_code """, purchase_receipt)) return returned_qty_map diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 3d42590e4c..649cfdcaac 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -475,6 +475,7 @@ class TestPurchaseReceipt(unittest.TestCase): pr1 = make_purchase_receipt(is_return=1, return_against=pr.name, qty=-1, do_not_submit=True) pr1.items[0].purchase_order = po.name pr1.items[0].purchase_order_item = po.items[0].name + pr1.items[0].purchase_receipt_item = pr.items[0].name pr1.submit() pi = make_purchase_invoice(pr.name) @@ -498,7 +499,9 @@ class TestPurchaseReceipt(unittest.TestCase): pi1.save() pi1.submit() - make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2) + pr2 = make_purchase_receipt(is_return=1, return_against=pr1.name, qty=-2, do_not_submit=True) + pr2.items[0].purchase_receipt_item = pr1.items[0].name + pr2.submit() pi2 = make_purchase_invoice(pr1.name) self.assertEquals(pi2.items[0].qty, 2) diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json index bc6bce95d6..c1e1f901ba 100644 --- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json +++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json @@ -71,6 +71,7 @@ "quality_inspection", "purchase_order_item", "material_request_item", + "purchase_receipt_item", "section_break_45", "allow_zero_valuation_rate", "bom", @@ -820,6 +821,15 @@ "label": "Supplier Warehouse", "options": "Warehouse" }, + { + "fieldname": "purchase_receipt_item", + "fieldtype": "Data", + "hidden": 1, + "label": "Purchase Receipt Item", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 + }, { "collapsible": 1, "fieldname": "image_column", @@ -829,7 +839,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-10 19:01:21.154963", + "modified": "2020-04-28 19:01:21.154963", "modified_by": "Administrator", "module": "Stock", "name": "Purchase Receipt Item", From 5a1a765ebf38b95b793f085da32aec3ae639b1ef Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 May 2020 15:37:17 +0530 Subject: [PATCH 17/27] Update test_leave_encashment.py --- erpnext/hr/doctype/leave_encashment/test_leave_encashment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py index 2819e1673c..ac7755b23a 100644 --- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py +++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py @@ -55,7 +55,7 @@ class TestLeaveEncashment(unittest.TestCase): leave_encashment.submit() # assert links - add_sal = frappe.get_all("Additional salary", filters = {"ref_docname": leave_encashment.name})[0] + add_sal = frappe.get_all("Additional Salary", filters = {"ref_docname": leave_encashment.name})[0] self.assertTrue(add_sal) def test_creation_of_leave_ledger_entry_on_submit(self): From 672b74d89c1f80afcca325bfcdb76b0286379787 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 May 2020 15:58:52 +0530 Subject: [PATCH 18/27] fix: Patch and tests --- .../test_additional_salary.py | 5 +- .../doctype/salary_slip/test_salary_slip.py | 56 ++++++++++--------- erpnext/patches.txt | 2 +- ...itional_salary_encashment_and_incentive.py | 43 +++++++++----- 4 files changed, 62 insertions(+), 44 deletions(-) diff --git a/erpnext/hr/doctype/additional_salary/test_additional_salary.py b/erpnext/hr/doctype/additional_salary/test_additional_salary.py index ebe6b20d81..6f93fb5df8 100644 --- a/erpnext/hr/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/hr/doctype/additional_salary/test_additional_salary.py @@ -7,14 +7,13 @@ import frappe, erpnext from frappe.utils import nowdate, add_days from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.salary_component.test_salary_component import create_salary_component -from erpnext.hr.doctype.salary_slip.test_salary_slip import make_employee_salary_slip +from erpnext.hr.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, setup_test class TestAdditionalSalary(unittest.TestCase): def setUp(self): - from erpnext.hr.doctype.salary_slip.test_salary_slip import TestSalarySlip - TestSalarySlip().setUp() + setup_test() def test_recurring_additional_salary(self): emp_id = make_employee("test_additional@salary.com") diff --git a/erpnext/hr/doctype/salary_slip/test_salary_slip.py b/erpnext/hr/doctype/salary_slip/test_salary_slip.py index fc687a355c..a7dcb94167 100644 --- a/erpnext/hr/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/hr/doctype/salary_slip/test_salary_slip.py @@ -18,19 +18,7 @@ from erpnext.hr.doctype.employee_tax_exemption_declaration.test_employee_tax_exe class TestSalarySlip(unittest.TestCase): def setUp(self): - make_earning_salary_component(setup=True, company_list=["_Test Company"]) - make_deduction_salary_component(setup=True, company_list=["_Test Company"]) - - for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance"]: - frappe.db.sql("delete from `tab%s`" % dt) - - self.make_holiday_list() - - frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") - frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) - frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) - frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) - + setup_test() def tearDown(self): frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0) frappe.set_user("Administrator") @@ -374,19 +362,6 @@ class TestSalarySlip(unittest.TestCase): # undelete fixture data frappe.db.rollback() - def make_holiday_list(self): - fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) - if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): - holiday_list = frappe.get_doc({ - "doctype": "Holiday List", - "holiday_list_name": "Salary Slip Test Holiday List", - "from_date": fiscal_year[1], - "to_date": fiscal_year[2], - "weekly_off": "Sunday" - }).insert() - holiday_list.get_weekly_off_dates() - holiday_list.save() - def make_activity_for_employee(self): activity_type = frappe.get_doc("Activity Type", "_Test Activity Type") activity_type.billing_rate = 50 @@ -702,4 +677,31 @@ def make_leave_application(employee, from_date, to_date, leave_type, company=Non status = "Approved", leave_approver = 'test@example.com' )) - leave_application.submit() \ No newline at end of file + leave_application.submit() + +def setup_test(): + make_earning_salary_component(setup=True, company_list=["_Test Company"]) + make_deduction_salary_component(setup=True, company_list=["_Test Company"]) + + for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Attendance"]: + frappe.db.sql("delete from `tab%s`" % dt) + + make_holiday_list() + + frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") + frappe.db.set_value("HR Settings", None, "email_salary_slip_to_employee", 0) + frappe.db.set_value('HR Settings', None, 'leave_status_notification_template', None) + frappe.db.set_value('HR Settings', None, 'leave_approval_notification_template', None) + +def make_holiday_list(): + fiscal_year = get_fiscal_year(nowdate(), company=erpnext.get_default_company()) + if not frappe.db.get_value("Holiday List", "Salary Slip Test Holiday List"): + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "holiday_list_name": "Salary Slip Test Holiday List", + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "weekly_off": "Sunday" + }).insert() + holiday_list.get_weekly_off_dates() + holiday_list.save() diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 8a6a83e2f6..c9642924dc 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -678,4 +678,4 @@ erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry erpnext.patches.v12_0.retain_permission_rules_for_video_doctype -erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive #gyhdghksdhsjsd +erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive diff --git a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py index 8b8e60db0b..ddcadcb4de 100644 --- a/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py +++ b/erpnext/patches/v13_0/patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive.py @@ -3,33 +3,50 @@ from __future__ import unicode_literals import frappe def execute(): + if not frappe.db.table_exists("Additional Salary"): + return - additional_salaries = frappe.get_all("Additional Salary", fields = ['name', "salary_slip", "type", "salary_component"], group_by = 'salary_slip') - leave_encashments = frappe.get_all("Leave Encashment", fields = ["name","additional_salary"]) - employee_incentives = frappe.get_all("Employee Incentive", fields= ["name", "additional_salary"]) + for doctype in ("Additional Salary", "Leave Encashment", "Employee Incentive", "Salary Detail"): + frappe.reload_doc("hr", "doctype", doctype) + + additional_salaries = frappe.get_all("Additional Salary", + fields = ['name', "salary_slip", "type", "salary_component"], + filters = {'salary_slip': ['!=', '']}, + group_by = 'salary_slip' + ) + leave_encashments = frappe.get_all("Leave Encashment", + fields = ["name","additional_salary"], + filters = {'additional_salary': ['!=', '']} + ) + employee_incentives = frappe.get_all("Employee Incentive", + fields= ["name", "additional_salary"], + filters = {'additional_salary': ['!=', '']} + ) for incentive in employee_incentives: frappe.db.sql(""" UPDATE `tabAdditional Salary` - SET ref_doctype = 'Employee Incentive', ref_docname = %s - WHERE name = %s - """, (incentive['name'], incentive['additional_salary'])) + SET ref_doctype = 'Employee Incentive', ref_docname = %s + WHERE name = %s + """, (incentive['name'], incentive['additional_salary'])) for leave_encashment in leave_encashments: frappe.db.sql(""" UPDATE `tabAdditional Salary` - SET ref_doctype = 'Leave Encashment', ref_docname = %s - WHERE name = %s - """, (leave_encashment['name'], leave_encashment['additional_salary'])) + SET ref_doctype = 'Leave Encashment', ref_docname = %s + WHERE name = %s + """, (leave_encashment['name'], leave_encashment['additional_salary'])) salary_slips = [sal["salary_slip"] for sal in additional_salaries] for salary in additional_salaries: comp_type = "earnings" if salary['type'] == 'Earning' else 'deductions' if salary["salary_slip"] and salary_slips.count(salary["salary_slip"]) == 1: - frappe.db.sql(""" UPDATE `tabsalary Detail` - SET additional_salary = %s - WHERE parenttype = 'Salary Slip' + frappe.db.sql(""" + UPDATE `tabSalary Detail` + SET additional_salary = %s + WHERE parenttype = 'Salary Slip' and parentfield = %s and parent = %s - and salary_component = %s""", (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"])) + and salary_component = %s + """, (salary["name"], comp_type, salary["salary_slip"], salary["salary_component"])) From 98229db3ae4ed31271ccd36b0e2c24cf0d2316c8 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 5 May 2020 16:02:16 +0530 Subject: [PATCH 19/27] feat: Default Dashboard for Healthcare Workspace (#21414) * feat: Default Dashboard for Healthcare Workspace * fix: patch * Update add_healthcare_dashboard.py * fix: failing patch test Co-authored-by: Nabin Hait Co-authored-by: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> --- .../desk_page/healthcare/healthcare.json | 9 +++++++-- erpnext/patches.txt | 2 +- .../patches/v12_0/add_default_dashboards.py | 1 + .../setup_wizard/data/dashboard_charts.py | 18 +++++++++++++++++- 4 files changed, 26 insertions(+), 4 deletions(-) diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json index 24c6d6fc37..5cf09b34b2 100644 --- a/erpnext/healthcare/desk_page/healthcare/healthcare.json +++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json @@ -47,7 +47,12 @@ } ], "category": "Domains", - "charts": [], + "charts": [ + { + "chart_name": "Patient Appointments", + "label": "Patient Appointments" + } + ], "charts_label": "", "creation": "2020-03-02 17:23:17.919682", "developer_mode_only": 0, @@ -58,7 +63,7 @@ "idx": 0, "is_standard": 1, "label": "Healthcare", - "modified": "2020-04-20 11:42:43.889576", + "modified": "2020-04-25 22:31:36.576444", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c9642924dc..ba17b67de1 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -630,7 +630,7 @@ execute:frappe.reload_doc('desk', 'doctype', 'dashboard') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_source') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart') execute:frappe.reload_doc('desk', 'doctype', 'dashboard_chart_field') -erpnext.patches.v12_0.add_default_dashboards +erpnext.patches.v12_0.add_default_dashboards # 2020-04-05 erpnext.patches.v12_0.remove_bank_remittance_custom_fields erpnext.patches.v12_0.generate_leave_ledger_entries execute:frappe.delete_doc_if_exists("Report", "Loan Repayment") diff --git a/erpnext/patches/v12_0/add_default_dashboards.py b/erpnext/patches/v12_0/add_default_dashboards.py index 0c3f2f86ae..24661ccfea 100644 --- a/erpnext/patches/v12_0/add_default_dashboards.py +++ b/erpnext/patches/v12_0/add_default_dashboards.py @@ -6,4 +6,5 @@ from erpnext.setup.setup_wizard.operations.install_fixtures import add_dashboard def execute(): frappe.reload_doc("desk", "doctype", "number_card_link") + frappe.reload_doc("desk", "doctype", "patient_appointment") add_dashboards() diff --git a/erpnext/setup/setup_wizard/data/dashboard_charts.py b/erpnext/setup/setup_wizard/data/dashboard_charts.py index bb8c1319bf..b182dfc103 100644 --- a/erpnext/setup/setup_wizard/data/dashboard_charts.py +++ b/erpnext/setup/setup_wizard/data/dashboard_charts.py @@ -29,7 +29,8 @@ def get_default_dashboards(): { "chart": "Incoming Bills (Purchase Invoice)" }, { "chart": "Bank Balance" }, { "chart": "Income" }, - { "chart": "Expenses" } + { "chart": "Expenses" }, + { "chart": "Patient Appointments" } ] } ], @@ -107,6 +108,21 @@ def get_default_dashboards(): "document_type": "Sales Invoice", "type": "Bar", "width": "Half" + }, + { + "doctype": "Dashboard Chart", + "time_interval": "Daily", + "chart_name": "Patient Appointments", + "timespan": "Last Month", + "color": "#77ecca", + "filters_json": json.dumps({}), + "chart_type": "Count", + "timeseries": 1, + "based_on": "appointment_datetime", + "owner": "Administrator", + "document_type": "Patient Appointment", + "type": "Line", + "width": "Half" } ] } From ddcc4fa9b966fc14e38c02f88a2bd0cbca4fa9ef Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Tue, 5 May 2020 16:08:05 +0530 Subject: [PATCH 20/27] Feat: Monthly attendance sheet Enhancements and UI/UX Improvements (#21331) * refactor: Employee attendance sheet redesign * feat: Added weekly off status in monthly Attendance sheet Co-authored-by: Nabin Hait --- erpnext/hr/doctype/holiday/holiday.json | 133 ++++------ .../hr/doctype/holiday_list/holiday_list.py | 1 + .../monthly_attendance_sheet.js | 12 + .../monthly_attendance_sheet.py | 236 ++++++++++++------ 4 files changed, 232 insertions(+), 150 deletions(-) diff --git a/erpnext/hr/doctype/holiday/holiday.json b/erpnext/hr/doctype/holiday/holiday.json index 6498530eb1..6bd0ab0dcb 100644 --- a/erpnext/hr/doctype/holiday/holiday.json +++ b/erpnext/hr/doctype/holiday/holiday.json @@ -1,87 +1,60 @@ { - "allow_copy": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:27:46", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 1, + "actions": [], + "creation": "2013-02-22 01:27:46", + "doctype": "DocType", + "document_type": "Setup", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "holiday_date", + "column_break_2", + "weekly_off", + "section_break_4", + "description" + ], "fields": [ { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "holiday_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Date", - "length": 0, - "no_copy": 0, - "oldfieldname": "holiday_date", - "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "holiday_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date", + "oldfieldname": "holiday_date", + "oldfieldtype": "Date", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "label": "Description", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "read_only": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text Editor", + "in_list_view": 1, + "label": "Description", + "print_width": "300px", + "reqd": 1, "width": "300px" + }, + { + "default": "0", + "fieldname": "weekly_off", + "fieldtype": "Check", + "label": "Weekly Off" + }, + { + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_4", + "fieldtype": "Section Break" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2016-07-11 03:28:00.660849", - "modified_by": "Administrator", - "module": "HR", - "name": "Holiday", - "owner": "Administrator", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", - "track_seen": 0 + ], + "idx": 1, + "istable": 1, + "links": [], + "modified": "2020-04-18 19:03:23.507845", + "modified_by": "Administrator", + "module": "HR", + "name": "Holiday", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py index 8c7b6f723f..76dc9429f1 100644 --- a/erpnext/hr/doctype/holiday_list/holiday_list.py +++ b/erpnext/hr/doctype/holiday_list/holiday_list.py @@ -23,6 +23,7 @@ class HolidayList(Document): ch = self.append('holidays', {}) ch.description = self.weekly_off ch.holiday_date = d + ch.weekly_off = 1 ch.idx = last_idx + i + 1 def validate_values(self): diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js index 348c5e7cb7..bd4ed3c4ca 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.js @@ -31,6 +31,18 @@ frappe.query_reports["Monthly Attendance Sheet"] = { "options": "Company", "default": frappe.defaults.get_user_default("Company"), "reqd": 1 + }, + { + "fieldname":"group_by", + "label": __("Group By"), + "fieldtype": "Select", + "options": ["","Branch","Grade","Department","Designation"] + }, + { + "fieldname":"summarized_view", + "label": __("Summarized View"), + "fieldtype": "Check", + "Default": 0, } ], diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py index 9a9e42e5e8..d98ed1b414 100644 --- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py +++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py @@ -7,65 +7,127 @@ from frappe.utils import cstr, cint, getdate from frappe import msgprint, _ from calendar import monthrange +status_map = { + "Absent": "A", + "Half Day": "HD", + "Holiday": "H", + "Weekly Off": "WO", + "On Leave": "L", + "Present": "P", + "Work From Home": "WFH" + } + +day_abbr = [ + "Mon", + "Tue", + "Wed", + "Thu", + "Fri", + "Sat", + "Sun" +] + def execute(filters=None): if not filters: filters = {} conditions, filters = get_conditions(filters) columns = get_columns(filters) att_map = get_attendance_list(conditions, filters) - emp_map = get_employee_details(filters) - holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]] + if filters.group_by: + emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company) + holiday_list = [] + for parameter in group_by_parameters: + h_list = [emp_map[parameter][d]["holiday_list"] for d in emp_map[parameter] if emp_map[parameter][d]["holiday_list"]] + holiday_list += h_list + else: + emp_map = get_employee_details(filters.group_by, filters.company) + holiday_list = [emp_map[d]["holiday_list"] for d in emp_map if emp_map[d]["holiday_list"]] + + default_holiday_list = frappe.get_cached_value('Company', filters.get("company"), "default_holiday_list") holiday_list.append(default_holiday_list) holiday_list = list(set(holiday_list)) holiday_map = get_holiday(holiday_list, filters["month"]) data = [] - leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) - leave_list = [d[0] for d in leave_types] - columns.extend(leave_list) - columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"]) - for emp in sorted(att_map): - emp_det = emp_map.get(emp) - if not emp_det: + leave_list = None + if filters.summarized_view: + leave_types = frappe.db.sql("""select name from `tabLeave Type`""", as_list=True) + leave_list = [d[0] + ":Float:120" for d in leave_types] + columns.extend(leave_list) + columns.extend([_("Total Late Entries") + ":Float:120", _("Total Early Exits") + ":Float:120"]) + + if filters.group_by: + for parameter in group_by_parameters: + data.append([ ""+ parameter + ""]) + record = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, leave_list=leave_list) + data += record + else: + record = add_data(emp_map, att_map, filters, holiday_map, conditions, leave_list=leave_list) + data += record + + return columns, data + + +def add_data(employee_map, att_map, filters, holiday_map, conditions, leave_list=None): + + record = [] + for emp in employee_map: + emp_det = employee_map.get(emp) + if not emp_det or emp not in att_map: continue - row = [emp, emp_det.employee_name, emp_det.branch, emp_det.department, emp_det.designation, - emp_det.company] + row = [] + if filters.group_by: + row += [" "] + row += [emp, emp_det.employee_name] - total_p = total_a = total_l = 0.0 + total_p = total_a = total_l = total_h = total_um= 0.0 for day in range(filters["total_days_in_month"]): + status = None status = att_map.get(emp).get(day + 1) - status_map = { - "Absent": "A", - "Half Day": "HD", - "Holiday":"H", - "On Leave": "L", - "Present": "P", - "Work From Home": "WFH" - } if status is None and holiday_map: emp_holiday_list = emp_det.holiday_list if emp_det.holiday_list else default_holiday_list - if emp_holiday_list in holiday_map and (day+1) in holiday_map[emp_holiday_list]: - status = "Holiday" - row.append(status_map.get(status, "")) + if emp_holiday_list in holiday_map: + for idx, ele in enumerate(holiday_map[emp_holiday_list]): + if day+1 == holiday_map[emp_holiday_list][idx][0]: + if holiday_map[emp_holiday_list][idx][1]: + status = "Weekly Off" + else: + status = "Holiday" + total_h += 1 - if status == "Present": - total_p += 1 - elif status == "Absent": - total_a += 1 - elif status == "On Leave": - total_l += 1 - elif status == "Half Day": - total_p += 0.5 - total_a += 0.5 - total_l += 0.5 - row += [total_p, total_l, total_a] + # if emp_holiday_list in holiday_map and (day+1) in holiday_map[emp_holiday_list][0]: + # if holiday_map[emp_holiday_list][1]: + # status= "Weekly Off" + # else: + # status = "Holiday" + + # += 1 + + if not filters.summarized_view: + row.append(status_map.get(status, "")) + else: + if status == "Present": + total_p += 1 + elif status == "Absent": + total_a += 1 + elif status == "On Leave": + total_l += 1 + elif status == "Half Day": + total_p += 0.5 + total_a += 0.5 + total_l += 0.5 + elif not status: + total_um += 1 + + if filters.summarized_view: + row += [total_p, total_l, total_a, total_h, total_um] if not filters.get("employee"): filters.update({"employee": emp}) @@ -73,43 +135,53 @@ def execute(filters=None): elif not filters.get("employee") == emp: filters.update({"employee": emp}) - leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ - where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) + if filters.summarized_view: + leave_details = frappe.db.sql("""select leave_type, status, count(*) as count from `tabAttendance`\ + where leave_type is not NULL %s group by leave_type, status""" % conditions, filters, as_dict=1) - time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \ - late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \ - early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters) + time_default_counts = frappe.db.sql("""select (select count(*) from `tabAttendance` where \ + late_entry = 1 %s) as late_entry_count, (select count(*) from tabAttendance where \ + early_exit = 1 %s) as early_exit_count""" % (conditions, conditions), filters) - leaves = {} - for d in leave_details: - if d.status == "Half Day": - d.count = d.count * 0.5 - if d.leave_type in leaves: - leaves[d.leave_type] += d.count - else: - leaves[d.leave_type] = d.count + leaves = {} + for d in leave_details: + if d.status == "Half Day": + d.count = d.count * 0.5 + if d.leave_type in leaves: + leaves[d.leave_type] += d.count + else: + leaves[d.leave_type] = d.count - for d in leave_list: - if d in leaves: - row.append(leaves[d]) - else: - row.append("0.0") + for d in leave_list: + if d in leaves: + row.append(leaves[d]) + else: + row.append("0.0") - row.extend([time_default_counts[0][0],time_default_counts[0][1]]) - data.append(row) - return columns, data + row.extend([time_default_counts[0][0],time_default_counts[0][1]]) + record.append(row) + + + return record def get_columns(filters): - columns = [ - _("Employee") + ":Link/Employee:120", _("Employee Name") + "::140", _("Branch")+ ":Link/Branch:120", - _("Department") + ":Link/Department:120", _("Designation") + ":Link/Designation:120", - _("Company") + ":Link/Company:120" + + columns = [] + + if filters.group_by: + columns = [_(filters.group_by)+ ":Link/Branch:120"] + + columns += [ + _("Employee") + ":Link/Employee:120", _("Employee Name") + ":Link/Employee:120" ] - for day in range(filters["total_days_in_month"]): - columns.append(cstr(day+1) +"::20") - - columns += [_("Total Present") + ":Float:80", _("Total Leaves") + ":Float:80", _("Total Absent") + ":Float:80"] + if not filters.summarized_view: + for day in range(filters["total_days_in_month"]): + date = str(filters.year) + "-" + str(filters.month)+ "-" + str(day+1) + day_name = day_abbr[getdate(date).weekday()] + columns.append(cstr(day+1)+ " " +day_name +"::65") + else: + columns += [_("Total Present") + ":Float:120", _("Total Leaves") + ":Float:120", _("Total Absent") + ":Float:120", _("Total Holidays") + ":Float:120", _("Unmarked Days")+ ":Float:120"] return columns def get_attendance_list(conditions, filters): @@ -140,19 +212,43 @@ def get_conditions(filters): return conditions, filters -def get_employee_details(filters): - emp_map = frappe._dict() - for d in frappe.db.sql("""select name, employee_name, designation, department, branch, company, - holiday_list from tabEmployee where company = %s""", (filters.get("company")), as_dict=1): - emp_map.setdefault(d.name, d) +def get_employee_details(group_by, company): + emp_map = {} + query = """select name, employee_name, designation, department, branch, company, + holiday_list from `tabEmployee` where company = '%s' """ % frappe.db.escape(company) - return emp_map + if group_by: + group_by = group_by.lower() + query += " order by " + group_by + " ASC" + + employee_details = frappe.db.sql(query , as_dict=1) + + group_by_parameters = [] + if group_by: + + group_by_parameters = list(set(detail.get(group_by, "") for detail in employee_details if detail.get(group_by, ""))) + for parameter in group_by_parameters: + emp_map[parameter] = {} + + + for d in employee_details: + if group_by and len(group_by_parameters): + if d.get(group_by, None): + + emp_map[d.get(group_by)][d.name] = d + else: + emp_map[d.name] = d + + if not group_by: + return emp_map + else: + return emp_map, group_by_parameters def get_holiday(holiday_list, month): holiday_map = frappe._dict() for d in holiday_list: if d: - holiday_map.setdefault(d, frappe.db.sql_list('''select day(holiday_date) from `tabHoliday` + holiday_map.setdefault(d, frappe.db.sql('''select day(holiday_date), weekly_off from `tabHoliday` where parent=%s and month(holiday_date)=%s''', (d, month))) return holiday_map From d46696fa034e41a33008bdb232529d4162f5956a Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 May 2020 16:10:47 +0530 Subject: [PATCH 21/27] fix: cannot make payment entry against shareholder (#21597) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bcb22f0e57..83c670eace 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -228,6 +228,8 @@ class PaymentEntry(AccountsController): valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") elif self.party_type == "Employee": valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance") + elif self.party_type == "Shareholder": + valid_reference_doctypes = ("Journal Entry") for d in self.get("references"): if not d.allocated_amount: From fff00cee3aa03953b447f746421bd687d26801cd Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 May 2020 16:14:17 +0530 Subject: [PATCH 22/27] chore: fix error message (#21593) * chore: fix error message * chore: add row idx --- erpnext/controllers/selling_controller.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 90ba8b3644..1e0a48c134 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -165,9 +165,9 @@ class SellingController(StockController): d.stock_qty = flt(d.qty) * flt(d.conversion_factor) def validate_selling_price(self): - def throw_message(item_name, rate, ref_rate_field): - frappe.throw(_("""Selling rate for item {0} is lower than its {1}. Selling rate should be atleast {2}""") - .format(item_name, ref_rate_field, rate)) + def throw_message(idx, item_name, rate, ref_rate_field): + frappe.throw(_("""Row #{}: Selling rate for item {} is lower than its {}. Selling rate should be atleast {}""") + .format(idx, item_name, ref_rate_field, rate)) if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): return @@ -181,8 +181,8 @@ class SellingController(StockController): last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"]) last_purchase_rate_in_sales_uom = last_purchase_rate / (it.conversion_factor or 1) - if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom) and not self.get('is_internal_customer'): - throw_message(it.item_name, last_purchase_rate_in_sales_uom, "last purchase rate") + if flt(it.base_rate) < flt(last_purchase_rate_in_sales_uom): + throw_message(it.idx, frappe.bold(it.item_name), last_purchase_rate_in_sales_uom, "last purchase rate") last_valuation_rate = frappe.db.sql(""" SELECT valuation_rate FROM `tabStock Ledger Entry` WHERE item_code = %s @@ -193,7 +193,7 @@ class SellingController(StockController): last_valuation_rate_in_sales_uom = last_valuation_rate[0][0] / (it.conversion_factor or 1) if is_stock_item and flt(it.base_rate) < flt(last_valuation_rate_in_sales_uom) \ and not self.get('is_internal_customer'): - throw_message(it.name, last_valuation_rate_in_sales_uom, "valuation rate") + throw_message(it.idx, frappe.bold(it.item_name), last_valuation_rate_in_sales_uom, "valuation rate") def get_item_list(self): From d76f167e96071212d0680125044c9d1a5f8637a4 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 5 May 2020 16:14:45 +0530 Subject: [PATCH 23/27] fix: handle make_gl_entry in case of cwip enable after puchasing (#21529) * fix: handle make_gl_entry in case of cwip enable after puchasing * fix: invalid variable assignment * fix: make gl entries if cwip has been booked even if cwip is disabled * add tests * fix: conditions Co-authored-by: Nabin Hait --- erpnext/assets/doctype/asset/asset.py | 53 +++++++++++-- erpnext/assets/doctype/asset/test_asset.py | 75 +++++++++++++++++++ .../doctype/asset_movement/asset_movement.py | 1 + 3 files changed, 121 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 06dfa19bf2..a3200d5644 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -32,7 +32,7 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.make_asset_movement() - if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category): + if not self.booked_fixed_asset and self.validate_make_gl_entry(): self.make_gl_entries() def before_cancel(self): @@ -455,18 +455,55 @@ class Asset(AccountsController): for d in self.get('finance_books'): if d.finance_book == self.default_finance_book: return cint(d.idx) - 1 + + def validate_make_gl_entry(self): + purchase_document = self.get_purchase_document() + asset_bought_with_invoice = purchase_document == self.purchase_invoice + fixed_asset_account, cwip_account = self.get_asset_accounts() + cwip_enabled = is_cwip_accounting_enabled(self.asset_category) + # check if expense already has been booked in case of cwip was enabled after purchasing asset + expense_booked = False + cwip_booked = False + + if asset_bought_with_invoice: + expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", + (purchase_document, fixed_asset_account), as_dict=1) + else: + cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", + (purchase_document, cwip_account), as_dict=1) + + if cwip_enabled and (expense_booked or not cwip_booked): + # if expense has already booked from invoice or cwip is booked from receipt + return False + elif not cwip_enabled and (not expense_booked or cwip_booked): + # if cwip is disabled but expense hasn't been booked yet + return True + elif cwip_enabled: + # default condition + return True + + def get_purchase_document(self): + asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') + purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt + + return purchase_document + + def get_asset_accounts(self): + fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, + asset_category = self.asset_category, company = self.company) + + cwip_account = get_asset_account("capital_work_in_progress_account", + self.name, self.asset_category, self.company) + + return fixed_asset_account, cwip_account def make_gl_entries(self): gl_entries = [] - if ((self.purchase_receipt \ - or (self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock'))) - and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): - fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, - asset_category = self.asset_category, company = self.company) + purchase_document = self.get_purchase_document() + fixed_asset_account, cwip_account = self.get_asset_accounts() - cwip_account = get_asset_account("capital_work_in_progress_account", - self.name, self.asset_category, self.company) + if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): gl_entries.append(self.get_gl_dict({ "account": cwip_account, diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index a0f8d156d7..aed78e7746 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -560,6 +560,81 @@ class TestAsset(unittest.TestCase): self.assertEqual(gle, expected_gle) + def test_gle_with_cwip_toggling(self): + # TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=5000, do_not_submit=True, location="Test Location") + pr.set('taxes', [{ + 'category': 'Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Service Tax - _TC', + 'description': '_Test Account Service Tax', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }, { + 'category': 'Valuation and Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Shipping Charges - _TC', + 'description': '_Test Account Shipping Charges', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }]) + pr.submit() + expected_gle = ( + ("Asset Received But Not Billed - _TC", 0.0, 5250.0), + ("CWIP Account - _TC", 5250.0, 0.0) + ) + pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Receipt' and voucher_no = %s + order by account""", pr.name) + self.assertEqual(pr_gle, expected_gle) + + pi = make_invoice(pr.name) + pi.submit() + expected_gle = ( + ("_Test Account Service Tax - _TC", 250.0, 0.0), + ("_Test Account Shipping Charges - _TC", 250.0, 0.0), + ("Asset Received But Not Billed - _TC", 5250.0, 0.0), + ("Creditors - _TC", 0.0, 5500.0), + ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), + ) + pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Invoice' and voucher_no = %s + order by account""", pi.name) + self.assertEqual(pi_gle, expected_gle) + + asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + asset_doc = frappe.get_doc('Asset', asset) + month_end_date = get_last_day(nowdate()) + asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) + asset_doc.append("finance_books", { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date + }) + + # disable cwip and try submitting + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) + asset_doc.submit() + # asset should have gl entries even if cwip is disabled + expected_gle = ( + ("_Test Fixed Asset - _TC", 5250.0, 0.0), + ("CWIP Account - _TC", 0.0, 5250.0) + ) + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Asset' and voucher_no = %s + order by account""", asset_doc.name) + self.assertEqual(gle, expected_gle) + + frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) + def test_expense_head(self): pr = make_purchase_receipt(item_code="Macbook Pro", qty=2, rate=200000.0, location="Test Location") diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 3a08baa714..3da355e2b9 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -110,6 +110,7 @@ class AssetMovement(Document): ORDER BY asm.transaction_date asc """, (d.asset, self.company, 'Receipt'), as_dict=1) + if auto_gen_movement_entry and auto_gen_movement_entry[0].get('name') == self.name: frappe.throw(_('{0} will be cancelled automatically on asset cancellation as it was \ auto generated for Asset {1}').format(self.name, d.asset)) From 54a6c64b0fd6eeedb1a44bfd27caff430dbb7c6c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 5 May 2020 16:17:15 +0530 Subject: [PATCH 24/27] fix: against voucher no not all records showing in case of Group By Voucher (consolidated) (#21592) --- erpnext/accounts/report/general_ledger/general_ledger.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 7af5fa8eaa..6afe208b5b 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -296,6 +296,9 @@ def get_accountwise_gle(filters, gl_entries, gle_map): data[key].debit_in_account_currency += flt(gle.debit_in_account_currency) data[key].credit_in_account_currency += flt(gle.credit_in_account_currency) + if data[key].against_voucher: + data[key].against_voucher += ', ' + gle.against_voucher + from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) for gle in gl_entries: if (gle.posting_date < from_date or From af27c5142ff33679642218eb3312f2be317bd6ea Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Tue, 5 May 2020 16:54:48 +0530 Subject: [PATCH 25/27] fix: Loan securiyt unpledge fixes --- erpnext/loan_management/doctype/loan/loan.py | 7 +- .../loan_management/doctype/loan/test_loan.py | 51 ++++++++++ .../doctype/loan_repayment/loan_repayment.py | 5 +- .../loan_security_pledge.py | 10 ++ .../loan_security_shortfall.py | 2 +- .../loan_security_unpledge.json | 13 +-- .../loan_security_unpledge.py | 94 ++++++++++++------- 7 files changed, 136 insertions(+), 46 deletions(-) diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py index c7a2fba878..c550d4952d 100644 --- a/erpnext/loan_management/doctype/loan/loan.py +++ b/erpnext/loan_management/doctype/loan/loan.py @@ -233,7 +233,7 @@ def make_repayment_entry(loan, applicant_type, applicant, loan_type, company, as return repayment_entry @frappe.whitelist() -def create_loan_security_unpledge(loan, applicant_type, applicant, company): +def create_loan_security_unpledge(loan, applicant_type, applicant, company, as_dict=1): loan_security_pledge_details = frappe.db.sql(""" SELECT p.parent, p.loan_security, p.qty as qty FROM `tabLoan Security Pledge` lsp , `tabPledge` p WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 @@ -252,7 +252,10 @@ def create_loan_security_unpledge(loan, applicant_type, applicant, company): "against_pledge": loan_security.parent }) - return unpledge_request.as_dict() + if as_dict: + return unpledge_request.as_dict() + else: + return unpledge_request diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py index 90b8534bc8..77a1fcc574 100644 --- a/erpnext/loan_management/doctype/loan/test_loan.py +++ b/erpnext/loan_management/doctype/loan/test_loan.py @@ -14,6 +14,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ process_loan_interest_accrual_for_term_loans) from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year from erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall import create_process_loan_security_shortfall +from erpnext.loan_management.doctype.loan.loan import create_loan_security_unpledge class TestLoan(unittest.TestCase): def setUp(self): @@ -276,6 +277,56 @@ class TestLoan(unittest.TestCase): frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250 where loan_security='Test Security 2'""") + def test_loan_security_unpledge(self): + pledges = [] + pledges.append({ + "loan_security": "Test Security 1", + "qty": 4000.00, + "haircut": 50 + }) + + loan_security_pledge = create_loan_security_pledge(self.applicant2, pledges) + loan = create_demand_loan(self.applicant2, "Demand Loan", loan_security_pledge.name, + posting_date=get_first_day(nowdate())) + loan.submit() + + self.assertEquals(loan.loan_amount, 1000000) + + first_date = '2019-10-01' + last_date = '2019-10-30' + + no_of_days = date_diff(last_date, first_date) + 1 + + no_of_days += 6 + + accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \ + / (days_in_year(get_datetime(first_date).year) * 100) + + make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) + process_loan_interest_accrual_for_demand_loans(posting_date = last_date) + + repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), + "Loan Closure", flt(loan.loan_amount + accrued_interest_amount)) + repayment_entry.submit() + + amounts = frappe.db.get_value('Loan Interest Accrual', {'loan': loan.name}, ['paid_interest_amount', + 'paid_principal_amount']) + + loan.load_from_db() + self.assertEquals(loan.status, "Loan Closure Requested") + + unpledge_request = create_loan_security_unpledge(loan.name, loan.applicant_type, loan.applicant, loan.company, as_dict=0) + unpledge_request.submit() + unpledge_request.status = 'Approved' + unpledge_request.save() + + loan_security_pledge.load_from_db() + loan.load_from_db() + + self.assertEqual(loan.status, 'Closed') + for security in loan_security_pledge.securities: + self.assertEquals(security.qty, 0) + def create_loan_accounts(): if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index 5979ee3102..452c836819 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -78,7 +78,10 @@ class LoanRepayment(AccountsController): (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual)) if flt(loan.total_principal_paid + self.principal_amount_paid, 2) >= flt(loan.total_payment, 2): - frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") + if loan.is_secured_loan: + frappe.db.set_value("Loan", self.against_loan, "status", "Loan Closure Requested") + else: + frappe.db.set_value("Loan", self.against_loan, "status", "Closed") frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s WHERE name = %s """, (loan.total_amount_paid + self.amount_paid, diff --git a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py index eb6135868d..f97e5965a5 100644 --- a/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py +++ b/erpnext/loan_management/doctype/loan_security_pledge/loan_security_pledge.py @@ -13,6 +13,7 @@ from erpnext.loan_management.doctype.loan_security_price.loan_security_price imp class LoanSecurityPledge(Document): def validate(self): self.set_pledge_amount() + self.validate_duplicate_securities() def on_submit(self): if self.loan: @@ -21,6 +22,15 @@ class LoanSecurityPledge(Document): update_shortfall_status(self.loan, self.total_security_value) update_loan(self.loan, self.maximum_loan_value) + def validate_duplicate_securities(self): + security_list = [] + for security in self.securities: + if security.loan_security not in security_list: + security_list.append(security.loan_security) + else: + frappe.throw(_('Loan Security {0} added multiple times').format(frappe.bold( + security.loan_security))) + def set_pledge_amount(self): total_security_value = 0 maximum_loan_value = 0 diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py index ab040f1d33..8ca6e3e908 100644 --- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py +++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py @@ -53,7 +53,7 @@ def check_for_ltv_shortfall(process_loan_security_shortfall): loans = frappe.db.sql(""" SELECT l.name, l.loan_amount, l.total_principal_paid, lp.loan_security, lp.haircut, lp.qty, lp.loan_security_type FROM `tabLoan` l, `tabPledge` lp , `tabLoan Security Pledge`p WHERE lp.parent = p.name and p.loan = l.name and l.docstatus = 1 - and l.is_secured_loan and l.status = 'Disbursed' and p.status in ('Pledged', 'Partially Unpledged')""", as_dict=1) + and l.is_secured_loan and l.status = 'Disbursed' and p.status = 'Pledged'""", as_dict=1) loan_security_map = {} diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json index ba94855031..aece46ffda 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "LSU-.{applicant}.-.#####", "creation": "2019-09-21 13:23:16.117028", "doctype": "DocType", @@ -15,7 +16,6 @@ "status", "loan_security_details_section", "securities", - "unpledge_type", "amended_from" ], "fields": [ @@ -47,6 +47,7 @@ { "allow_on_submit": 1, "default": "Requested", + "depends_on": "eval:doc.docstatus == 1", "fieldname": "status", "fieldtype": "Select", "label": "Status", @@ -80,13 +81,6 @@ "options": "Unpledge", "reqd": 1 }, - { - "fieldname": "unpledge_type", - "fieldtype": "Data", - "hidden": 1, - "label": "Unpledge Type", - "read_only": 1 - }, { "fieldname": "company", "fieldtype": "Link", @@ -104,7 +98,8 @@ } ], "is_submittable": 1, - "modified": "2019-10-28 07:41:47.084882", + "links": [], + "modified": "2020-05-05 07:23:18.440058", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Security Unpledge", diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py index 02b1ecb4ca..b2bb22a3ce 100644 --- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py +++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py @@ -13,31 +13,43 @@ from erpnext.loan_management.doctype.loan_security_price.loan_security_price imp class LoanSecurityUnpledge(Document): def validate(self): self.validate_pledges() + self.validate_duplicate_securities() + + def on_cancel(self): + self.update_loan_security_pledge(cancel=1) + self.update_loan_status(cancel=1) + self.db_set('status', 'Requested') + + def validate_duplicate_securities(self): + security_list = [] + for d in self.securities: + security = [d.loan_security, d.against_pledge] + if security not in security_list: + security_list.append(security) + else: + frappe.throw(_("Row {0}: Loan Security {1} against Loan Security Pledge {2} added multiple times").format( + d.idx, frappe.bold(d.loan_security), frappe.bold(d.against_pledge))) def validate_pledges(self): - pledge_details = self.get_pledge_details() - + pledge_qty_map = self.get_pledge_details() loan = frappe.get_doc("Loan", self.loan) - pledge_qty_map = {} remaining_qty = 0 unpledge_value = 0 - for pledge in pledge_details: - pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty) - for security in self.securities: pledged_qty = pledge_qty_map.get((security.against_pledge, security.loan_security), 0) if not pledged_qty: - frappe.throw(_("Zero qty of {0} pledged against loan {0}").format(frappe.bold(security.loan_security), + frappe.throw(_("Zero qty of {0} pledged against loan {1}").format(frappe.bold(security.loan_security), frappe.bold(self.loan))) unpledge_qty = pledged_qty - security.qty security_price = security.qty * get_loan_security_price(security.loan_security) if unpledge_qty < 0: - frappe.throw(_("Cannot unpledge more than {0} qty of {0}").format(frappe.bold(pledged_qty), - frappe.bold(security.loan_security))) + frappe.throw(_("""Row {0}: Cannot unpledge more than {1} qty of {2} against + Loan Security Pledge {3}""").format(security.idx, frappe.bold(pledged_qty), + frappe.bold(security.loan_security), frappe.bold(security.against_pledge))) remaining_qty += unpledge_qty unpledge_value += security_price - flt(security_price * security.haircut/100) @@ -45,41 +57,57 @@ class LoanSecurityUnpledge(Document): if unpledge_value > loan.total_principal_paid: frappe.throw(_("Cannot Unpledge, loan security value is greater than the repaid amount")) - if not remaining_qty: - self.db_set('unpledge_type', 'Unpledged') - else: - self.db_set('unpledge_type', 'Partially Pledged') - - def get_pledge_details(self): + pledge_qty_map = {} + pledge_details = frappe.db.sql(""" - SELECT p.parent, p.loan_security, p.qty as qty FROM + SELECT p.parent, p.loan_security, p.qty FROM `tabLoan Security Pledge` lsp, `tabPledge` p WHERE p.parent = lsp.name AND lsp.loan = %s AND lsp.docstatus = 1 - AND lsp.status = "Pledged" - """,(self.loan), as_dict=1) + AND lsp.status in ('Pledged', 'Partially Pledged') + """, (self.loan), as_dict=1) - return pledge_details + for pledge in pledge_details: + pledge_qty_map.setdefault((pledge.parent, pledge.loan_security), pledge.qty) + + return pledge_qty_map def on_update_after_submit(self): if self.status == "Approved": - frappe.db.sql(""" - UPDATE - `tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, - `tabLoan Security Unpledge` lsu SET p.qty = (p.qty - u.qty) - WHERE - lsp.loan = %s - AND lsu.status = 'Requested' - AND u.parent = %s - AND p.parent = u.against_pledge - AND p.loan_security = u.loan_security""",(self.loan, self.name)) + self.update_loan_security_pledge() + self.update_loan_status() - frappe.db.sql("""UPDATE `tabLoan Security Pledge` - SET status = %s WHERE loan = %s""", (self.unpledge_type, self.loan)) + def update_loan_security_pledge(self, cancel=0): + if cancel: + new_qty = 'p.qty + u.qty' + else: + new_qty = 'p.qty - u.qty' + + frappe.db.sql(""" + UPDATE + `tabPledge` p, `tabUnpledge` u, `tabLoan Security Pledge` lsp, `tabLoan Security Unpledge` lsu + SET p.qty = {new_qty} + WHERE + lsp.loan = %s + AND p.parent = u.against_pledge + AND p.parent = lsp.name + AND lsp.docstatus = 1 + AND p.loan_security = u.loan_security""".format(new_qty=new_qty),(self.loan)) + + def update_loan_status(self, cancel=0): + if cancel: + loan_status = frappe.get_value('Loan', self.loan, 'status') + if loan_status == 'Closed': + frappe.db.set_value('Loan', self.loan, 'status', 'Loan Closure Requested') + else: + pledge_qty = frappe.db.sql("""SELECT SUM(c.qty) + FROM `tabLoan Security Pledge` p, `tabPledge` c + WHERE p.loan = %s AND c.parent = p.name""", (self.loan))[0][0] + + if not pledge_qty: + frappe.db.set_value('Loan', self.loan, 'status', 'Closed') - if self.unpledge_type == 'Unpledged': - frappe.db.set_value("Loan", self.loan, 'status', 'Closed') From 07fe4e1933f7c94f4ef1de358967e6379465c16f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 May 2020 18:27:13 +0530 Subject: [PATCH 26/27] Update add_default_dashboards.py --- erpnext/patches/v12_0/add_default_dashboards.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/patches/v12_0/add_default_dashboards.py b/erpnext/patches/v12_0/add_default_dashboards.py index 24661ccfea..2a91e1b932 100644 --- a/erpnext/patches/v12_0/add_default_dashboards.py +++ b/erpnext/patches/v12_0/add_default_dashboards.py @@ -6,5 +6,5 @@ from erpnext.setup.setup_wizard.operations.install_fixtures import add_dashboard def execute(): frappe.reload_doc("desk", "doctype", "number_card_link") - frappe.reload_doc("desk", "doctype", "patient_appointment") + frappe.reload_doc("healthcare", "doctype", "patient_appointment") add_dashboards() From a4567a446f30138bb4c9bea12176e0fe2cd30b73 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 5 May 2020 18:36:12 +0530 Subject: [PATCH 27/27] fix: Test for payroll entry --- erpnext/hr/doctype/payroll_entry/test_payroll_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py index 49671d5e22..e43f744bd4 100644 --- a/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/hr/doctype/payroll_entry/test_payroll_entry.py @@ -17,7 +17,7 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_ class TestPayrollEntry(unittest.TestCase): def setUp(self): - for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry"]: + for dt in ["Salary Slip", "Salary Component", "Salary Component Account", "Payroll Entry", "Salary Structure"]: frappe.db.sql("delete from `tab%s`" % dt) make_earning_salary_component(setup=True, company_list=["_Test Company"])