Merge pull request #24176 from GangaManoj/Year-to-Date

feat: Add year_to_date field (cumulative salary for a fiscal year)
This commit is contained in:
Deepesh Garg 2020-12-30 16:53:47 +05:30 committed by GitHub
commit 5891b48cf6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 174 additions and 46 deletions

View File

@ -45,7 +45,7 @@ class TestLoan(unittest.TestCase):
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24))) create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
self.applicant1 = make_employee("robert_loan@loan.com") self.applicant1 = make_employee("robert_loan@loan.com")
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR') make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company")
if not frappe.db.exists("Customer", "_Test Loan Customer"): if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True) frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)

View File

@ -86,19 +86,21 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
self.assertEqual(declaration.total_exemption_amount, 100000) self.assertEqual(declaration.total_exemption_amount, 100000)
def create_payroll_period(): def create_payroll_period(**args):
if not frappe.db.exists("Payroll Period", "_Test Payroll Period"): args = frappe._dict(args)
name = args.name or "_Test Payroll Period"
if not frappe.db.exists("Payroll Period", name):
from datetime import date from datetime import date
payroll_period = frappe.get_doc(dict( payroll_period = frappe.get_doc(dict(
doctype = 'Payroll Period', doctype = 'Payroll Period',
name = "_Test Payroll Period", name = name,
company = erpnext.get_default_company(), company = args.company or erpnext.get_default_company(),
start_date = date(date.today().year, 1, 1), start_date = args.start_date or date(date.today().year, 1, 1),
end_date = date(date.today().year, 12, 31) end_date = args.end_date or date(date.today().year, 12, 31)
)).insert() )).insert()
return payroll_period return payroll_period
else: else:
return frappe.get_doc("Payroll Period", "_Test Payroll Period") return frappe.get_doc("Payroll Period", name)
def create_exemption_category(): def create_exemption_category():
if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"):

View File

@ -3,8 +3,11 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
# import frappe #import frappe
import erpnext
from frappe.model.document import Document from frappe.model.document import Document
class IncomeTaxSlab(Document): class IncomeTaxSlab(Document):
pass def validate(self):
if self.company:
self.currency = erpnext.get_company_currency(self.company)

View File

@ -125,15 +125,15 @@ frappe.ui.form.on("Salary Slip", {
change_form_labels: function(frm, company_currency) { change_form_labels: function(frm, company_currency) {
frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction", frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
"base_net_pay", "base_rounded_total", "base_total_in_words"], "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
company_currency); company_currency);
frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"], frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"],
frm.doc.currency); frm.doc.currency);
// toggle fields // toggle fields
frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction", frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction",
"base_net_pay", "base_rounded_total", "base_total_in_words"], "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
frm.doc.currency != company_currency); frm.doc.currency != company_currency);
}, },

View File

