From e04b3aaf7a2cb0792436a7e5f868572cb35c39f4 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:22 +0530 Subject: [PATCH 1/7] feat(Employee Advance): add 'Returned' and 'Partly Claimed and Returned' status --- .../employee_advance/employee_advance.json | 42 +++++++++++++++++-- .../employee_advance/employee_advance.py | 39 ++++++++++------- .../hr/doctype/expense_claim/expense_claim.js | 2 +- .../hr/doctype/expense_claim/expense_claim.py | 31 +++++++++----- 4 files changed, 85 insertions(+), 29 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 04754530c3..b0501830cc 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2017-10-09 14:26:29.612365", + "creation": "2022-01-17 18:36:51.450395", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -121,7 +121,7 @@ "fieldtype": "Select", "label": "Status", "no_copy": 1, - "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled", + "options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled", "read_only": 1 }, { @@ -200,7 +200,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-09-11 18:38:38.617478", + "modified": "2022-01-17 19:33:52.345823", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -237,5 +237,41 @@ "search_fields": "employee,employee_name", "sort_field": "modified", "sort_order": "DESC", + "states": [ + { + "color": "Red", + "custom": 1, + "title": "Draft" + }, + { + "color": "Green", + "custom": 1, + "title": "Paid" + }, + { + "color": "Orange", + "custom": 1, + "title": "Unpaid" + }, + { + "color": "Blue", + "custom": 1, + "title": "Claimed" + }, + { + "color": "Gray", + "title": "Returned" + }, + { + "color": "Yellow", + "title": "Partly Claimed and Returned" + }, + { + "color": "Red", + "custom": 1, + "title": "Cancelled" + } + ], + "title_field": "employee_name", "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 7aac2b63ed..e17eb214a1 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -28,18 +28,31 @@ class EmployeeAdvance(Document): def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') - def set_status(self): + def set_status(self, update=False): + precision = self.precision("paid_amount") + total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision) + status = None + if self.docstatus == 0: - self.status = "Draft" - if self.docstatus == 1: - if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount): - self.status = "Claimed" - elif self.paid_amount and self.advance_amount == flt(self.paid_amount): - self.status = "Paid" + status = "Draft" + elif self.docstatus == 1: + if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt(self.paid_amount, precision): + status = "Claimed" + elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt(self.paid_amount, precision): + status = "Returned" + elif flt(self.claimed_amount) > 0 and (flt(self.return_amount) > 0) and total_amount == flt(self.paid_amount, precision): + status = "Partly Claimed and Returned" + elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt(self.paid_amount, precision): + status = "Paid" else: - self.status = "Unpaid" + status = "Unpaid" elif self.docstatus == 2: - self.status = "Cancelled" + status = "Cancelled" + + if update: + self.db_set("status", status) + else: + self.status = status def set_total_advance_paid(self): gle = frappe.qb.DocType("GL Entry") @@ -85,9 +98,7 @@ class EmployeeAdvance(Document): self.db_set("paid_amount", paid_amount) self.db_set("return_amount", return_amount) - self.set_status() - frappe.db.set_value("Employee Advance", self.name , "status", self.status) - + self.set_status(update=True) def update_claimed_amount(self): claimed_amount = frappe.db.sql(""" @@ -103,8 +114,8 @@ class EmployeeAdvance(Document): frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount)) self.reload() - self.set_status() - frappe.db.set_value("Employee Advance", self.name, "status", self.status) + self.set_status(update=True) + @frappe.whitelist() def get_pending_amount(employee, posting_date): diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 047945787d..af80b63845 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", { ['docstatus', '=', 1], ['employee', '=', frm.doc.employee], ['paid_amount', '>', 0], - ['status', '!=', 'Claimed'] + ['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']] ] }; }); diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 7e3898b7d5..2d2bb093ce 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -341,18 +341,27 @@ def get_expense_claim_account(expense_claim_type, company): @frappe.whitelist() def get_advances(employee, advance_id=None): - if not advance_id: - condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee)) - else: - condition = 'name={0}'.format(frappe.db.escape(advance_id)) + advance = frappe.qb.DocType("Employee Advance") - return frappe.db.sql(""" - select - name, posting_date, paid_amount, claimed_amount, advance_account - from - `tabEmployee Advance` - where {0} - """.format(condition), as_dict=1) + query = ( + frappe.qb.from_(advance) + .select( + advance.name, advance.posting_date, advance.paid_amount, + advance.claimed_amount, advance.advance_account + ) + ) + + if not advance_id: + query = query.where( + (advance.docstatus == 1) + & (advance.employee == employee) + & (advance.paid_amount > 0) + & (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"])) + ) + else: + query = query.where(advance.name == advance_id) + + return query.run(as_dict=True) @frappe.whitelist() From bf30932de0524c42ab3d9718e3d19a73e1cb2d39 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:38 +0530 Subject: [PATCH 2/7] patch: Employee Advance return statuses --- erpnext/patches.txt | 3 ++- .../v14_0/update_employee_advance_status.py | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v14_0/update_employee_advance_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index fa62b7fc27..ed39c204f6 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -326,4 +326,5 @@ erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v14_0.delete_agriculture_doctypes erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v14_0.rearrange_company_fields -erpnext.patches.v14_0.update_leave_notification_template \ No newline at end of file +erpnext.patches.v14_0.update_leave_notification_template +erpnext.patches.v14_0.update_employee_advance_status \ No newline at end of file diff --git a/erpnext/patches/v14_0/update_employee_advance_status.py b/erpnext/patches/v14_0/update_employee_advance_status.py new file mode 100644 index 0000000000..a20e35a9f6 --- /dev/null +++ b/erpnext/patches/v14_0/update_employee_advance_status.py @@ -0,0 +1,26 @@ +import frappe + + +def execute(): + frappe.reload_doc('hr', 'doctype', 'employee_advance') + + advance = frappe.qb.DocType('Employee Advance') + (frappe.qb + .update(advance) + .set(advance.status, 'Returned') + .where( + (advance.docstatus == 1) + & ((advance.return_amount) & (advance.paid_amount == advance.return_amount)) + & (advance.status == 'Paid') + ) + ).run() + + (frappe.qb + .update(advance) + .set(advance.status, 'Partly Claimed and Returned') + .where( + (advance.docstatus == 1) + & ((advance.claimed_amount & advance.return_amount) & (advance.paid_amount == (advance.return_amount + advance.claimed_amount))) + & (advance.status == 'Paid') + ) + ).run() \ No newline at end of file From 0843d4388569616803a562a6928d6f1204f0e733 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:37:45 +0530 Subject: [PATCH 3/7] fix(Expense Claim): validate advances after setting totals --- erpnext/hr/doctype/expense_claim/expense_claim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 2d2bb093ce..5146a5be90 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -23,10 +23,10 @@ class ExpenseClaim(AccountsController): def validate(self): validate_active_employee(self.employee) - self.validate_advances() + set_employee_name(self) self.validate_sanctioned_amount() self.calculate_total_amount() - set_employee_name(self) + self.validate_advances() self.set_expense_account(validate=True) self.set_payable_account() self.set_cost_center() From 85be0d22d4300a40f46b7268f7c56d8e7facbd40 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:38:16 +0530 Subject: [PATCH 4/7] fix: employee advance status update on return via additional salary --- erpnext/payroll/doctype/additional_salary/additional_salary.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index bf8bd05fcc..d618568416 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -105,6 +105,8 @@ class AdditionalSalary(Document): return_amount += self.amount frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount) + advance = frappe.get_doc("Employee Advance", self.ref_docname) + advance.set_status(update=True) def update_employee_referral(self, cancel=False): if self.ref_doctype == "Employee Referral": From 17b1f5f256ff63d34b3c20b4792cd77ae75402e0 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 18:35:25 +0530 Subject: [PATCH 5/7] test: employee advance status --- .../employee_advance/employee_advance.py | 6 +- .../employee_advance/test_employee_advance.py | 97 ++++++++++++++++++- 2 files changed, 100 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index e17eb214a1..f63bb86129 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -233,7 +233,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, 'reference_name': employee_advance_name, 'party_type': 'Employee', 'party': employee, - 'is_advance': 'Yes' + 'is_advance': 'Yes', + 'cost_center': erpnext.get_default_cost_center(company) }) bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \ @@ -244,7 +245,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, "debit_in_account_currency": bank_amount, "account_currency": bank_cash_account.account_currency, "account_type": bank_cash_account.account_type, - "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1 + "exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1, + "cost_center": erpnext.get_default_cost_center(company) }) return je.as_dict() diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 5f2e720eb4..5f3a66a04f 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -4,7 +4,7 @@ import unittest import frappe -from frappe.utils import nowdate +from frappe.utils import flt, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee @@ -12,6 +12,12 @@ from erpnext.hr.doctype.employee_advance.employee_advance import ( EmployeeAdvanceOverPayment, create_return_through_additional_salary, make_bank_entry, + make_return_entry, +) +from erpnext.hr.doctype.expense_claim.expense_claim import get_advances +from erpnext.hr.doctype.expense_claim.test_expense_claim import ( + get_payable_account, + make_expense_claim, ) from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure @@ -52,9 +58,75 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.paid_amount, 0) self.assertEqual(advance.status, "Unpaid") + def test_claimed_and_returned_status(self): + # Claimed Status check, full amount claimed + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 1000) + self.assertEqual(advance.status, "Claimed") + + # cancel claim; status should be Paid + claim.cancel() + advance.reload() + self.assertEqual(advance.claimed_amount, 0) + self.assertEqual(advance.status, "Paid") + + # Partly Claimed and Returned status check + # 500 Claimed, 500 Returned + claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + claim = get_advances_for_claim(claim, advance.name, amount=500) + claim.save() + claim.submit() + + advance.reload() + self.assertEqual(advance.claimed_amount, 500) + self.assertEqual(advance.status, "Paid") + + entry = make_return_entry( + employee=advance.employee, + company=advance.company, + employee_advance_name=advance.name, + return_amount=flt(advance.paid_amount - advance.claimed_amount), + advance_account=advance.advance_account, + mode_of_payment=advance.mode_of_payment, + currency=advance.currency, + exchange_rate=advance.exchange_rate + ) + + entry = frappe.get_doc(entry) + entry.insert() + entry.submit() + + advance.reload() + self.assertEqual(advance.return_amount, 500) + self.assertEqual(advance.status, "Partly Claimed and Returned") + + # Cancel return entry; status should change to Paid + entry.cancel() + advance.reload() + self.assertEqual(advance.return_amount, 0) + self.assertEqual(advance.status, "Paid") + def test_repay_unclaimed_amount_from_salary(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1}) + pe = make_payment_entry(advance) + pe.submit() args = {"type": "Deduction"} create_salary_component("Advance Salary - Deduction", **args) @@ -82,11 +154,13 @@ class TestEmployeeAdvance(unittest.TestCase): advance.reload() self.assertEqual(advance.return_amount, 1000) + self.assertEqual(advance.status, "Returned") # update advance return amount on additional salary cancellation additional_salary.cancel() advance.reload() self.assertEqual(advance.return_amount, 700) + self.assertEqual(advance.status, "Paid") def tearDown(self): frappe.db.rollback() @@ -118,3 +192,24 @@ def make_employee_advance(employee_name, args=None): doc.submit() return doc + + +def get_advances_for_claim(claim, advance_name, amount=None): + advances = get_advances(claim.employee, advance_name) + + for entry in advances: + if amount: + allocated_amount = amount + else: + allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount) + + claim.append("advances", { + "employee_advance": entry.name, + "posting_date": entry.posting_date, + "advance_account": entry.advance_account, + "advance_paid": entry.paid_amount, + "unclaimed_amount": allocated_amount, + "allocated_amount": allocated_amount + }) + + return claim \ No newline at end of file From 57f92df108ae6c33567a95be22a43015fe3178e5 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 1 Mar 2022 20:50:36 +0530 Subject: [PATCH 6/7] fix: flake8 issues --- erpnext/hr/doctype/expense_claim/expense_claim.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 5146a5be90..fe04efbbab 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -345,10 +345,10 @@ def get_advances(employee, advance_id=None): query = ( frappe.qb.from_(advance) - .select( - advance.name, advance.posting_date, advance.paid_amount, - advance.claimed_amount, advance.advance_account - ) + .select( + advance.name, advance.posting_date, advance.paid_amount, + advance.claimed_amount, advance.advance_account + ) ) if not advance_id: From 9988ec697f056b476ead3be1a371a82c6337be8e Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 1 Mar 2022 21:40:39 +0530 Subject: [PATCH 7/7] test: test advance filters in expense claim and cancelled status --- .../employee_advance/employee_advance.py | 1 + .../employee_advance/test_employee_advance.py | 38 +++++++++++++++++-- 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index f63bb86129..79d389d440 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -27,6 +27,7 @@ class EmployeeAdvance(Document): def on_cancel(self): self.ignore_linked_doctypes = ('GL Entry') + self.set_status(update=True) def set_status(self, update=False): precision = self.precision("paid_amount") diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 5f3a66a04f..e3c1487ca2 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -24,6 +24,9 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_ class TestEmployeeAdvance(unittest.TestCase): + def setUp(self): + frappe.db.delete("Employee Advance") + def test_paid_amount_and_status(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name) @@ -58,8 +61,12 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.paid_amount, 0) self.assertEqual(advance.status, "Unpaid") - def test_claimed_and_returned_status(self): - # Claimed Status check, full amount claimed + advance.cancel() + advance.reload() + self.assertEqual(advance.status, "Cancelled") + + def test_claimed_status(self): + # CLAIMED Status check, full amount claimed payable_account = get_payable_account("_Test Company") claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) @@ -75,13 +82,26 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.claimed_amount, 1000) self.assertEqual(advance.status, "Claimed") + # advance should not be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name not in advances) + # cancel claim; status should be Paid claim.cancel() advance.reload() self.assertEqual(advance.claimed_amount, 0) self.assertEqual(advance.status, "Paid") - # Partly Claimed and Returned status check + def test_partly_claimed_and_returned_status(self): + payable_account = get_payable_account("_Test Company") + claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) + + advance = make_employee_advance(claim.employee) + pe = make_payment_entry(advance) + pe.submit() + + # PARTLY CLAIMED AND RETURNED status check # 500 Claimed, 500 Returned claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) @@ -116,12 +136,22 @@ class TestEmployeeAdvance(unittest.TestCase): self.assertEqual(advance.return_amount, 500) self.assertEqual(advance.status, "Partly Claimed and Returned") - # Cancel return entry; status should change to Paid + # advance should not be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name not in advances) + + # Cancel return entry; status should change to PAID entry.cancel() advance.reload() self.assertEqual(advance.return_amount, 0) self.assertEqual(advance.status, "Paid") + # advance should be shown in claims + advances = get_advances(claim.employee) + advances = [entry.name for entry in advances] + self.assertTrue(advance.name in advances) + def test_repay_unclaimed_amount_from_salary(self): employee_name = make_employee("_T@employe.advance") advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})