feat: Full and Final Settlement and Gratuity Fix (#26364)

* feat: Full and Final Settlement

* removed option to pay via salary slip

* feat: Create JV

* test:fnf

* fix: tracking asset movement

* fix: sider and test

* fix: changes Requested

* fix: changes requested

* fix: valication for Asset

* fix: add filter for reference document only if relevant field is present

* fix: doctype cleanup

- add more fields to the list view and standard filter

- set title field

- incorrect field labels

* feat: add list view settings for FNF

* fix: incorrect reference type set in Journal Entry

* fix: validation message

* chore: add Full and Final Statement link to Workspace

Co-authored-by: Rucha Mahabal <ruchamahabal2@gmail.com>
This commit is contained in:
Anurag Mishra
2021-08-31 17:59:26 +05:30
committed by GitHub
parent 48f2e5ac1d
commit ab47409e91
23 changed files with 835 additions and 84 deletions

View File

@@ -43,8 +43,6 @@ frappe.ui.form.on("Employee Referral", {
});
}
},
create_job_applicant: function(frm) {
frappe.model.open_mapped_doc({

View File

@@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Full and Final Asset', {
// refresh: function(frm) {
// }
});

View File

@@ -0,0 +1,64 @@
{
"actions": [],
"creation": "2021-06-28 13:36:58.658985",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"reference",
"asset_name",
"date",
"status",
"description"
],
"fields": [
{
"fieldname": "reference",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference",
"options": "Asset Movement",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Owned\nReturned",
"reqd": 1
},
{
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description"
},
{
"fieldname": "asset_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Asset Name",
"read_only": 1
},
{
"fieldname": "date",
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Date",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-15 15:17:31.309834",
"modified_by": "Administrator",
"module": "HR",
"name": "Full and Final Asset",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestFullandFinalAsset(unittest.TestCase):
pass

View File

@@ -0,0 +1,96 @@
{
"actions": [],
"creation": "2021-06-28 13:32:02.167317",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"component",
"reference_document_type",
"reference_document",
"account",
"paid_via_salary_slip",
"column_break_4",
"amount",
"status",
"remark"
],
"fields": [
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"columns": 2,
"default": "Unsettled",
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
"options": "Settled\nUnsettled"
},
{
"fieldname": "remark",
"fieldtype": "Small Text",
"label": "Remark"
},
{
"columns": 2,
"depends_on": "reference_document_type",
"fieldname": "reference_document",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Document",
"mandatory_depends_on": "reference_document_type",
"options": "reference_document_type",
"search_index": 1
},
{
"columns": 2,
"fieldname": "component",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Component",
"reqd": 1
},
{
"fieldname": "account",
"fieldtype": "Link",
"label": "Account",
"options": "Account"
},
{
"columns": 2,
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount"
},
{
"columns": 2,
"fieldname": "reference_document_type",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Reference Document Type",
"options": "DocType"
},
{
"default": "0",
"fieldname": "paid_via_salary_slip",
"fieldtype": "Check",
"label": "Paid via Salary Slip"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-20 16:59:34.447934",
"modified_by": "Administrator",
"module": "HR",
"name": "Full and Final Outstanding Statement",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -0,0 +1,115 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Full and Final Statement', {
refresh: function(frm) {
frm.events.set_queries(frm, "payables");
frm.events.set_queries(frm, "receivables");
if (frm.doc.docstatus == 1 && frm.doc.status == "Unpaid") {
frm.add_custom_button(__("Create Journal Entry"), function () {
frm.events.create_journal_entry(frm);
});
}
},
set_queries: function(frm, type) {
frm.set_query("reference_document_type", type, function () {
let modules = ["HR", "Payroll", "Loan Management"];
return {
filters: {
istable: 0,
issingle: 0,
module: ["In", modules]
}
};
});
let filters = {};
frm.set_query('reference_document', type, function(doc, cdt, cdn) {
let fnf_doc = frappe.get_doc(cdt, cdn);
frappe.model.with_doctype(fnf_doc.reference_document_type, function() {
if (frappe.model.is_tree(fnf_doc.reference_document_type)) {
filters['is_group'] = 0;
}
if (frappe.meta.has_field(fnf_doc.reference_document_type, 'company')) {
filters['company'] = frm.doc.company;
}
if (frappe.meta.has_field(fnf_doc.reference_document_type, 'employee')) {
filters['employee'] = frm.doc.employee;
}
});
return {
filters: filters
};
});
},
employee: function(frm) {
frm.events.get_outstanding_statements(frm);
},
get_outstanding_statements: function(frm) {
if (frm.doc.employee) {
frappe.call({
method: "get_outstanding_statements",
doc: frm.doc,
callback: function() {
frm.refresh();
}
});
}
},
create_journal_entry: function(frm) {
frappe.call({
method: "create_journal_entry",
doc: frm.doc,
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
}
});
frappe.ui.form.on("Full and Final Outstanding Statement", {
reference_document: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
if (child.reference_document_type && child.reference_document) {
frappe.call({
method: "erpnext.hr.doctype.full_and_final_statement.full_and_final_statement.get_account_and_amount",
args: {
ref_doctype: child.reference_document_type,
ref_document: child.reference_document
},
callback: function(r) {
if (r.message) {
frappe.model.set_value(cdt, cdn, "account", r.message[0]);
frappe.model.set_value(cdt, cdn, "amount", r.message[1]);
}
}
});
}
},
amount: function(frm) {
var total_payable_amount = 0;
var total_receivable_amount = 0;
frm.doc.payables.forEach(element => {
total_payable_amount = total_payable_amount + element.amount;
});
frm.doc.receivables.forEach(element => {
total_receivable_amount = total_receivable_amount + element.amount;
});
frm.set_value("total_payable_amount", flt(total_payable_amount));
frm.set_value("total_receivable_amount", flt(total_receivable_amount));
}
});

View File

@@ -0,0 +1,231 @@
{
"actions": [],
"autoname": "HR-FNF-.YYYY.-.#####",
"creation": "2021-06-28 13:17:36.050459",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"transaction_date",
"column_break_12",
"company",
"status",
"amended_from",
"employee_details_section",
"date_of_joining",
"relieving_date",
"column_break_4",
"designation",
"department",
"section_break_8",
"payables",
"section_break_10",
"receivables",
"totals_section",
"total_payable_amount",
"column_break_21",
"total_receivable_amount",
"section_break_15",
"assets_allocated"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fetch_from": "employee.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"default": "Unpaid",
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Paid\nUnpaid",
"read_only": 1
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Full and Final Statement",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"label": "Payables"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break",
"label": "Receivables"
},
{
"fieldname": "assets_allocated",
"fieldtype": "Table",
"options": "Full and Final Asset"
},
{
"fetch_from": "employee.relieving_date",
"fieldname": "relieving_date",
"fieldtype": "Date",
"label": "Relieving Date ",
"read_only": 1,
"reqd": 1
},
{
"fetch_from": "employee.date_of_joining",
"fieldname": "date_of_joining",
"fieldtype": "Date",
"label": "Date of Joining",
"read_only": 1
},
{
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Assets Allocated"
},
{
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"read_only": 1
},
{
"fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fieldname": "payables",
"fieldtype": "Table",
"options": "Full and Final Outstanding Statement"
},
{
"fieldname": "receivables",
"fieldtype": "Table",
"options": "Full and Final Outstanding Statement"
},
{
"fieldname": "employee_details_section",
"fieldtype": "Section Break",
"label": "Employee Details"
},
{
"fieldname": "transaction_date",
"fieldtype": "Date",
"in_standard_filter": 1,
"label": "Transaction Date",
"reqd": 1
},
{
"fieldname": "totals_section",
"fieldtype": "Section Break",
"label": "Totals"
},
{
"fieldname": "total_payable_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total Payable Amount",
"read_only": 1
},
{
"fieldname": "column_break_21",
"fieldtype": "Column Break"
},
{
"fieldname": "total_receivable_amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total Receivable Amount",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-08-30 21:11:09.892560",
"modified_by": "Administrator",
"module": "HR",
"name": "Full and Final Statement",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
}

View File

@@ -0,0 +1,176 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.utils import get_link_to_form, today, flt
from frappe.model.document import Document
class FullandFinalStatement(Document):
def validate(self):
self.get_outstanding_statements()
if self.docstatus == 1:
self.validate_settlement("payables")
self.validate_settlement("receivables")
self.validate_asset()
def validate_settlement(self, component_type):
for data in self.get(component_type, []):
if data.status == "Unsettled":
frappe.throw(_("Settle all Payables and Receivables before submission"))
def validate_asset(self):
for data in self.assets_allocated:
if data.status == "Owned":
frappe.throw(_("All allocated assets should be returned before submission"))
@frappe.whitelist()
def get_outstanding_statements(self):
if self.relieving_date:
if not len(self.get("payables", [])):
components = self.get_payable_component()
self.create_component_row(components, "payables")
if not len(self.get("receivables", [])):
components = self.get_receivable_component()
self.create_component_row(components, "receivables")
if not len(self.get("assets_allocated", [])):
for data in self.get_assets_movement():
self.append("assets_allocated", data)
else:
frappe.throw(_("Set Relieving Date for Employee: {0}").format(get_link_to_form("Employee", self.employee)))
def create_component_row(self, components, component_type):
for component in components:
self.append(component_type, {
"status": "Unsettled",
"reference_document_type": component if component != "Bonus" else "Additional Salary",
"component": component
})
def get_payable_component(self):
return [
"Salary Slip",
"Gratuity",
"Expense Claim",
"Bonus",
"Leave Encashment",
]
def get_receivable_component(self):
return [
"Loan",
"Employee Advance",
]
def get_assets_movement(self):
asset_movements = frappe.get_all("Asset Movement Item",
filters = {"docstatus": 1},
fields = ["asset", "from_employee", "to_employee", "parent", "asset_name"],
or_filters = {
"from_employee": self.employee,
"to_employee": self.employee
}
)
data = []
inward_movements = []
outward_movements = []
for movement in asset_movements:
if movement.to_employee and movement.to_employee == self.employee:
inward_movements.append(movement)
if movement.from_employee and movement.from_employee == self.employee:
outward_movements.append(movement)
for movement in inward_movements:
outwards_count = [movement.asset for movement in outward_movements].count(movement.asset)
inwards_counts = [movement.asset for movement in inward_movements].count(movement.asset)
if inwards_counts > outwards_count:
data.append({
"reference": movement.parent,
"asset_name": movement.asset_name,
"date": frappe.db.get_value("Asset Movement", movement.parent, "transaction_date"),
"status": "Owned"
})
return data
@frappe.whitelist()
def create_journal_entry(self):
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
jv = frappe.new_doc("Journal Entry")
jv.company = self.company
jv.voucher_type = "Bank Entry"
jv.posting_date = today()
difference = self.total_payable_amount - self.total_receivable_amount
for data in self.payables:
if data.amount > 0 and not data.paid_via_salary_slip:
account_dict = {
"account": data.account,
"debit_in_account_currency": flt(data.amount, precision)
}
if data.reference_document_type == "Expense Claim":
account_dict["party_type"] = "Employee"
account_dict["party"] = self.employee
jv.append("accounts", account_dict)
for data in self.receivables:
if data.amount > 0:
account_dict = {
"account": data.account,
"credit_in_account_currency": flt(data.amount, precision)
}
if data.reference_document_type == "Employee Advance":
account_dict["party_type"] = "Employee"
account_dict["party"] = self.employee
jv.append("accounts", account_dict)
jv.append("accounts", {
"credit_in_account_currency": difference if difference > 0 else 0,
"debit_in_account_currency": -(difference) if difference < 0 else 0,
"reference_type": self.doctype,
"reference_name": self.name
})
return jv
@frappe.whitelist()
def get_account_and_amount(ref_doctype, ref_document):
if not ref_doctype or not ref_document:
return None
if ref_doctype == "Salary Slip":
salary_details = frappe.db.get_value("Salary Slip", ref_document, ["payroll_entry", "net_pay"], as_dict=1)
amount = salary_details.net_pay
payable_account = frappe.db.get_value("Payroll Entry", salary_details.payroll_entry, "payroll_payable_account") if salary_details.payroll_entry else None
return [payable_account, amount]
if ref_doctype == "Gratuity":
payable_account, amount = frappe.db.get_value("Gratuity", ref_document, ["payable_account", "amount"])
return [payable_account, amount]
if ref_doctype == "Expense Claim":
details = frappe.db.get_value("Expense Claim", ref_document,
["payable_account", "grand_total", "total_amount_reimbursed", "total_advance_amount"], as_dict=True)
payable_account = details.payable_account
amount = details.grand_total - (details.total_amount_reimbursed + details.total_advance_amount)
return [payable_account, amount]
if ref_doctype == "Loan":
details = frappe.db.get_value("Loan", ref_document,
["payment_account", "total_payment", "total_amount_paid"], as_dict=1)
payment_account = details.payment_account
amount = details.total_payment - details.total_amount_paid
return [payment_account, amount]
if ref_doctype == "Employee Advance":
details = frappe.db.get_value("Employee Advance", ref_document,
["advance_account","paid_amount", "claimed_amount", "return_amount"], as_dict = 1)
payment_account = details.advance_account
amount = details.paid_amount - (details.claimed_amount + details.return_amount)
return [payment_account, amount]

View File

@@ -0,0 +1,11 @@
frappe.listview_settings["Full and Final Statement"] = {
get_indicator: function(doc) {
var colors = {
"Draft": "red",
"Unpaid": "orange",
"Paid": "green",
"Cancelled": "red"
};
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
}
};

View File

@@ -0,0 +1,71 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from frappe.utils import today, add_days
import unittest
class TestFullandFinalStatement(unittest.TestCase):
def setUp(self):
create_asset_data()
def tearDown(self):
frappe.db.sql("Delete from `tabFull and Final Statement`")
frappe.db.sql("Delete from `tabAsset`")
frappe.db.sql("Delete from `tabAsset Movement`")
def test_check_bootstraped_data_asset_movement_and_jv_creation(self):
employee = make_employee("test_fnf@example.com", company="_Test Company")
movement = create_asset_movement(employee)
frappe.db.set_value("Employee", employee, "relieving_date", add_days(today(), 30))
fnf = create_full_and_final_statement(employee)
payables_bootstraped_component = ["Salary Slip", "Gratuity",
"Expense Claim", "Bonus", "Leave Encashment"]
receivable_bootstraped_component = ["Loan", "Employee Advance"]
#checking payable s and receivables bootstraped value
self.assertEqual([payable.component for payable in fnf.payables], payables_bootstraped_component)
self.assertEqual([receivable.component for receivable in fnf.receivables], receivable_bootstraped_component)
#checking allocated asset
self.assertIn(movement, [asset.reference for asset in fnf.assets_allocated])
def create_full_and_final_statement(employee):
fnf = frappe.new_doc("Full and Final Statement")
fnf.employee = employee
fnf.transaction_date = today()
fnf.save()
return fnf
def create_asset_movement(employee):
asset_name = create_asset()
movement = frappe.new_doc("Asset Movement")
movement.company = "_Test Company"
movement.purpose = "Issue"
movement.transaction_date = today()
movement.append("assets", {
"asset": asset_name,
"to_employee": employee
})
movement.save()
movement.submit()
return movement.name
def create_asset():
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=100000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset = frappe.get_doc("Asset", asset_name)
asset.calculate_depreciation = 0
asset.available_for_use_date = today()
asset.submit()
return asset_name

View File

@@ -223,6 +223,17 @@
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Full and Final Statement",
"link_count": 0,
"link_to": "Full and Final Statement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@@ -931,7 +942,7 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:59.842918",
"modified": "2021-08-31 12:18:59.842918",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",