[WIP] Formula based Salary Structure and multiple employees for same Salary Structure

This commit is contained in:
Kanchan Chauhan 2016-07-29 12:56:30 +05:30
parent 0c1be8df35
commit bccf0fa041
29 changed files with 1242 additions and 485 deletions

View File

@ -0,0 +1,90 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2016-07-27 17:24:24.956896",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Default Bank / Cash account will be automatically updated in POS Invoice when this mode is selected.",
"fieldname": "default_account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Default Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-07-27 17:24:24.956896",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Salary Component Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class SalaryComponentAccount(Document):
pass

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 105 KiB

After

Width:  |  Height:  |  Size: 231 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View File

@ -42,15 +42,52 @@ To create a new Salary Structure go to:
> Human Resources > Setup > Salary Structure > New Salary Structure
#### Figure 1:Salary Structure
#### Figure 1.1:Salary Structure
<img class="screenshot" alt="Salary Structure" src="{{docs_base_url}}/assets/img/human-resources/salary-structure.png">
### In the Salary Structure,
* Select the Employee
* Select the Employees and enter Base (which is base salary or CTC) and Variable (if applicable)
* Set the starting date from which this is valid (Note: There can only be one Salary Structure that can be “Active” for an Employee during any period)
* In the “Earnings” and “Deductions” table all your defined Earning Type and Deductions Type will be auto-populated. Set the values of the Earnings and Deductions and save the Salary Structure.
#### Figure 1.2:Salary Structure for Salary Slip based on Timesheet
<img class="screenshot" alt="Salary Structure" src="{{docs_base_url}}/assets/img/human-resources/salary-timesheet.png">
### Salary Slip Based on Timesheet
Salary Slip based on Timesheet is applicable if you have timesheet based payroll system
* Check "Salary Slip Based on Timesheet"
* Select the salary component and enter Hour Rate (Note: This salary component gets added to earnings in Salary Slip)
### Earnings and Deductions in Salary Structure
In the “Earnings” and “Deductions” tables, you can calculate the values of Salary Components based on,
* Condition and Formula
#### Figure 1.3:Condition and Formula
<img class="screenshot" alt="Salary Structure" src="{{docs_base_url}}/assets/img/human-resources/condition-formula.png">
* Condition and Amount
#### Figure 1.4:Condition and Amount
<img class="screenshot" alt="Salary Structure" src="{{docs_base_url}}/assets/img/human-resources/condition-amount.png">
* Only Formula
* Only Amount
Save the Salary Structure.
In conditions and formulas,
* Use field "base" for using base salary of the Employee
* Use Salary Component abbreviations. For example: BS for Basic Salary
* Use field name for employee details. For example: Employment Type for employment_type
### Leave Without Pay (LWP)
@ -64,6 +101,7 @@ days for the month (based on the Holiday List).
If you dont want ERPNext to manage LWP, just dont click on LWP in any of the
Earning Types and Deduction Types.
* * *
### Creating Salary Slips
@ -71,8 +109,9 @@ Earning Types and Deduction Types.
Once the Salary Structure is created, you can make a salary slip from the same
form or you can process your payroll for the month using Process Payroll.
To create a salary slip from Salary Structure, click on the button Make Salary
Slip.
To create a new Salary Slip go to:
> Human Resources > Setup > Salary Slip > New Salary Slip
#### Figure 2: Salary Slip

View File

@ -40,13 +40,6 @@ erpnext.hr.EmployeeController = frappe.ui.form.Controller.extend({
"Ms": "Female"
}[this.frm.doc.salutation]);
}
},
make_salary_structure: function(btn) {
frappe.model.open_mapped_doc({
method: "erpnext.hr.doctype.employee.employee.make_salary_structure",
frm: cur_frm
});
}
});
cur_frm.cscript = new erpnext.hr.EmployeeController({frm: cur_frm});

View File

@ -178,19 +178,6 @@ def get_retirement_date(date_of_birth=None):
return ret
@frappe.whitelist()
def make_salary_structure(source_name, target=None):
target = get_mapped_doc("Employee", source_name, {
"Employee": {
"doctype": "Salary Structure",
"field_map": {
"name": "employee",
}
}
})
target.make_earn_ded_table()
return target
def validate_employee_role(doc, method):
# called via User hook
if "Employee" in [d.role for d in doc.get("user_roles")]:

View File