@ -69,9 +69,13 @@
"net_pay_info", "net_pay_info",
"net_pay", "net_pay",
"base_net_pay", "base_net_pay",
"year_to_date",
"base_year_to_date",
"column_break_53", "column_break_53",
"rounded_total", "rounded_total",
"base_rounded_total", "base_rounded_total",
"month_to_date",
"base_month_to_date",
"section_break_55", "section_break_55",
"total_in_words", "total_in_words",
"column_break_69", "column_break_69",
@ -578,13 +582,41 @@
{ {
"fieldname": "column_break_69", "fieldname": "column_break_69",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "year_to_date",
"fieldtype": "Currency",
"label": "Year To Date",
"options": "currency",
"read_only": 1
},
{
"fieldname": "month_to_date",
"fieldtype": "Currency",
"label": "Month To Date",
"options": "currency",
"read_only": 1
},
{
"fieldname": "base_year_to_date",
"fieldtype": "Currency",
"label": "Year To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "base_month_to_date",
"fieldtype": "Currency",
"label": "Month To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 9, "idx": 9,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-10-21 23:02:59.400249", "modified": "2020-12-21 23:43:44.959840",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Payroll", "module": "Payroll",
"name": "Salary Slip", "name": "Salary Slip",

View File

@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
import datetime, math import datetime, math
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe import msgprint, _ from frappe import msgprint, _
@ -18,6 +18,7 @@ from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_fac
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
from erpnext.accounts.utils import get_fiscal_year
class SalarySlip(TransactionBase): class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
@ -49,6 +50,8 @@ class SalarySlip(TransactionBase):
self.get_working_days_details(lwp = self.leave_without_pay) self.get_working_days_details(lwp = self.leave_without_pay)
self.calculate_net_pay() self.calculate_net_pay()
self.compute_year_to_date()
self.compute_month_to_date()
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"): if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet") max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
@ -1125,6 +1128,46 @@ class SalarySlip(TransactionBase):
self.gross_pay += self.earnings[i].amount self.gross_pay += self.earnings[i].amount
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
def compute_year_to_date(self):
year_to_date = 0
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
if payroll_period:
period_start_date = payroll_period.start_date
period_end_date = payroll_period.end_date
else:
# get dates based on fiscal year if no payroll period exists
fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
period_start_date = fiscal_year.year_start_date
period_end_date = fiscal_year.year_end_date
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', period_start_date],
'end_date' : ['<', period_end_date]})
year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
year_to_date += self.net_pay
self.year_to_date = year_to_date
def compute_month_to_date(self):
month_to_date = 0
first_day_of_the_month = get_first_day(self.start_date)
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'],
filters = {'employee_name' : self.employee_name,
'start_date' : ['>=', first_day_of_the_month],
'end_date' : ['<', self.start_date]
})
month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
month_to_date += self.net_pay
self.month_to_date = month_to_date
def unlink_ref_doc_from_salary_slip(ref_no): def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip` linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
where journal_entry=%s and docstatus < 2""", (ref_no)) where journal_entry=%s and docstatus < 2""", (ref_no))

View File

@ -9,7 +9,7 @@ import calendar
import random import random
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from frappe.utils.make_random import get_random from frappe.utils.make_random import get_random
from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
@ -240,7 +240,11 @@ class TestSalarySlip(unittest.TestCase):
interest_income_account='Interest Income Account - _TC', interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC') penalty_income_account='Penalty Income Account - _TC')
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR') payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period)
frappe.db.sql("""delete from `tabLoan""") frappe.db.sql("""delete from `tabLoan""")
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1 loan.repay_from_salary = 1
@ -290,6 +294,33 @@ class TestSalarySlip(unittest.TestCase):
self.assertEqual(salary_slip.gross_pay, 78000) self.assertEqual(salary_slip.gross_pay, 78000)
self.assertEqual(salary_slip.base_gross_pay, 78000*70) self.assertEqual(salary_slip.base_gross_pay, 78000*70)
def test_year_to_date_computation(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
applicant = make_employee("test_ytd@salary.com", company="_Test Company")
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"),
company="_Test Company")
salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD",
"Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
# clear salary slip for this employee
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
create_salary_slips_for_payroll_period(applicant, salary_structure.name,
payroll_period, deduct_random=False)
salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name':
'test_ytd@salary.com'}, order_by = 'posting_date')
year_to_date = 0
for slip in salary_slips:
year_to_date += slip.net_pay
self.assertEqual(slip.year_to_date, year_to_date)
def test_tax_for_payroll_period(self): def test_tax_for_payroll_period(self):
data = {} data = {}
# test the impact of tax exemption declaration, tax exemption proof submission # test the impact of tax exemption declaration, tax exemption proof submission
@ -410,10 +441,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip" salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
employee = frappe.db.get_value("Employee", {"user_id": user}) employee = frappe.db.get_value("Employee", {"user_id": user})
if not frappe.db.exists('Salary Structure', salary_structure): salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee)
else:
salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}) salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
if not salary_slip_name: if not salary_slip_name:
@ -631,8 +659,13 @@ def create_benefit_claim(employee, payroll_period, amount, component):
}).submit() }).submit()
return claim_date return claim_date
def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()): def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None,
frappe.db.sql("""delete from `tabIncome Tax Slab`""") company=None):
if not currency:
currency = erpnext.get_default_currency()
if company:
currency = erpnext.get_company_currency(company)
slabs = [ slabs = [
{ {
@ -652,26 +685,33 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption =
} }
] ]
income_tax_slab = frappe.new_doc("Income Tax Slab") income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency})
income_tax_slab.name = "Tax Slab: " + payroll_period.name if not income_tax_slab_name:
income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2) income_tax_slab = frappe.new_doc("Income Tax Slab")
income_tax_slab.currency = currency income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency)
income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2)
income_tax_slab.company = company or ''
income_tax_slab.currency = currency
if allow_tax_exemption: if allow_tax_exemption:
income_tax_slab.allow_tax_exemption = 1 income_tax_slab.allow_tax_exemption = 1
income_tax_slab.standard_tax_exemption_amount = 50000 income_tax_slab.standard_tax_exemption_amount = 50000
for item in slabs: for item in slabs:
income_tax_slab.append("slabs", item) income_tax_slab.append("slabs", item)
income_tax_slab.append("other_taxes_and_charges", { income_tax_slab.append("other_taxes_and_charges", {
"description": "cess", "description": "cess",
"percent": 4 "percent": 4
}) })
income_tax_slab.save() income_tax_slab.save()
if not dont_submit: if not dont_submit:
income_tax_slab.submit() income_tax_slab.submit()
return income_tax_slab.name
else:
return income_tax_slab_name
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True): def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True):
deducted_dates = [] deducted_dates = []

View File

@ -114,7 +114,7 @@ class TestSalaryStructure(unittest.TestCase):
self.assertEqual(sal_struct.currency, 'USD') self.assertEqual(sal_struct.currency, 'USD')
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None, def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
test_tax=False, company=None, currency=erpnext.get_default_currency()): test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None):
if test_tax: if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure)) frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
@ -141,16 +141,24 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
if employee and not frappe.db.get_value("Salary Structure Assignment", if employee and not frappe.db.get_value("Salary Structure Assignment",
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1: {'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency) create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency,
payroll_period=payroll_period)
return salary_structure_doc return salary_structure_doc
def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()): def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(),
payroll_period=None):
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}): if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee)) frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
payroll_period = create_payroll_period() if not payroll_period:
create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency) payroll_period = create_payroll_period()
income_tax_slab = frappe.db.get_value("Income Tax Slab", {"currency": currency})
if not income_tax_slab:
income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency)
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment") salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
salary_structure_assignment.employee = employee salary_structure_assignment.employee = employee
@ -162,7 +170,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
salary_structure_assignment.payroll_payable_account = get_payable_account(company) salary_structure_assignment.payroll_payable_account = get_payable_account(company)
salary_structure_assignment.company = company or erpnext.get_default_company() salary_structure_assignment.company = company or erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True) salary_structure_assignment.save(ignore_permissions=True)
salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period" salary_structure_assignment.income_tax_slab = income_tax_slab
salary_structure_assignment.submit() salary_structure_assignment.submit()
return salary_structure_assignment return salary_structure_assignment