From 42e7a86a3b86672dc1cd102b7452ce1b85ce2957 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:22 +0530 Subject: [PATCH 1/6] feat(Employee Advance): add 'Returned' and 'Partly Claimed and Returned' status --- .../employee_advance/employee_advance.json | 42 +++++++++++++++-- .../employee_advance/employee_advance.py | 47 ++++++++++++++----- .../hr/doctype/expense_claim/expense_claim.js | 2 +- .../hr/doctype/expense_claim/expense_claim.py | 32 +++++++------ 4 files changed, 91 insertions(+), 32 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 3a561216cca..1b838ad9863 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", @@ -236,5 +236,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 3d4023d3195..7605d209ad1 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -30,18 +30,41 @@ 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") @@ -89,8 +112,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 = ( @@ -112,8 +134,7 @@ 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() diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 047945787d7..af80b63845e 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 311a1eb81c8..89d86c1bc7c 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -414,25 +414,27 @@ def get_expense_claim_account(expense_claim_type, company): @frappe.whitelist() def get_advances(employee, advance_id=None): + advance = frappe.qb.DocType("Employee Advance") + + query = frappe.qb.from_(advance).select( + advance.name, + advance.posting_date, + advance.paid_amount, + advance.claimed_amount, + advance.advance_account, + ) + 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) + query = query.where( + (advance.docstatus == 1) + & (advance.employee == employee) + & (advance.paid_amount > 0) + & (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"])) ) else: - condition = "name={0}".format(frappe.db.escape(advance_id)) + query = query.where(advance.name == advance_id) - return frappe.db.sql( - """ - select - name, posting_date, paid_amount, claimed_amount, advance_account - from - `tabEmployee Advance` - where {0} - """.format( - condition - ), - as_dict=1, - ) + return query.run(as_dict=True) @frappe.whitelist() From cac9e245b62a4f35c78720c2a250ce5c687be35d Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:36:38 +0530 Subject: [PATCH 2/6] patch: Employee Advance return statuses --- erpnext/patches.txt | 2 ++ .../v13_0/update_employee_advance_status.py | 29 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 erpnext/patches/v13_0/update_employee_advance_status.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 1ff33c7f7bf..85780501def 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -366,3 +366,5 @@ erpnext.patches.v13_0.education_deprecation_warning erpnext.patches.v13_0.requeue_recoverable_reposts erpnext.patches.v13_0.create_accounting_dimensions_in_orders erpnext.patches.v13_0.set_per_billed_in_return_delivery_note +erpnext.patches.v13_0.update_employee_advance_status + diff --git a/erpnext/patches/v13_0/update_employee_advance_status.py b/erpnext/patches/v13_0/update_employee_advance_status.py new file mode 100644 index 00000000000..fc9e05e836d --- /dev/null +++ b/erpnext/patches/v13_0/update_employee_advance_status.py @@ -0,0 +1,29 @@ +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() From d59c3d21423639c13055309ec30bfc2b04c44545 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 14:38:16 +0530 Subject: [PATCH 3/6] 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 f57d9d37cf1..18bd3b7733c 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -124,6 +124,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 806752111eadbe7b3679d13620c64dc38488ae50 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 18 Jan 2022 18:35:25 +0530 Subject: [PATCH 4/6] test: employee advance status --- .../employee_advance/employee_advance.py | 2 + .../employee_advance/test_employee_advance.py | 77 +++++++++++++++++++ 2 files changed, 79 insertions(+) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 7605d209ad1..2378482364d 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -286,6 +286,7 @@ def make_return_entry( "party_type": "Employee", "party": employee, "is_advance": "Yes", + "cost_center": erpnext.get_default_cost_center(company), }, ) @@ -303,6 +304,7 @@ def make_return_entry( "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, + "cost_center": erpnext.get_default_cost_center(company), }, ) diff --git a/erpnext/hr/doctype/employee_advance/test_employee_advance.py b/erpnext/hr/doctype/employee_advance/test_employee_advance.py index 9b006ffcffe..a8ff6a794d6 100644 --- a/erpnext/hr/doctype/employee_advance/test_employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/test_employee_advance.py @@ -12,8 +12,13 @@ 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 @@ -53,9 +58,79 @@ 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) @@ -85,11 +160,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() From b265ca467c6ff2d406786fb43b1c96281644b007 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 1 Mar 2022 21:40:39 +0530 Subject: [PATCH 5/6] test: test advance filters in expense claim and cancelled status --- .../employee_advance/employee_advance.py | 1 + .../employee_advance/test_employee_advance.py | 40 +++++++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.py b/erpnext/hr/doctype/employee_advance/employee_advance.py index 2378482364d..c1876b11757 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.py +++ b/erpnext/hr/doctype/employee_advance/employee_advance.py @@ -29,6 +29,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 a8ff6a794d6..44d68c94833 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 @@ -77,13 +84,28 @@ 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 @@ -120,12 +142,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}) From 78e9e66d63955b95e6b2f1c10b92c55973207234 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 23 May 2022 19:49:05 +0530 Subject: [PATCH 6/6] fix: replace document states with list settings --- .../employee_advance/employee_advance.json | 37 +------------------ .../employee_advance/employee_advance_list.js | 15 ++++++++ 2 files changed, 16 insertions(+), 36 deletions(-) create mode 100644 erpnext/hr/doctype/employee_advance/employee_advance_list.js diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json index 1b838ad9863..8b2eea11337 100644 --- a/erpnext/hr/doctype/employee_advance/employee_advance.json +++ b/erpnext/hr/doctype/employee_advance/employee_advance.json @@ -200,7 +200,7 @@ ], "is_submittable": 1, "links": [], - "modified": "2022-01-17 19:33:52.345823", + "modified": "2022-05-23 19:33:52.345823", "modified_by": "Administrator", "module": "HR", "name": "Employee Advance", @@ -236,41 +236,6 @@ "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_list.js b/erpnext/hr/doctype/employee_advance/employee_advance_list.js new file mode 100644 index 00000000000..433669a71f3 --- /dev/null +++ b/erpnext/hr/doctype/employee_advance/employee_advance_list.js @@ -0,0 +1,15 @@ +frappe.listview_settings["Employee Advance"] = { + get_indicator: function(doc) { + let status_color = { + "Draft": "red", + "Submitted": "blue", + "Cancelled": "red", + "Paid": "green", + "Unpaid": "orange", + "Claimed": "blue", + "Returned": "gray", + "Partly Claimed and Returned": "yellow" + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; \ No newline at end of file