@ -8,6 +8,7 @@
"docstatus": 0,
"doctype": "DocType",
"document_type": "Other",
"editable_grid": 1,
"fields": [
{
"allow_on_submit": 0,
@ -187,6 +188,31 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "max_working_hours_against_timesheet",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Max working hours against Timesheet",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
@ -200,7 +226,7 @@
"issingle": 1,
"istable": 0,
"max_attachments": 0,
"modified": "2016-06-27 16:20:59.737869",
"modified": "2016-08-10 12:32:39.780599",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

@ -9,6 +9,7 @@
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"fields": [
{
"allow_on_submit": 0,
@ -35,6 +36,33 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "salary_component_abbr",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 1,
"label": "Abbr",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "120px",
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "120px"
},
{
"allow_on_submit": 0,
"bold": 0,
@ -59,6 +87,32 @@
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"options": "Salary Component Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
@ -72,7 +126,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-07-01 12:42:46.103131",
"modified": "2016-07-27 17:40:18.335540",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Component",

View File

@ -7,4 +7,21 @@ import frappe
from frappe.model.document import Document
class SalaryComponent(Document):
pass
def validate(self):
self.validate_abbr()
def validate_abbr(self):
if not self.salary_component_abbr:
self.salary_component_abbr = ''.join([c[0] for c in self.salary_component.split()]).upper()
self.salary_component_abbr = self.salary_component_abbr.strip()
if self.get('__islocal') and len(self.salary_component_abbr) > 5:
frappe.throw(_("Abbreviation cannot have more than 5 characters"))
if not self.salary_component_abbr.strip():
frappe.throw(_("Abbreviation is mandatory"))
if frappe.db.sql("select salary_component_abbr from `tabSalary Component` where name!=%s and salary_component_abbr=%s", (self.name, self.salary_component_abbr)):
frappe.throw(_("Abbreviation already used for another salary component"))

View File

@ -40,13 +40,171 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "abbr",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Abbr",
"length": 0,
"no_copy": 0,
"options": "salary_component.salary_component_abbr",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "condition",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Condition",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "1",
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "amount_based_on_formula",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Amount based on formula",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"default": "",
"depends_on": "eval:doc.amount_based_on_formula!=0 && doc.parenttype=='Salary Structure'",
"description": "",
"fieldname": "formula",
"fieldtype": "Code",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Formula",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "section_break_8",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.amount_based_on_formula!==1 || doc.parenttype==='Salary Slip'",
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"in_list_view": 0,
"label": "Amount",
"length": 0,
"no_copy": 0,
@ -66,30 +224,7 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "",
"fieldname": "depends_on_lwp",
"fieldtype": "Check",
"hidden": 0,
@ -102,6 +237,33 @@
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "default_amount",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Default Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
@ -115,17 +277,43 @@
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "default_amount",
"fieldtype": "Currency",
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Default Amount",
"length": 0,
"no_copy": 0,
"options": "Company:company:default_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"depends_on": "eval:doc.parenttype=='Salary Structure'",
"fieldname": "condition_and_formula_help",
"fieldtype": "HTML",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Condition and Formula Help",
"length": 0,
"no_copy": 0,
"options": "<h3>Condition and Formula Help</h3>\n\n<p>Notes:</p>\n\n<ol>\n<li>Use field <code>base</code> for using base salary of the Employee</li>\n<li>Use Salary Component abbreviations in conditions and formulas. <code>BS = Basic Salary</code></li>\n<li>Use field name for employee details in conditions and formulas. <code>Employment Type = employment_type</code><code>Branch = branch</code></li>\n<li>Direct Amount can also be entered based on Condtion. See example 3</li></ol>\n\n<h4>Examples</h4>\n<ol>\n<li>Calculating Basic Salary based on <code>base</code>\n<pre><code>Condition: base &lt; 10000</code></pre>\n<pre><code>Formula: base * .2</code></pre></li>\n<li>Calculating HRA based on Basic Salary<code>BS</code> \n<pre><code>Condition: BS &gt; 2000</code></pre>\n<pre><code>Formula: BS * .1</code></pre></li>\n<li>Calculating TDS based on Employment Type<code>employment_type</code> \n<pre><code>Condition: employment_type==\"Intern\"</code></pre>\n<pre><code>Amount: 1000</code></pre></li>\n</ol>",
"permlevel": 0,
"precision": "",
"print_hide": 0,
@ -148,7 +336,7 @@
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-07-11 03:28:06.925361",
"modified": "2016-08-18 13:01:37.617174",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Detail",

View File

@ -24,7 +24,7 @@ frappe.ui.form.on("Salary Slip", {
refresh: function(frm) {
frm.trigger("toggle_fields")
},
},
salary_slip_based_on_timesheet: function(frm) {
frm.trigger("toggle_fields")

View File

@ -9,6 +9,7 @@
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 0,
"fields": [
{
"allow_on_submit": 0,
@ -1172,7 +1173,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-07-07 12:49:01.596547",
"modified": "2016-08-10 15:57:59.944600",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Slip",

View File

@ -25,23 +25,84 @@ class SalarySlip(TransactionBase):
self.set_month_dates()
if not (len(self.get("earnings")) or len(self.get("deductions"))):
# get details from salary structure
self.get_emp_and_leave_details()
else:
self.get_leave_details(lwp = self.leave_without_pay)
if self.salary_slip_based_on_timesheet or not self.net_pay:
self.calculate_net_pay()
# if self.salary_slip_based_on_timesheet or not self.net_pay:
# self.calculate_net_pay()
company_currency = get_company_currency(self.company)
self.total_in_words = money_in_words(self.rounded_total, company_currency)
set_employee_name(self)
if frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("HR Settings", "max_working_hours_against_timesheet")
if self.salary_slip_based_on_timesheet and (self.total_working_hours > int(max_working_hours)):
frappe.msgprint(_("Total working hours should not be greater than max working hours {0}").
format(max_working_hours), alert=True)
def validate_dates(self):
if date_diff(self.end_date, self.start_date) < 0:
frappe.throw(_("To date cannot be before From date"))
def calculate_component_amounts(self):
if not getattr(self, '_salary_structure_doc', None):
self._salary_structure_doc = frappe.get_doc('Salary Structure', self.salary_structure)
data = self.get_data_for_eval()
for key in ('earnings', 'deductions'):
for d in self._salary_structure_doc.get(key):
amount = self.eval_condition_and_formula(d, data)
if amount:
self.append(key, {
'amount': amount,
'default_amount': amount,
'depends_on_lwp' : d.depends_on_lwp,
'salary_component' : d.salary_component
})
def eval_condition_and_formula(self, d, data):
try:
if d.condition:
if not eval(d.condition, None, data):
return None
amount = d.amount
if d.amount_based_on_formula:
if d.formula:
amount = eval(d.formula, None, data)
data[d.abbr] = amount
return amount
except NameError as err:
frappe.throw(_("Name error: {0}".format(err)))
except SyntaxError as err:
frappe.throw(_("Syntax error in formula or condition: {0}".format(err)))
except:
frappe.throw(_("Error in formula or condition"))
raise
def get_data_for_eval(self):
'''Returns data for evaluating formula'''
data = frappe._dict()
for d in self._salary_structure_doc.employees:
if d.employee == self.employee:
data.base, data.variable = d.base, d.variable
data.update(frappe.get_doc("Employee", self.employee).as_dict())
# set values for components
salary_components = frappe.get_all("Salary Component", fields=["salary_component_abbr"])
for salary_component in salary_components:
data[salary_component.salary_component_abbr] = 0
return data
def get_emp_and_leave_details(self):
'''First time, load all the components from salary structure'''
if self.employee:
self.set("earnings", [])
self.set("deductions", [])
@ -55,10 +116,10 @@ class SalarySlip(TransactionBase):
struct = self.check_sal_struct(joining_date, relieving_date)
if struct:
ss_doc = frappe.get_doc('Salary Structure', struct)
self.salary_slip_based_on_timesheet = ss_doc.salary_slip_based_on_timesheet or 0
self._salary_structure_doc = frappe.get_doc('Salary Structure', struct)
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet()
self.pull_sal_struct(ss_doc)
self.pull_sal_struct()
def set_time_sheet(self):
if self.salary_slip_based_on_timesheet:
@ -79,28 +140,44 @@ class SalarySlip(TransactionBase):
self.end_date = m['month_end_date']
def check_sal_struct(self, joining_date, relieving_date):
struct = frappe.db.sql("""select name from `tabSalary Structure`
where employee=%s and is_active = 'Yes'
and (from_date <= %s or from_date <= %s)
and (to_date is null or to_date >= %s or to_date >= %s) order by from_date desc limit 1""",
(self.employee, self.start_date, joining_date, self.end_date, relieving_date))
st_name = frappe.db.sql("""select parent from `tabSalary Structure Employee`
where employee=%s order by modified desc limit 1""",self.employee)
if st_name:
struct = frappe.db.sql("""select name from `tabSalary Structure`
where name=%s and is_active = 'Yes'
and (from_date <= %s or from_date <= %s)
and (to_date is null or to_date >= %s or to_date >= %s) order by from_date desc limit 1""",
(st_name, self.start_date, joining_date, self.end_date, relieving_date))
if not struct:
if not struct:
self.salary_structure = None
frappe.throw(_("No active or default Salary Structure found for employee {0} for the given dates")
.format(self.employee), title=_('Salary Structure Missing'))
return struct and struct[0][0] or ''
else:
self.salary_structure = None
frappe.throw(_("No active or default Salary Structure found for employee {0} for the given dates")
.format(self.employee), title=_('Salary Structure Missing'))
.format(self.employee), title=_('Salary Structure Missing'))
return struct and struct[0][0] or ''
def pull_sal_struct(self, ss_doc):
def pull_sal_struct(self):
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
make_salary_slip(ss_doc.name, self)
make_salary_slip(self._salary_structure_doc.name, self)
if self.salary_slip_based_on_timesheet:
self.salary_structure = ss_doc.name
self.hour_rate = ss_doc.hour_rate
self.salary_structure = self._salary_structure_doc.name
self.hour_rate = self._salary_structure_doc.hour_rate
self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0
self.add_earning_for_hourly_wages(ss_doc.salary_component)
self.add_earning_for_hourly_wages(self._salary_structure_doc.salary_component)
def process_salary_structure(self):
'''Calculate salary after salary structure details have been updated'''
self.pull_emp_details()
self.get_leave_details()
self.calculate_net_pay()
def add_earning_for_hourly_wages(self, salary_component):
default_type = False
@ -121,6 +198,7 @@ class SalarySlip(TransactionBase):
self.bank_name = emp.bank_name
self.bank_account_no = emp.bank_ac_no
def get_leave_details(self, joining_date=None, relieving_date=None, lwp=None):
if not self.fiscal_year:
# if default fiscal year is not set, get from nowdate
@ -222,35 +300,28 @@ class SalarySlip(TransactionBase):
if frappe.db.get_value('Timesheet', data.time_sheet, 'status') == 'Payrolled':
frappe.throw(_("Salary Slip of employee {0} already created for time sheet {1}").format(self.employee, data.time_sheet))
def calculate_earning_total(self):
self.gross_pay = flt(self.arrear_amount) + flt(self.leave_encashment_amount)
for d in self.get("earnings"):
if cint(d.depends_on_lwp) == 1 and not self.salary_slip_based_on_timesheet:
d.amount = rounded((flt(d.default_amount) * flt(self.payment_days)
/ cint(self.total_days_in_month)), self.precision("amount", "earnings"))
elif not self.payment_days and not self.salary_slip_based_on_timesheet:
d.amount = 0
elif not d.amount:
d.amount = d.default_amount
self.gross_pay += flt(d.amount)
def calculate_ded_total(self):
self.total_deduction = 0
for d in self.get('deductions'):
def sum_components(self, component_type, total_field):
for d in self.get(component_type):
if cint(d.depends_on_lwp) == 1 and not self.salary_slip_based_on_timesheet:
d.amount = rounded((flt(d.amount) * flt(self.payment_days)
/ cint(self.total_days_in_month)), self.precision("amount", "deductions"))
/ cint(self.total_days_in_month)), self.precision("amount", component_type))
elif not self.payment_days and not self.salary_slip_based_on_timesheet:
d.amount = 0
elif not d.amount:
d.amount = d.default_amount
self.total_deduction += flt(d.amount)
self.set(total_field, self.get(total_field) + flt(d.amount))
def calculate_net_pay(self):
self.calculate_component_amounts()
disable_rounded_total = cint(frappe.db.get_value("Global Defaults", None, "disable_rounded_total"))
self.calculate_earning_total()
self.calculate_ded_total()
self.gross_pay = flt(self.arrear_amount) + flt(self.leave_encashment_amount)
self.total_deduction = 0
self.sum_components('earnings', 'gross_pay')
self.sum_components('deductions', 'total_deduction')
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
self.rounded_total = rounded(self.net_pay,
self.precision("net_pay") if disable_rounded_total else 0)

View File

@ -4,19 +4,22 @@ from __future__ import unicode_literals
import unittest
import frappe
from frappe.utils import today, now_datetime, getdate, cstr
from erpnext.hr.doctype.employee.employee import make_salary_structure
import erpnext
from frappe.utils import today, now_datetime, getdate, cstr, add_years, nowdate
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.hr.doctype.leave_application.test_leave_application import make_allocation_record
class TestSalarySlip(unittest.TestCase):
def setUp(self):
self.make_salary_component(["Basic Salary", "Allowance", "HRA", "Professional Tax", "TDS"])
for dt in ["Leave Application", "Leave Allocation", "Salary Slip"]:
frappe.db.sql("delete from `tab%s`" % dt)
make_allocation_record(leave_type="_Test Leave Type LWP")
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", "_Test Holiday List")
self.make_holiday_list()
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
from erpnext.hr.doctype.leave_application.test_leave_application import _test_records as leave_applications
la = frappe.copy_doc(leave_applications[2])
@ -30,71 +33,78 @@ class TestSalarySlip(unittest.TestCase):
def test_salary_slip_with_holidays_included(self):
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1)
ss = frappe.copy_doc(test_records[0])
ss.insert()
self.make_employee("test_employee@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None)
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active")
ss = frappe.get_doc("Salary Slip",
self.make_employee_salary_slip("test_employee@salary.com"))
self.assertEquals(ss.total_days_in_month, 31)
self.assertEquals(ss.payment_days, 30)
self.assertEquals(ss.earnings[0].amount, 14516.13)
self.assertEquals(ss.earnings[1].amount, 500)
self.assertEquals(ss.deductions[0].amount, 100)
self.assertEquals(ss.deductions[1].amount, 48.39)
self.assertEquals(ss.gross_pay, 15016.13)
self.assertEquals(ss.net_pay, 14867.74)
self.assertEquals(ss.payment_days, 31)
self.assertEquals(ss.earnings[0].amount, 0)
self.assertEquals(ss.earnings[1].amount, 0)
self.assertEquals(ss.deductions[0].amount, 0)
self.assertEquals(ss.deductions[1].amount, 0)
self.assertEquals(ss.gross_pay, 0)
self.assertEquals(ss.net_pay, 0)
def test_salary_slip_with_holidays_excluded(self):
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0)
ss = frappe.copy_doc(test_records[0])
ss.insert()
self.assertEquals(ss.total_days_in_month, 29)
self.assertEquals(ss.payment_days, 28)
self.assertEquals(ss.earnings[0].amount, 14516.13)
self.assertEquals(ss.earnings[1].amount, 500)
self.assertEquals(ss.deductions[0].amount, 100)
self.assertEquals(ss.deductions[1].amount, 48.39)
self.assertEquals(ss.gross_pay, 15016.13)
self.assertEquals(ss.net_pay, 14867.74)
self.make_employee("test_employee@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None)
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active")
ss = frappe.get_doc("Salary Slip",
self.make_employee_salary_slip("test_employee@salary.com"))
self.assertEquals(ss.total_days_in_month, 27)
self.assertEquals(ss.payment_days, 27)
self.assertEquals(ss.earnings[0].amount, 0)
self.assertEquals(ss.earnings[0].default_amount, 5000)
self.assertEquals(ss.earnings[1].amount, 0)
self.assertEquals(ss.deductions[0].amount, 0)
self.assertEquals(ss.deductions[1].amount, 0)
self.assertEquals(ss.gross_pay, 0)
self.assertEquals(ss.net_pay, 0)
def test_payment_days(self):
# Holidays not included in working days
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 0)
# set joinng date in the same month
frappe.db.set_value("Employee", "_T-Employee-0001", "date_of_joining", "2013-01-11")
self.make_employee("test_employee@salary.com")
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", "2013-01-11")
ss = frappe.get_doc("Salary Slip",
self.make_employee_salary_slip("test_employee@salary.com"))
ss = frappe.copy_doc(test_records[0])
ss.insert()
self.assertEquals(ss.total_days_in_month, 29)
self.assertEquals(ss.payment_days, 19)
self.assertEquals(ss.total_days_in_month, 27)
self.assertEquals(ss.payment_days, 27)
# set relieving date in the same month
frappe.db.set_value("Employee", "_T-Employee-0001", "relieving_date", "2013-01-28")
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", "12-12-2016")
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "status", "Left")
self.assertEquals(ss.total_days_in_month, 27)
self.assertEquals(ss.payment_days, 27)
ss.save()
self.assertEquals(ss.total_days_in_month, 29)
self.assertEquals(ss.payment_days, 16)
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None)
frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "status", "Active")
# Holidays included in working days
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1)
frappe.db.set_value("HR Settings", None, "include_holidays_in_total_working_days", 1)
self.assertEquals(ss.total_days_in_month, 27)
self.assertEquals(ss.payment_days, 27)
ss.save()
self.assertEquals(ss.total_days_in_month, 31)
self.assertEquals(ss.payment_days, 17)
frappe.db.set_value("Employee", "_T-Employee-0001", "date_of_joining", "2001-01-11")
frappe.db.set_value("Employee", "_T-Employee-0001", "relieving_date", None)
#
# frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "date_of_joining", "2001-01-11")
# frappe.db.set_value("Employee", frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"), "relieving_date", None)
def test_employee_salary_slip_read_permission(self):
self.make_employee("test_employee@example.com")
self.make_employee("test_employee_2@example.com")
self.make_employee("test_employee@salary.com")
salary_slip_test_employee = frappe.get_doc("Salary Slip",
self.make_employee_salary_slip("test_employee@example.com"))
salary_slip_test_employee_2 = frappe.get_doc("Salary Slip",
self.make_employee_salary_slip("test_employee_2@example.com"))
frappe.set_user("test_employee@example.com")
self.make_employee_salary_slip("test_employee@salary.com"))
frappe.set_user("test_employee@salary.com")
self.assertTrue(salary_slip_test_employee.has_permission("read"))
def test_email_salary_slip(self):
@ -104,8 +114,10 @@ class TestSalarySlip(unittest.TestCase):
hr_settings.email_salary_slip_to_employee = 1
hr_settings.save()
self.make_employee("test_employee@example.com")
self.make_employee_salary_slip("test_employee@example.com")
self.make_employee("test_employee@salary.com")
ss = frappe.get_doc("Salary Slip",
self.make_employee_salary_slip("test_employee@salary.com"))
ss.submit()
email_queue = frappe.db.sql("""select name from `tabEmail Queue`""")
self.assertTrue(email_queue)
@ -123,32 +135,52 @@ class TestSalarySlip(unittest.TestCase):
if not frappe.db.get_value("Employee", {"user_id": user}):
frappe.get_doc({
"doctype": "Employee",
"naming_series": "_T-Employee-",
"naming_series": "EMP-",
"employee_name": user,
"company": erpnext.get_default_company(),
"user_id": user,
"company": "_Test Company",
"date_of_birth": "1990-05-08",
"date_of_joining": "2013-01-01",
"department": "_Test Department 1",
"department": frappe.get_all("Department", fields="name")[0].name,
"gender": "Female",
"company_email": user,
"status": "Active"
"status": "Active",
"employment_type": "Intern"
}).insert()
def make_holiday_list(self):
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": nowdate(),
"to_date": add_years(nowdate(), 1),
"weekly_off": "Sunday"
}).insert()
holiday_list.get_weekly_off_dates()
holiday_list.save()
def make_salary_component(self, salary_components):
for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component):
sal_comp = frappe.get_doc({
"doctype": "Salary Component",
"salary_component": salary_component
})
sal_comp.insert()
def make_employee_salary_slip(self, user):
employee = frappe.db.get_value("Employee", {"user_id": user})
salary_structure = frappe.db.get_value("Salary Structure", {"employee": employee})
if not salary_structure:
salary_structure = make_salary_structure(employee)
salary_structure.from_date = today()
salary_structure.insert()
salary_structure = salary_structure.name
salary_slip = frappe.db.get_value("Salary Slip", {"employee": employee})
salary_structure = make_salary_structure("Salary Structure Test for Salary Slip")
salary_slip = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
if not salary_slip:
salary_slip = make_salary_slip(salary_structure)
salary_slip = make_salary_slip(salary_structure, employee = employee)
salary_slip.employee_name = frappe.get_value("Employee", {"name":frappe.db.get_value("Employee", {"user_id": user})}, "employee_name")
salary_slip.month = "12"
salary_slip.fiscal_year = "_Test Fiscal Year 2016"
salary_slip.insert()
salary_slip.submit()
# salary_slip.submit()
salary_slip = salary_slip.name
return salary_slip
@ -160,6 +192,82 @@ class TestSalarySlip(unittest.TestCase):
activity_type.wage_rate = 25
activity_type.save()
test_dependencies = ["Leave Application", "Holiday List"]
test_records = frappe.get_test_records('Salary Slip')
def make_salary_structure(sal_struct):
if not frappe.db.exists('Salary Structure', sal_struct):
frappe.get_doc({
"doctype": "Salary Structure",
"name": sal_struct,
"company": erpnext.get_default_company(),
"from_date": nowdate(),
"employees": get_employee_details(),
"earnings": get_earnings_component(),
"deductions": get_deductions_component()
}).insert()
return sal_struct
def get_employee_details():
return [{"employee": frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"),
"base": 25000,
"variable": 5000
}
]
def get_earnings_component():
return [
{
"salary_component": 'Basic Salary',
"abbr":'BS',
"condition": 'base > 10000',
"formula": 'base*.2',
"idx": 1
},
{
"salary_component": 'Basic Salary',
"abbr":'BS',
"condition": 'base < 10000',
"formula": 'base*.1',
"idx": 2
},
{
"salary_component": 'HRA',
"abbr":'H',
"amount": 3000,
"idx": 3
},
{
"salary_component": 'Allowance',
"abbr":'A',
"condition": 'H < 10000',
"formula": 'BS*.5',
"idx": 4
},
]
def get_deductions_component():
return [
{
"salary_component": 'Professional Tax',
"abbr":'PT',
"condition": 'base > 10000',
"formula": 'base*.2',
"idx": 1
},
{
"salary_component": 'TDS',
"abbr":'T',
"formula": 'base*.5',
"idx": 2
},
{
"salary_component": 'TDS',
"abbr":'T',
"condition": 'employment_type=="Intern"',
"formula": 'base*.1',
"idx": 3
}
]
test_dependencies = ["Leave Application", "Holiday List"]

View File

@ -9,15 +9,7 @@ cur_frm.cscript.onload = function(doc, dt, dn){
e_tbl = doc.earnings || [];
d_tbl = doc.deductions || [];
if (e_tbl.length == 0 && d_tbl.length == 0)
return $c_obj(doc,'make_earn_ded_table','', function(r, rt) { refresh_many(['earnings', 'deductions']);});
}
cur_frm.cscript.refresh = function(doc, dt, dn){
if((!doc.__islocal) && (doc.is_active == 'Yes') && cint(doc.salary_slip_based_on_timesheet == 0)){
cur_frm.add_custom_button(__('Salary Slip'),
cur_frm.cscript['Make Salary Slip'], __("Make"));
cur_frm.page.set_inner_btn_group_as_primary(__("Make"));
}
return function(r, rt) { refresh_many(['earnings', 'deductions']);};
}
frappe.ui.form.on('Salary Structure', {
@ -25,29 +17,54 @@ frappe.ui.form.on('Salary Structure', {
frm.trigger("toggle_fields")
frm.fields_dict['earnings'].grid.set_column_disp("default_amount", false);
frm.fields_dict['deductions'].grid.set_column_disp("default_amount", false);
},
frm.add_custom_button(__("Preview Salary Slip"),
function() { frm.trigger('preview_salary_slip'); }, "icon-sitemap", "btn-default");
},
salary_slip_based_on_timesheet: function(frm) {
frm.trigger("toggle_fields")
},
preview_salary_slip: function(frm) {
var d = new frappe.ui.Dialog({
title: __("Preview Salary Slip"),
fields: [
{"fieldname":"employee", "fieldtype":"Select", "label":__("Employee"),
options: $.map(frm.doc.employees, function(d) { return d.employee }), reqd: 1, label:"Employee"},
{fieldname:"fetch", "label":__("Show Salary Slip"), "fieldtype":"Button"}
]
});
d.get_input("fetch").on("click", function() {
var values = d.get_values();
if(!values) return;
frm.doc.salary_slip_based_on_timesheet?print_format="Salary Slip based on Timesheet":print_format="Salary Slip Standard";
frappe.call({
method: "erpnext.hr.doctype.salary_structure.salary_structure.make_salary_slip",
args: {
source_name: frm.doc.name,
employee: values.employee,
as_print: 1,
print_format: print_format
},
callback: function(r) {
var new_window = window.open();
new_window.document.write(r.message);
// frappe.msgprint(r.message);
}
});
});
d.show();
},
toggle_fields: function(frm) {
frm.toggle_display(['salary_component', 'hour_rate'], frm.doc.salary_slip_based_on_timesheet);
frm.toggle_reqd(['salary_component', 'hour_rate'], frm.doc.salary_slip_based_on_timesheet);
}
})
cur_frm.cscript['Make Salary Slip'] = function() {
frappe.model.open_mapped_doc({
method: "erpnext.hr.doctype.salary_structure.salary_structure.make_salary_slip",
frm: cur_frm
});
}
cur_frm.cscript.employee = function(doc, dt, dn){
if (doc.employee)
return get_server_fields('get_employee_details','','',doc,dt,dn);
}
cur_frm.cscript.amount = function(doc, cdt, cdn){
calculate_totals(doc, cdt, cdn);
@ -79,10 +96,6 @@ cur_frm.cscript.validate = function(doc, cdt, cdn) {
if(doc.employee && doc.is_active == "Yes") frappe.model.clear_doc("Employee", doc.employee);
}
cur_frm.fields_dict.employee.get_query = function(doc,cdt,cdn) {
return{ query: "erpnext.controllers.queries.employee_query" }
}
frappe.ui.form.on('Salary Detail', {
amount: function(frm) {
@ -96,4 +109,12 @@ frappe.ui.form.on('Salary Detail', {
deductions_remove: function(frm) {
calculate_totals(frm.doc);
}
})
})
frappe.ui.form.on('Salary Structure Employee', {
onload: function(frm) {
frm.set_query("employee","employees", function(doc,cdt,cdn) {
return{ query: "erpnext.controllers.queries.employee_query" }
})
}
});

View File

@ -2,6 +2,7 @@
"allow_copy": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "Prompt",
"beta": 0,
"creation": "2013-03-07 18:50:29",
"custom": 0,
@ -34,140 +35,6 @@
"unique": 0,
"width": "50%"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "employee",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 0,
"label": "Employee",
"length": 0,
"no_copy": 0,
"oldfieldname": "employee",
"oldfieldtype": "Link",
"options": "Employee",
"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
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "employee_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Employee Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "employee_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "branch",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 0,
"label": "Branch",
"length": 0,
"no_copy": 0,
"oldfieldname": "branch",
"oldfieldtype": "Select",
"options": "Branch",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "designation",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 0,
"label": "Designation",
"length": 0,
"no_copy": 0,
"oldfieldname": "designation",
"oldfieldtype": "Select",
"options": "Designation",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "department",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 1,
"in_list_view": 0,
"label": "Department",
"length": 0,
"no_copy": 0,
"oldfieldname": "department",
"oldfieldtype": "Select",
"options": "Department",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
@ -350,6 +217,57 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "employee_break",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"description": "Select employees for current Salary Structure",
"fieldname": "employees",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Employees",
"length": 0,
"no_copy": 0,
"options": "Salary Structure Employee",
"permlevel": 0,
"precision": "",
"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
},
{
"allow_on_submit": 0,
"bold": 0,
@ -498,6 +416,7 @@
"oldfieldname": "earning_deduction",
"oldfieldtype": "Section Break",
"permlevel": 0,
"precision": "2",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
@ -671,7 +590,7 @@
"collapsible": 0,
"fieldname": "total_earning",
"fieldtype": "Currency",
"hidden": 0,
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
@ -698,7 +617,7 @@
"collapsible": 0,
"fieldname": "total_deduction",
"fieldtype": "Currency",
"hidden": 0,
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
@ -719,37 +638,13 @@
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "column_break3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0,
"width": "50%"
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "net_pay",
"fieldtype": "Currency",
"hidden": 0,
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
@ -780,7 +675,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2016-07-13 23:56:01.550518",
"modified": "2016-08-10 12:18:31.521436",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Structure",
@ -832,7 +727,7 @@
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "employee",
"title_field": "employee_name",
"timeline_field": "",
"title_field": "",
"track_seen": 0
}

View File

@ -4,7 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe.utils import cstr, flt, getdate
from frappe.utils import cstr, flt, getdate, cint
from frappe.model.naming import make_autoname
from frappe import _
from frappe.model.mapper import get_mapped_doc
@ -12,29 +12,12 @@ from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name
class SalaryStructure(Document):
def autoname(self):
self.name = make_autoname(self.employee + '/.SST' + '/.#####')
def validate(self):
self.check_overlap()
self.validate_amount()
self.validate_employee()
self.validate_joining_date()
set_employee_name(self)
def get_employee_details(self):
ret = {}
det = frappe.db.sql("""select employee_name, branch, designation, department
from `tabEmployee` where name = %s""", self.employee)
if det:
ret = {
'employee_name': cstr(det[0][0]),
'branch': cstr(det[0][1]),
'designation': cstr(det[0][2]),
'department': cstr(det[0][3]),
'backup_employee': cstr(self.employee)
}
return ret
for e in self.get('employees'):
set_employee_name(e)
def get_ss_values(self,employee):
basic_info = frappe.db.sql("""select bank_name, bank_ac_no
@ -43,71 +26,23 @@ class SalaryStructure(Document):
'bank_ac_no': basic_info and basic_info[0][1] or ''}
return ret
def make_table(self, doct_name, tab_fname, tab_name):
list1 = frappe.db.sql("select name from `tab%s` where docstatus != 2" % doct_name)
for li in list1:
child = self.append(tab_fname, {})
if(tab_fname == 'earnings'):
child.salary_component = cstr(li[0])
child.amount = 0
elif(tab_fname == 'deductions'):
child.salary_component = cstr(li[0])
child.amount = 0
def make_earn_ded_table(self):
self.make_table('Salary Component','earnings','Salary Detail')
self.make_table('Salary Component','deductions', 'Salary Detail')
def check_overlap(self):
existing = frappe.db.sql("""select name from `tabSalary Structure`
where employee = %(employee)s and
(
(%(from_date)s > from_date and %(from_date)s < to_date) or
(%(to_date)s > from_date and %(to_date)s < to_date) or
(%(from_date)s <= from_date and %(to_date)s >= to_date))
and name!=%(name)s
and docstatus < 2""",
{
"employee": self.employee,
"from_date": self.from_date,
"to_date": self.to_date,
"name": self.name or "No Name"
}, as_dict=True)
if existing:
frappe.throw(_("Salary structure {0} already exist, more than one salary structure for same period is not allowed").format(existing[0].name))
def validate_amount(self):
if flt(self.net_pay) < 0 and self.salary_slip_based_on_timesheet:
frappe.throw(_("Net pay cannot be negative"))
def validate_employee(self):
old_employee = frappe.db.get_value("Salary Structure", self.name, "employee")
if old_employee and self.employee != old_employee:
frappe.throw(_("Employee can not be changed"))
def validate_joining_date(self):
joining_date = getdate(frappe.db.get_value("Employee", self.employee, "date_of_joining"))
if getdate(self.from_date) < joining_date:
frappe.throw(_("From Date in Salary Structure cannot be lesser than Employee Joining Date."))
for e in self.get('employees'):
joining_date = getdate(frappe.db.get_value("Employee", e.employee, "date_of_joining"))
if getdate(self.from_date) < joining_date:
frappe.throw(_("From Date in Salary Structure cannot be lesser than Employee Joining Date."))
@frappe.whitelist()
def make_salary_slip(source_name, target_doc=None):
def make_salary_slip(source_name, target_doc = None, employee = None, as_print = False, print_format = None):
def postprocess(source, target):
# copy earnings and deductions table
for key in ('earnings', 'deductions'):
for d in source.get(key):
target.append(key, {
'amount': d.amount,
'default_amount': d.amount,
'depends_on_lwp' : d.depends_on_lwp,
'salary_component' : d.salary_component
})
target.run_method("pull_emp_details")
target.run_method("get_leave_details")
target.run_method("calculate_net_pay")
if employee:
target.employee = employee
target.run_method('process_salary_structure')
doc = get_mapped_doc("Salary Structure", source_name, {
"Salary Structure": {
@ -119,4 +54,8 @@ def make_salary_slip(source_name, target_doc=None):
}
}, target_doc, postprocess, ignore_child_tables=True)
return doc
if cint(as_print):
doc.name = 'Preview for {0}'.format(employee)
return frappe.get_print(doc.doctype, doc.name, doc = doc, print_format = print_format)
else:
return doc

View File

@ -1,24 +0,0 @@
[
{
"doctype": "Salary Structure",
"name": "_Test Salary Structure 1",
"employee": "_T-Employee-0001",
"from_date": "2014-02-01",
"earnings": [
{
"salary_component": "_Test Basic Salary"
},
{
"salary_component": "_Test Allowance"
}
],
"deductions": [
{
"salary_component": "_Test Professional Tax"
},
{
"salary_component": "_Test TDS"
}
]
}
]

View File

@ -4,8 +4,183 @@ from __future__ import unicode_literals
import frappe
import unittest
test_records = frappe.get_test_records('Salary Structure')
import erpnext
from frappe.utils import nowdate, add_days, add_years
from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip
# test_records = frappe.get_test_records('Salary Structure')
class TestSalaryStructure(unittest.TestCase):
pass
def test_setup(self):
if not frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2016"):
fy = frappe.get_doc({
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2016",
"year_end_date": "2016-12-31",
"year_start_date": "2016-01-01"
})
fy.insert()
self.make_holiday_list()
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Structure Test Holiday List")
self.make_salary_component(["Basic Salary", "Allowance", "HRA", "Professional Tax", "TDS"])
employee1 = self.make_employee("test_employee@salary.com")
employee2 = self.make_employee("test_employee_2@salary.com")
def make_holiday_list(self):
if not frappe.db.get_value("Holiday List", "Salary Structure Test Holiday List"):
holiday_list = frappe.get_doc({
"doctype": "Holiday List",
"holiday_list_name": "Salary Structure Test Holiday List",
"from_date": nowdate(),
"to_date": add_years(nowdate(), 1),
"weekly_off": "Sunday"
}).insert()
holiday_list.get_weekly_off_dates()
holiday_list.save()
def make_employee(self, user):
if not frappe.db.get_value("User", user):
frappe.get_doc({
"doctype": "User",
"email": user,
"first_name": user,
"new_password": "password",
"user_roles": [{"doctype": "UserRole", "role": "Employee"}]
}).insert()
if not frappe.db.get_value("Employee", {"user_id": user}):
emp = frappe.get_doc({
"doctype": "Employee",
"naming_series": "EMP-",
"employee_name": user,
"company": erpnext.get_default_company(),
"user_id": user,
"date_of_birth": "1990-05-08",
"date_of_joining": "2013-01-01",
"relieving_date": "",
"department": frappe.get_all("Department", fields="name")[0].name,
"gender": "Female",
"company_email": user,
"status": "Active",
"employment_type": "Intern"
}).insert()
return emp.name
else:
return frappe.get_value("Employee", {"employee_name":user}, "name")
def make_salary_component(self, salary_components):
for salary_component in salary_components:
if not frappe.db.exists('Salary Component', salary_component):
sal_comp = frappe.get_doc({
"doctype": "Salary Component",
"salary_component": salary_component
})
sal_comp.insert()
def test_amount_totals(self):
sal_slip = frappe.get_value("Salary Slip", {"employee_name":"test_employee@salary.com"})
if not sal_slip:
sal_slip = make_salary_slip_from_salary_structure(employee=frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}))
self.assertEquals(sal_slip.get("salary_structure"), 'Salary Structure Sample')
self.assertEquals(sal_slip.get("earnings")[0].amount, 0)
self.assertEquals(sal_slip.get("deductions")[0].amount, 0)
self.assertEquals(sal_slip.get("deductions")[1].amount, 0)
self.assertEquals(sal_slip.get("total_deduction"), 0)
self.assertEquals(sal_slip.get("net_pay"), 0)
def make_salary_slip_from_salary_structure(employee):
sal_struct = make_salary_structure('Salary Structure Sample')
sal_slip = make_salary_slip(sal_struct, employee = employee)
sal_slip.employee_name = frappe.get_value("Employee", {"name":employee}, "employee_name")
sal_slip.month = "11"
sal_slip.fiscal_year = "_Test Fiscal Year 2016"
sal_slip.insert()
sal_slip.submit()
return sal_slip
def make_salary_structure(sal_struct):
if not frappe.db.exists('Salary Structure', sal_struct):
frappe.get_doc({
"doctype": "Salary Structure",
"name": sal_struct,
"company": erpnext.get_default_company(),
"from_date": nowdate(),
"employees": get_employee_details(),
"earnings": get_earnings_component(),
"deductions": get_deductions_component()
}).insert()
return sal_struct
def get_employee_details():
return [{"employee": frappe.get_value("Employee", {"employee_name":"test_employee@salary.com"}, "name"),
"base": 25000,
"variable": 5000,
"idx": 1
},
{"employee": frappe.get_value("Employee", {"employee_name":"test_employee_2@salary.com"}, "name"),
"base": 2100,
"variable": 100,
"idx": 2
}
]
def get_earnings_component():
return [
{
"salary_component": 'Basic Salary',
"abbr":'BS',
"condition": 'base > 10000',
"formula": 'base*.2',
"idx": 1
},
{
"salary_component": 'Basic Salary',
"abbr":'BS',
"condition": 'base < 10000',
"formula": 'base*.1',
"idx": 2
},
{
"salary_component": 'HRA',
"abbr":'H',
"amount": 3000,
"idx": 3
},
{
"salary_component": 'Allowance',
"abbr":'A',
"condition": 'H < 10000',
"formula": 'BS*.5',
"idx": 4
},
]
def get_deductions_component():
return [
{
"salary_component": 'Professional Tax',
"abbr":'PT',
"condition": 'base > 10000',
"formula": 'base*.2',
"idx": 1
},
{
"salary_component": 'TDS',
"abbr":'T',
"formula": 'base*.5',
"idx": 2
},
{
"salary_component": 'TDS',
"abbr":'T',
"condition": 'employment_type=="Intern"',
"formula": 'base*.1',
"idx": 3
}
]

View File

@ -0,0 +1,139 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "employee",
"beta": 0,
"creation": "2016-07-26 11:53:43.621605",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "employee",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Employee",
"length": 0,
"no_copy": 0,
"options": "Employee",
"permlevel": 0,
"precision": "",
"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
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "employee_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 0,
"label": "Employee Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "base",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Base",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"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
},
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "variable",
"fieldtype": "Currency",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Variable",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"in_dialog": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-08-11 12:18:14.526977",
"modified_by": "Administrator",
"module": "HR",
"name": "Salary Structure Employee",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
}

View File

@ -0,0 +1,10 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
class SalaryStructureEmployee(Document):
pass

View File

@ -310,4 +310,5 @@ erpnext.patches.v7_0.set_material_request_type_in_item
erpnext.patches.v7_0.rename_examination_to_assessment
erpnext.patches.v7_0.set_portal_settings
erpnext.patches.v7_0.repost_future_gle_for_purchase_invoice
erpnext.patches.v7_0.fix_duplicate_icons
erpnext.patches.v7_0.fix_duplicate_icons
erpnext.patches.v7_0.move_employee_parent_to_child_in_salary_structure

View File

@ -0,0 +1,10 @@
import frappe
def execute():
frappe.reload_doc('hr', 'doctype', 'salary_structure')
for ss in frappe.db.sql(""" select employee, name from `tabSalary Structure`""", as_dict=True):
ss_doc = frappe.get_doc('Salary Structure', ss.name)
se = ss_doc.append('employees',{})
se.employee = ss.employee
se.base = 0
ss_doc.save()

View File

@ -27,7 +27,7 @@ class TestTimesheet(unittest.TestCase):
self.assertEquals(salary_slip.total_working_hours, 2)
self.assertEquals(salary_slip.hour_rate, 50)
self.assertEquals(salary_slip.net_pay, 150)
self.assertEquals(salary_slip.net_pay, 50)
self.assertEquals(salary_slip.timesheets[0].time_sheet, timesheet.name)
self.assertEquals(salary_slip.timesheets[0].working_hours, 2)
@ -54,35 +54,42 @@ class TestTimesheet(unittest.TestCase):
timesheet = frappe.get_doc('Timesheet', timesheet.name)
self.assertEquals(sales_invoice.total_billing_amount, 100)
self.assertEquals(timesheet.status, 'Billed')
def make_salary_structure(employee):
name = frappe.db.get_value('Salary Structure', {'employee': employee, 'salary_slip_based_on_timesheet': 1}, 'name')
name = frappe.db.get_value('Salary Structure Employee', {'employee': employee}, 'parent')
if name:
salary_structure = frappe.get_doc('Salary Structure', name)
else:
salary_structure = frappe.new_doc("Salary Structure")
salary_structure.name = "Timesheet Salary Structure Test"
salary_structure.salary_slip_based_on_timesheet = 1
salary_structure.from_date = nowdate()
salary_structure.salary_component = "Basic"
salary_structure.hour_rate = 50.0
salary_structure.company= "_Test Company"
salary_structure.salary_slip_based_on_timesheet = 1
salary_structure.employee = employee
salary_structure.from_date = nowdate()
salary_structure.salary_component = "Basic"
salary_structure.hour_rate = 50.0
salary_structure.company= "_Test Company"
salary_structure.set('employees', [])
salary_structure.set('earnings', [])
salary_structure.set('deductions', [])
salary_structure.set('earnings', [])
salary_structure.set('deductions', [])
es = salary_structure.append('employees', {
"employee": employee,
"base": 1200
})
es = salary_structure.append('earnings', {
"salary_component": "_Test Allowance",
"amount": 100
})
es = salary_structure.append('earnings', {
"salary_component": "_Test Allowance",
"amount": 100
})
ds = salary_structure.append('deductions', {
"salary_component": "_Test Professional Tax",
"amount": 50
})
ds = salary_structure.append('deductions', {
"salary_component": "_Test Professional Tax",
"amount": 50
})
salary_structure.save(ignore_permissions=True)
salary_structure.save(ignore_permissions=True)
return salary_structure