diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
new file mode 100644
index 00000000000..3ef9762a839
--- /dev/null
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -0,0 +1,293 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+cur_frm.add_fetch('employee', 'company', 'company');
+cur_frm.add_fetch('time_sheet', 'total_hours', 'working_hours');
+
+frappe.ui.form.on("Salary Slip", {
+ setup: function(frm) {
+ $.each(["earnings", "deductions"], function(i, table_fieldname) {
+ frm.get_field(table_fieldname).grid.editable_fields = [
+ {fieldname: 'salary_component', columns: 6},
+ {fieldname: 'amount', columns: 4}
+ ];
+ });
+
+ frm.fields_dict["timesheets"].grid.get_field("time_sheet").get_query = function() {
+ return {
+ filters: {
+ employee: frm.doc.employee
+ }
+ };
+ };
+
+ frm.set_query("salary_component", "earnings", function() {
+ return {
+ filters: {
+ type: "earning"
+ }
+ };
+ });
+
+ frm.set_query("salary_component", "deductions", function() {
+ return {
+ filters: {
+ type: "deduction"
+ }
+ };
+ });
+
+ frm.set_query("employee", function() {
+ return {
+ query: "erpnext.controllers.queries.employee_query",
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ });
+ },
+
+ start_date: function(frm) {
+ if (frm.doc.start_date) {
+ frm.trigger("set_end_date");
+ }
+ },
+
+ end_date: function(frm) {
+ frm.events.get_emp_and_working_day_details(frm);
+ },
+
+ set_end_date: function(frm) {
+ frappe.call({
+ method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
+ args: {
+ frequency: frm.doc.payroll_frequency,
+ start_date: frm.doc.start_date
+ },
+ callback: function (r) {
+ if (r.message) {
+ frm.set_value('end_date', r.message.end_date);
+ }
+ }
+ });
+ },
+
+ company: function(frm) {
+ var company = locals[':Company'][frm.doc.company];
+ if (!frm.doc.letter_head && company.default_letter_head) {
+ frm.set_value('letter_head', company.default_letter_head);
+ }
+ },
+
+ currency: function(frm) {
+ frm.trigger("set_dynamic_labels");
+ },
+
+ set_dynamic_labels: function(frm) {
+ var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency");
+ if (frm.doc.employee && frm.doc.currency) {
+ frappe.run_serially([
+ () => frm.events.set_exchange_rate(frm, company_currency),
+ () => frm.events.change_form_labels(frm, company_currency),
+ () => frm.events.change_grid_labels(frm),
+ () => frm.refresh_fields()
+ ]);
+ }
+ },
+
+ set_exchange_rate: function(frm, company_currency) {
+ if (frm.doc.docstatus === 0) {
+ if (frm.doc.currency) {
+ var from_currency = frm.doc.currency;
+ if (from_currency != company_currency) {
+ frm.events.hide_loan_section(frm);
+ frappe.call({
+ method: "erpnext.setup.utils.get_exchange_rate",
+ args: {
+ from_currency: from_currency,
+ to_currency: company_currency,
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.set_value("exchange_rate", flt(r.message));
+ frm.set_df_property('exchange_rate', 'hidden', 0);
+ frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
+ + " = [?] " + company_currency);
+ }
+ }
+ });
+ } else {
+ frm.set_value("exchange_rate", 1.0);
+ frm.set_df_property('exchange_rate', 'hidden', 1);
+ frm.set_df_property("exchange_rate", "description", "" );
+ }
+ }
+ }
+ },
+
+ exchange_rate: function(frm) {
+ set_totals(frm);
+ },
+
+ hide_loan_section: function(frm) {
+ frm.set_df_property('section_break_43', 'hidden', 1);
+ },
+
+ change_form_labels: function(frm, company_currency) {
+ frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
+ "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date", "gross_base_year_to_date"],
+ company_currency);
+
+ frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date", "gross_year_to_date"],
+ frm.doc.currency);
+
+ // toggle fields
+ 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_year_to_date", "base_month_to_date", "base_gross_year_to_date"],
+ frm.doc.currency != company_currency);
+ },
+
+ change_grid_labels: function(frm) {
+ let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit",
+ "tax_on_additional_salary"];
+
+ frm.set_currency_labels(fields, frm.doc.currency, "earnings");
+ frm.set_currency_labels(fields, frm.doc.currency, "deductions");
+ },
+
+ refresh: function(frm) {
+ frm.trigger("toggle_fields");
+
+ var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
+ frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
+ frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false);
+ frm.trigger("set_dynamic_labels");
+ },
+
+ salary_slip_based_on_timesheet: function(frm) {
+ frm.trigger("toggle_fields");
+ frm.events.get_emp_and_working_day_details(frm);
+ },
+
+ payroll_frequency: function(frm) {
+ frm.trigger("toggle_fields");
+ frm.set_value('end_date', '');
+ },
+
+ employee: function(frm) {
+ frm.events.get_emp_and_working_day_details(frm);
+ },
+
+ leave_without_pay: function(frm) {
+ if (frm.doc.employee && frm.doc.start_date && frm.doc.end_date) {
+ return frappe.call({
+ method: 'process_salary_based_on_working_days',
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh();
+ }
+ });
+ }
+ },
+
+ toggle_fields: function(frm) {
+ frm.toggle_display(['hourly_wages', 'timesheets'], cint(frm.doc.salary_slip_based_on_timesheet)===1);
+
+ frm.toggle_display(['payment_days', 'total_working_days', 'leave_without_pay'],
+ frm.doc.payroll_frequency != "");
+ },
+
+ get_emp_and_working_day_details: function(frm) {
+ if (frm.doc.employee) {
+ return frappe.call({
+ method: 'get_emp_and_working_day_details',
+ doc: frm.doc,
+ callback: function(r) {
+ if (r.message[1] !== "Leave" && r.message[0]) {
+ frm.fields_dict.absent_days.set_description(__("Unmarked Days is treated as {0}. You can can change this in {1}", [r.message, frappe.utils.get_form_link("Payroll Settings", "Payroll Settings", true)]));
+ }
+ frm.refresh();
+ }
+ });
+ }
+ }
+});
+
+frappe.ui.form.on('Salary Slip Timesheet', {
+ time_sheet: function(frm) {
+ set_totals(frm);
+ },
+ timesheets_remove: function(frm) {
+ set_totals(frm);
+ }
+});
+
+var set_totals = function(frm) {
+ if (frm.doc.docstatus === 0 && frm.doc.doctype === "Salary Slip") {
+ if (frm.doc.earnings || frm.doc.deductions) {
+ frappe.call({
+ method: "set_totals",
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh_fields();
+ }
+ });
+ }
+ }
+};
+
+frappe.ui.form.on('Salary Detail', {
+ amount: function(frm) {
+ set_totals(frm);
+ },
+
+ earnings_remove: function(frm) {
+ set_totals(frm);
+ },
+
+ deductions_remove: function(frm) {
+ set_totals(frm);
+ },
+
+ salary_component: function(frm, cdt, cdn) {
+ var child = locals[cdt][cdn];
+ if (child.salary_component) {
+ frappe.call({
+ method: "frappe.client.get",
+ args: {
+ doctype: "Salary Component",
+ name: child.salary_component
+ },
+ callback: function(data) {
+ if (data.message) {
+ var result = data.message;
+ frappe.model.set_value(cdt, cdn, 'condition', result.condition);
+ frappe.model.set_value(cdt, cdn, 'amount_based_on_formula', result.amount_based_on_formula);
+ if (result.amount_based_on_formula === 1) {
+ frappe.model.set_value(cdt, cdn, 'formula', result.formula);
+ } else {
+ frappe.model.set_value(cdt, cdn, 'amount', result.amount);
+ }
+ frappe.model.set_value(cdt, cdn, 'statistical_component', result.statistical_component);
+ frappe.model.set_value(cdt, cdn, 'depends_on_payment_days', result.depends_on_payment_days);
+ frappe.model.set_value(cdt, cdn, 'do_not_include_in_total', result.do_not_include_in_total);
+ frappe.model.set_value(cdt, cdn, 'variable_based_on_taxable_salary', result.variable_based_on_taxable_salary);
+ frappe.model.set_value(cdt, cdn, 'is_tax_applicable', result.is_tax_applicable);
+ frappe.model.set_value(cdt, cdn, 'is_flexible_benefit', result.is_flexible_benefit);
+ refresh_field("earnings");
+ refresh_field("deductions");
+ }
+ }
+ });
+ }
+ },
+
+ amount_based_on_formula: function(frm, cdt, cdn) {
+ var child = locals[cdt][cdn];
+ if (child.amount_based_on_formula === 1) {
+ frappe.model.set_value(cdt, cdn, 'amount', null);
+ } else {
+ frappe.model.set_value(cdt, cdn, 'formula', null);
+ }
+ }
+});
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
new file mode 100644
index 00000000000..fbbf86c4a98
--- /dev/null
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -0,0 +1,691 @@
+{
+ "actions": [],
+ "allow_import": 1,
+ "creation": "2013-01-10 16:34:15",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "posting_date",
+ "employee",
+ "employee_name",
+ "department",
+ "designation",
+ "branch",
+ "payroll_cost_center",
+ "column_break1",
+ "status",
+ "journal_entry",
+ "payroll_entry",
+ "company",
+ "currency",
+ "exchange_rate",
+ "letter_head",
+ "section_break_10",
+ "start_date",
+ "end_date",
+ "salary_structure",
+ "column_break_18",
+ "salary_slip_based_on_timesheet",
+ "payroll_frequency",
+ "section_break_20",
+ "total_working_days",
+ "unmarked_days",
+ "leave_without_pay",
+ "column_break_24",
+ "absent_days",
+ "payment_days",
+ "hourly_wages",
+ "timesheets",
+ "column_break_20",
+ "total_working_hours",
+ "hour_rate",
+ "base_hour_rate",
+ "section_break_26",
+ "bank_name",
+ "bank_account_no",
+ "mode_of_payment",
+ "section_break_32",
+ "deduct_tax_for_unclaimed_employee_benefits",
+ "deduct_tax_for_unsubmitted_tax_exemption_proof",
+ "earning_deduction",
+ "earning",
+ "earnings",
+ "deduction",
+ "deductions",
+ "totals",
+ "gross_pay",
+ "base_gross_pay",
+ "gross_year_to_date",
+ "base_gross_year_to_date",
+ "column_break_25",
+ "total_deduction",
+ "base_total_deduction",
+ "loan_repayment",
+ "loans",
+ "section_break_43",
+ "total_principal_amount",
+ "total_interest_amount",
+ "column_break_45",
+ "total_loan_repayment",
+ "net_pay_info",
+ "net_pay",
+ "base_net_pay",
+ "year_to_date",
+ "base_year_to_date",
+ "column_break_53",
+ "rounded_total",
+ "base_rounded_total",
+ "month_to_date",
+ "base_month_to_date",
+ "section_break_55",
+ "total_in_words",
+ "column_break_69",
+ "base_total_in_words",
+ "leave_details_section",
+ "leave_details",
+ "section_break_75",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Employee",
+ "oldfieldname": "employee",
+ "oldfieldtype": "Link",
+ "options": "Employee",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Read Only",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Employee Name",
+ "oldfieldname": "employee_name",
+ "oldfieldtype": "Data",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Department",
+ "oldfieldname": "department",
+ "oldfieldtype": "Link",
+ "options": "Department",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.designation",
+ "fetch_from": "employee.designation",
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "oldfieldname": "designation",
+ "oldfieldtype": "Link",
+ "options": "Designation",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "employee.branch",
+ "fieldname": "branch",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Branch",
+ "oldfieldname": "branch",
+ "oldfieldtype": "Link",
+ "options": "Branch",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break1",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "width": "50%"
+ },
+ {
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "options": "Draft\nSubmitted\nCancelled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "journal_entry",
+ "fieldtype": "Link",
+ "label": "Journal Entry",
+ "options": "Journal Entry",
+ "read_only": 1
+ },
+ {
+ "fieldname": "payroll_entry",
+ "fieldtype": "Link",
+ "label": "Payroll Entry",
+ "options": "Payroll Entry",
+ "read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Company",
+ "options": "Company",
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "letter_head",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Letter Head",
+ "options": "Letter Head",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "salary_slip_based_on_timesheet",
+ "fieldtype": "Check",
+ "label": "Salary Slip Based on Timesheet",
+ "read_only": 1
+ },
+ {
+ "fieldname": "start_date",
+ "fieldtype": "Date",
+ "label": "Start Date"
+ },
+ {
+ "fieldname": "end_date",
+ "fieldtype": "Date",
+ "label": "End Date"
+ },
+ {
+ "fieldname": "salary_structure",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Salary Structure",
+ "options": "Salary Structure",
+ "read_only": 1,
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "depends_on": "eval:(!doc.salary_slip_based_on_timesheet)",
+ "fieldname": "payroll_frequency",
+ "fieldtype": "Select",
+ "label": "Payroll Frequency",
+ "options": "\nMonthly\nFortnightly\nBimonthly\nWeekly\nDaily"
+ },
+ {
+ "fieldname": "total_working_days",
+ "fieldtype": "Float",
+ "label": "Working Days",
+ "oldfieldname": "total_days_in_month",
+ "oldfieldtype": "Int",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "leave_without_pay",
+ "fieldtype": "Float",
+ "label": "Leave Without Pay",
+ "oldfieldname": "leave_without_pay",
+ "oldfieldtype": "Currency"
+ },
+ {
+ "fieldname": "payment_days",
+ "fieldtype": "Float",
+ "label": "Payment Days",
+ "oldfieldname": "payment_days",
+ "oldfieldtype": "Float",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "hourly_wages",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "timesheets",
+ "fieldtype": "Table",
+ "label": "Salary Slip Timesheet",
+ "options": "Salary Slip Timesheet"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "total_working_hours",
+ "fieldtype": "Float",
+ "label": "Total Working Hours",
+ "print_hide_if_no_value": 1
+ },
+ {
+ "fieldname": "hour_rate",
+ "fieldtype": "Currency",
+ "label": "Hour Rate",
+ "options": "currency",
+ "print_hide_if_no_value": 1
+ },
+ {
+ "fieldname": "section_break_26",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "bank_name",
+ "fieldtype": "Data",
+ "label": "Bank Name",
+ "oldfieldname": "bank_name",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "fieldname": "bank_account_no",
+ "fieldtype": "Data",
+ "label": "Bank Account No.",
+ "oldfieldname": "bank_account_no",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_32",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "deduct_tax_for_unclaimed_employee_benefits",
+ "fieldtype": "Check",
+ "label": "Deduct Tax For Unclaimed Employee Benefits"
+ },
+ {
+ "default": "0",
+ "fieldname": "deduct_tax_for_unsubmitted_tax_exemption_proof",
+ "fieldtype": "Check",
+ "label": "Deduct Tax For Unsubmitted Tax Exemption Proof"
+ },
+ {
+ "fieldname": "earning_deduction",
+ "fieldtype": "Section Break",
+ "label": "Earning & Deduction",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "fieldname": "earning",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "width": "50%"
+ },
+ {
+ "fieldname": "earnings",
+ "fieldtype": "Table",
+ "label": "Earnings",
+ "oldfieldname": "earning_details",
+ "oldfieldtype": "Table",
+ "options": "Salary Detail"
+ },
+ {
+ "fieldname": "deduction",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break",
+ "width": "50%"
+ },
+ {
+ "fieldname": "deductions",
+ "fieldtype": "Table",
+ "label": "Deductions",
+ "oldfieldname": "deduction_details",
+ "oldfieldtype": "Table",
+ "options": "Salary Detail"
+ },
+ {
+ "fieldname": "totals",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "fieldname": "gross_pay",
+ "fieldtype": "Currency",
+ "label": "Gross Pay",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_25",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "total_loan_repayment",
+ "fieldname": "loan_repayment",
+ "fieldtype": "Section Break",
+ "label": "Loan repayment"
+ },
+ {
+ "fieldname": "loans",
+ "fieldtype": "Table",
+ "label": "Employee Loan",
+ "options": "Salary Slip Loan",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.docstatus != 0",
+ "fieldname": "section_break_43",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "total_principal_amount",
+ "fieldtype": "Currency",
+ "label": "Total Principal Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "total_interest_amount",
+ "fieldtype": "Currency",
+ "label": "Total Interest Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_45",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "total_loan_repayment",
+ "fieldtype": "Currency",
+ "label": "Total Loan Repayment",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "net_pay_info",
+ "fieldtype": "Section Break",
+ "label": "net pay info"
+ },
+ {
+ "fieldname": "net_pay",
+ "fieldtype": "Currency",
+ "label": "Net Pay",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_53",
+ "fieldtype": "Column Break"
+ },
+ {
+ "bold": 1,
+ "fieldname": "rounded_total",
+ "fieldtype": "Currency",
+ "label": "Rounded Total",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_55",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "label": "Amended From",
+ "no_copy": 1,
+ "oldfieldname": "amended_from",
+ "oldfieldtype": "Data",
+ "options": "Salary Slip",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "employee.payroll_cost_center",
+ "fetch_if_empty": 1,
+ "fieldname": "payroll_cost_center",
+ "fieldtype": "Link",
+ "label": "Payroll Cost Center",
+ "options": "Cost Center",
+ "read_only": 1
+ },
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Select",
+ "label": "Mode Of Payment",
+ "read_only": 1
+ },
+ {
+ "fieldname": "absent_days",
+ "fieldtype": "Float",
+ "label": "Absent Days",
+ "read_only": 1
+ },
+ {
+ "fieldname": "unmarked_days",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Unmarked days"
+ },
+ {
+ "fieldname": "section_break_20",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_24",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)",
+ "fetch_from": "salary_structure.currency",
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "total_deduction",
+ "fieldtype": "Currency",
+ "label": "Total Deduction",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_in_words",
+ "fieldtype": "Data",
+ "label": "Total in words",
+ "length": 240,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_75",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "base_hour_rate",
+ "fieldtype": "Currency",
+ "label": "Hour Rate (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide_if_no_value": 1
+ },
+ {
+ "fieldname": "base_gross_pay",
+ "fieldtype": "Currency",
+ "label": "Gross Pay (Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "default": "1.0",
+ "fieldname": "exchange_rate",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Exchange Rate",
+ "print_hide": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "base_total_deduction",
+ "fieldtype": "Currency",
+ "label": "Total Deduction (Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_net_pay",
+ "fieldtype": "Currency",
+ "label": "Net Pay (Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "bold": 1,
+ "fieldname": "base_rounded_total",
+ "fieldtype": "Currency",
+ "label": "Rounded Total (Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_total_in_words",
+ "fieldtype": "Data",
+ "label": "Total in words (Company Currency)",
+ "length": 240,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_69",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "Total salary booked for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.",
+ "fieldname": "year_to_date",
+ "fieldtype": "Currency",
+ "label": "Year To Date",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "description": "Total salary booked for this employee from the beginning of the month up to the current salary slip's end date.",
+ "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
+ },
+ {
+ "fieldname": "leave_details_section",
+ "fieldtype": "Section Break",
+ "label": "Leave Details"
+ },
+ {
+ "fieldname": "leave_details",
+ "fieldtype": "Table",
+ "label": "Leave Details",
+ "options": "Salary Slip Leave",
+ "read_only": 1
+ },
+ {
+ "fieldname": "gross_year_to_date",
+ "fieldtype": "Currency",
+ "label": "Gross Year To Date",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_gross_year_to_date",
+ "fieldtype": "Currency",
+ "label": "Gross Year To Date(Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ }
+ ],
+ "icon": "fa fa-file-text",
+ "idx": 9,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-09-01 10:22:52.374549",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Salary Slip",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "read": 1,
+ "role": "Employee"
+ }
+ ],
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "timeline_field": "employee",
+ "title_field": "employee_name"
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
new file mode 100644
index 00000000000..e1e7745f2d6
--- /dev/null
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -0,0 +1,1337 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe, erpnext
+import datetime, math
+
+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 import msgprint, _
+from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
+from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from erpnext.hr.utils import get_holiday_dates_for_employee
+from erpnext.utilities.transaction_base import TransactionBase
+from frappe.utils.background_jobs import enqueue
+from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
+from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
+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.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.hr.utils import validate_active_employee
+from six import iteritems
+
+class SalarySlip(TransactionBase):
+ def __init__(self, *args, **kwargs):
+ super(SalarySlip, self).__init__(*args, **kwargs)
+ self.series = 'Sal Slip/{0}/.#####'.format(self.employee)
+ self.whitelisted_globals = {
+ "int": int,
+ "float": float,
+ "long": int,
+ "round": round,
+ "date": datetime.date,
+ "getdate": getdate
+ }
+
+ def autoname(self):
+ self.name = make_autoname(self.series)
+
+ def validate(self):
+ self.status = self.get_status()
+ validate_active_employee(self.employee)
+ self.validate_dates()
+ self.check_existing()
+ if not self.salary_slip_based_on_timesheet:
+ self.get_date_details()
+
+ if not (len(self.get("earnings")) or len(self.get("deductions"))):
+ # get details from salary structure
+ self.get_emp_and_working_day_details()
+ else:
+ self.get_working_days_details(lwp = self.leave_without_pay)
+
+ self.calculate_net_pay()
+ self.compute_year_to_date()
+ self.compute_month_to_date()
+ self.compute_component_wise_year_to_date()
+ self.add_leave_balances()
+
+ 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")
+ 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 set_net_total_in_words(self):
+ doc_currency = self.currency
+ company_currency = erpnext.get_company_currency(self.company)
+ total = self.net_pay if self.is_rounding_total_disabled() else self.rounded_total
+ base_total = self.base_net_pay if self.is_rounding_total_disabled() else self.base_rounded_total
+ self.total_in_words = money_in_words(total, doc_currency)
+ self.base_total_in_words = money_in_words(base_total, company_currency)
+
+ def on_submit(self):
+ if self.net_pay < 0:
+ frappe.throw(_("Net Pay cannot be less than 0"))
+ else:
+ self.set_status()
+ self.update_status(self.name)
+ self.make_loan_repayment_entry()
+ if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
+ self.email_salary_slip()
+
+ self.update_payment_status_for_gratuity()
+
+ def update_payment_status_for_gratuity(self):
+ add_salary = frappe.db.get_all("Additional Salary",
+ filters = {
+ "payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
+ "employee": self.employee,
+ "ref_doctype": "Gratuity",
+ "docstatus": 1,
+ }, fields = ["ref_docname", "name"], limit=1)
+
+ if len(add_salary):
+ status = "Paid" if self.docstatus == 1 else "Unpaid"
+ if add_salary[0].name in [data.additional_salary for data in self.earnings]:
+ frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
+
+ def on_cancel(self):
+ self.set_status()
+ self.update_status()
+ self.update_payment_status_for_gratuity()
+ self.cancel_loan_repayment_entry()
+
+ def on_trash(self):
+ from frappe.model.naming import revert_series_if_last
+ revert_series_if_last(self.series, self.name)
+
+ def get_status(self):
+ if self.docstatus == 0:
+ status = "Draft"
+ elif self.docstatus == 1:
+ status = "Submitted"
+ elif self.docstatus == 2:
+ status = "Cancelled"
+ return status
+
+ def validate_dates(self, joining_date=None, relieving_date=None):
+ if date_diff(self.end_date, self.start_date) < 0:
+ frappe.throw(_("To date cannot be before From date"))
+
+ if not joining_date:
+ joining_date, relieving_date = frappe.get_cached_value(
+ "Employee",
+ self.employee,
+ ("date_of_joining", "relieving_date")
+ )
+
+ if date_diff(self.end_date, joining_date) < 0:
+ frappe.throw(_("Cannot create Salary Slip for Employee joining after Payroll Period"))
+
+ if relieving_date and date_diff(relieving_date, self.start_date) < 0:
+ frappe.throw(_("Cannot create Salary Slip for Employee who has left before Payroll Period"))
+
+ def is_rounding_total_disabled(self):
+ return cint(frappe.db.get_single_value("Payroll Settings", "disable_rounded_total"))
+
+ def check_existing(self):
+ if not self.salary_slip_based_on_timesheet:
+ cond = ""
+ if self.payroll_entry:
+ cond += "and payroll_entry = '{0}'".format(self.payroll_entry)
+ ret_exist = frappe.db.sql("""select name from `tabSalary Slip`
+ where start_date = %s and end_date = %s and docstatus != 2
+ and employee = %s and name != %s {0}""".format(cond),
+ (self.start_date, self.end_date, self.employee, self.name))
+ if ret_exist:
+ self.employee = ''
+ frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee))
+ else:
+ for data in self.timesheets:
+ 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 get_date_details(self):
+ if not self.end_date:
+ date_details = get_start_end_dates(self.payroll_frequency, self.start_date or self.posting_date)
+ self.start_date = date_details.start_date
+ self.end_date = date_details.end_date
+
+ @frappe.whitelist()
+ def get_emp_and_working_day_details(self):
+ '''First time, load all the components from salary structure'''
+ if self.employee:
+ self.set("earnings", [])
+ self.set("deductions", [])
+
+ if not self.salary_slip_based_on_timesheet:
+ self.get_date_details()
+
+ joining_date, relieving_date = frappe.get_cached_value(
+ "Employee",
+ self.employee,
+ ("date_of_joining", "relieving_date")
+ )
+
+ self.validate_dates(joining_date, relieving_date)
+
+ #getin leave details
+ self.get_working_days_details(joining_date, relieving_date)
+ struct = self.check_sal_struct(joining_date, relieving_date)
+
+ if struct:
+ 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()
+ ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1)
+ return [ps.payroll_based_on, ps.consider_unmarked_attendance_as]
+
+ def set_time_sheet(self):
+ if self.salary_slip_based_on_timesheet:
+ self.set("timesheets", [])
+ timesheets = frappe.db.sql(""" select * from `tabTimesheet` where employee = %(employee)s and start_date BETWEEN %(start_date)s AND %(end_date)s and (status = 'Submitted' or
+ status = 'Billed')""", {'employee': self.employee, 'start_date': self.start_date, 'end_date': self.end_date}, as_dict=1)
+
+ for data in timesheets:
+ self.append('timesheets', {
+ 'time_sheet': data.name,
+ 'working_hours': data.total_hours
+ })
+
+ def check_sal_struct(self, joining_date, relieving_date):
+ cond = """and sa.employee=%(employee)s and (sa.from_date <= %(start_date)s or
+ sa.from_date <= %(end_date)s or sa.from_date <= %(joining_date)s)"""
+ if self.payroll_frequency:
+ cond += """and ss.payroll_frequency = '%(payroll_frequency)s'""" % {"payroll_frequency": self.payroll_frequency}
+
+ st_name = frappe.db.sql("""
+ select sa.salary_structure
+ from `tabSalary Structure Assignment` sa join `tabSalary Structure` ss
+ where sa.salary_structure=ss.name
+ and sa.docstatus = 1 and ss.docstatus = 1 and ss.is_active ='Yes' %s
+ order by sa.from_date desc
+ limit 1
+ """ %cond, {'employee': self.employee, 'start_date': self.start_date,
+ 'end_date': self.end_date, 'joining_date': joining_date})
+
+ if st_name:
+ self.salary_structure = st_name[0][0]
+ return self.salary_structure
+
+ else:
+ self.salary_structure = None
+ frappe.msgprint(_("No active or default Salary Structure found for employee {0} for the given dates")
+ .format(self.employee), title=_('Salary Structure Missing'))
+
+ def pull_sal_struct(self):
+ from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
+
+ if self.salary_slip_based_on_timesheet:
+ self.salary_structure = self._salary_structure_doc.name
+ self.hour_rate = self._salary_structure_doc.hour_rate
+ self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate)
+ self.total_working_hours = sum([d.working_hours or 0.0 for d in self.timesheets]) or 0.0
+ wages_amount = self.hour_rate * self.total_working_hours
+
+ self.add_earning_for_hourly_wages(self, self._salary_structure_doc.salary_component, wages_amount)
+
+ make_salary_slip(self._salary_structure_doc.name, self)
+
+ def get_working_days_details(self, joining_date=None, relieving_date=None, lwp=None, for_preview=0):
+ payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on")
+ include_holidays_in_total_working_days = frappe.db.get_single_value("Payroll Settings", "include_holidays_in_total_working_days")
+
+ working_days = date_diff(self.end_date, self.start_date) + 1
+ if for_preview:
+ self.total_working_days = working_days
+ self.payment_days = working_days
+ return
+
+ holidays = self.get_holidays_for_employee(self.start_date, self.end_date)
+
+ if not cint(include_holidays_in_total_working_days):
+ working_days -= len(holidays)
+ if working_days < 0:
+ frappe.throw(_("There are more holidays than working days this month."))
+
+ if not payroll_based_on:
+ frappe.throw(_("Please set Payroll based on in Payroll settings"))
+
+ if payroll_based_on == "Attendance":
+ actual_lwp, absent = self.calculate_lwp_ppl_and_absent_days_based_on_attendance(holidays)
+ self.absent_days = absent
+ else:
+ actual_lwp = self.calculate_lwp_or_ppl_based_on_leave_application(holidays, working_days)
+
+ if not lwp:
+ lwp = actual_lwp
+ elif lwp != actual_lwp:
+ frappe.msgprint(_("Leave Without Pay does not match with approved {} records")
+ .format(payroll_based_on))
+
+ self.leave_without_pay = lwp
+ self.total_working_days = working_days
+
+ payment_days = self.get_payment_days(joining_date,
+ relieving_date, include_holidays_in_total_working_days)
+
+ if flt(payment_days) > flt(lwp):
+ self.payment_days = flt(payment_days) - flt(lwp)
+
+ if payroll_based_on == "Attendance":
+ self.payment_days -= flt(absent)
+
+ unmarked_days = self.get_unmarked_days()
+ consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, "consider_unmarked_attendance_as") or "Present"
+
+ if payroll_based_on == "Attendance" and consider_unmarked_attendance_as =="Absent":
+ self.absent_days += unmarked_days #will be treated as absent
+ self.payment_days -= unmarked_days
+ if include_holidays_in_total_working_days:
+ for holiday in holidays:
+ if not frappe.db.exists("Attendance", {"employee": self.employee, "attendance_date": holiday, "docstatus": 1 }):
+ self.payment_days += 1
+ else:
+ self.payment_days = 0
+
+ def get_unmarked_days(self):
+ marked_days = frappe.get_all("Attendance", filters = {
+ "attendance_date": ["between", [self.start_date, self.end_date]],
+ "employee": self.employee,
+ "docstatus": 1
+ }, fields = ["COUNT(*) as marked_days"])[0].marked_days
+
+ return self.total_working_days - marked_days
+
+
+ def get_payment_days(self, joining_date, relieving_date, include_holidays_in_total_working_days):
+ if not joining_date:
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+
+ start_date = getdate(self.start_date)
+ if joining_date:
+ if getdate(self.start_date) <= joining_date <= getdate(self.end_date):
+ start_date = joining_date
+ elif joining_date > getdate(self.end_date):
+ return
+
+ end_date = getdate(self.end_date)
+ if relieving_date:
+ if getdate(self.start_date) <= relieving_date <= getdate(self.end_date):
+ end_date = relieving_date
+ elif relieving_date < getdate(self.start_date):
+ frappe.throw(_("Employee relieved on {0} must be set as 'Left'")
+ .format(relieving_date))
+
+ payment_days = date_diff(end_date, start_date) + 1
+
+ if not cint(include_holidays_in_total_working_days):
+ holidays = self.get_holidays_for_employee(start_date, end_date)
+ payment_days -= len(holidays)
+
+ return payment_days
+
+ def get_holidays_for_employee(self, start_date, end_date):
+ return get_holiday_dates_for_employee(self.employee, start_date, end_date)
+
+ def calculate_lwp_or_ppl_based_on_leave_application(self, holidays, working_days):
+ lwp = 0
+ holidays = "','".join(holidays)
+ daily_wages_fraction_for_half_day = \
+ flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
+
+ for d in range(working_days):
+ dt = add_days(cstr(getdate(self.start_date)), d)
+ leave = frappe.db.sql("""
+ SELECT t1.name,
+ CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
+ THEN t1.half_day else 0 END,
+ t2.is_ppl,
+ t2.fraction_of_daily_salary_per_leave
+ FROM `tabLeave Application` t1, `tabLeave Type` t2
+ WHERE t2.name = t1.leave_type
+ AND (t2.is_lwp = 1 or t2.is_ppl = 1)
+ AND t1.docstatus = 1
+ AND t1.employee = %(employee)s
+ AND ifnull(t1.salary_slip, '') = ''
+ AND CASE
+ WHEN t2.include_holiday != 1
+ THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
+ WHEN t2.include_holiday
+ THEN %(dt)s between from_date and to_date
+ END
+ """.format(holidays), {"employee": self.employee, "dt": dt})
+
+ if leave:
+ equivalent_lwp_count = 0
+ is_half_day_leave = cint(leave[0][1])
+ is_partially_paid_leave = cint(leave[0][2])
+ fraction_of_daily_salary_per_leave = flt(leave[0][3])
+
+ equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
+
+ if is_partially_paid_leave:
+ equivalent_lwp_count *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+
+ lwp += equivalent_lwp_count
+
+ return lwp
+
+ def calculate_lwp_ppl_and_absent_days_based_on_attendance(self, holidays):
+ lwp = 0
+ absent = 0
+
+ daily_wages_fraction_for_half_day = \
+ flt(frappe.db.get_value("Payroll Settings", None, "daily_wages_fraction_for_half_day")) or 0.5
+
+ leave_types = frappe.get_all("Leave Type",
+ or_filters=[["is_ppl", "=", 1], ["is_lwp", "=", 1]],
+ fields =["name", "is_lwp", "is_ppl", "fraction_of_daily_salary_per_leave", "include_holiday"])
+
+ leave_type_map = {}
+ for leave_type in leave_types:
+ leave_type_map[leave_type.name] = leave_type
+
+ attendances = frappe.db.sql('''
+ SELECT attendance_date, status, leave_type
+ FROM `tabAttendance`
+ WHERE
+ status in ("Absent", "Half Day", "On leave")
+ AND employee = %s
+ AND docstatus = 1
+ AND attendance_date between %s and %s
+ ''', values=(self.employee, self.start_date, self.end_date), as_dict=1)
+
+ for d in attendances:
+ if d.status in ('Half Day', 'On Leave') and d.leave_type and d.leave_type not in leave_type_map.keys():
+ continue
+
+ if formatdate(d.attendance_date, "yyyy-mm-dd") in holidays:
+ if d.status == "Absent" or \
+ (d.leave_type and d.leave_type in leave_type_map.keys() and not leave_type_map[d.leave_type]['include_holiday']):
+ continue
+
+ if d.leave_type:
+ fraction_of_daily_salary_per_leave = leave_type_map[d.leave_type]["fraction_of_daily_salary_per_leave"]
+
+ if d.status == "Half Day":
+ equivalent_lwp = (1 - daily_wages_fraction_for_half_day)
+
+ if d.leave_type in leave_type_map.keys() and leave_type_map[d.leave_type]["is_ppl"]:
+ equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+ lwp += equivalent_lwp
+ elif d.status == "On Leave" and d.leave_type and d.leave_type in leave_type_map.keys():
+ equivalent_lwp = 1
+ if leave_type_map[d.leave_type]["is_ppl"]:
+ equivalent_lwp *= fraction_of_daily_salary_per_leave if fraction_of_daily_salary_per_leave else 1
+ lwp += equivalent_lwp
+ elif d.status == "Absent":
+ absent += 1
+ return lwp, absent
+
+ def add_earning_for_hourly_wages(self, doc, salary_component, amount):
+ row_exists = False
+ for row in doc.earnings:
+ if row.salary_component == salary_component:
+ row.amount = amount
+ row_exists = True
+ break
+
+ if not row_exists:
+ wages_row = {
+ "salary_component": salary_component,
+ "abbr": frappe.db.get_value("Salary Component", salary_component, "salary_component_abbr"),
+ "amount": self.hour_rate * self.total_working_hours,
+ "default_amount": 0.0,
+ "additional_amount": 0.0
+ }
+ doc.append('earnings', wages_row)
+
+ def calculate_net_pay(self):
+ if self.salary_structure:
+ self.calculate_component_amounts("earnings")
+ self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
+ self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay'))
+
+ if self.salary_structure:
+ self.calculate_component_amounts("deductions")
+
+ self.set_loan_repayment()
+ self.set_component_amounts_based_on_payment_days()
+ self.set_net_pay()
+
+ def set_net_pay(self):
+ self.total_deduction = self.get_component_totals("deductions")
+ self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
+ self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
+ self.rounded_total = rounded(self.net_pay)
+ self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay'))
+ self.base_rounded_total = flt(rounded(self.base_net_pay), self.precision('base_net_pay'))
+ if self.hour_rate:
+ self.base_hour_rate = flt(flt(self.hour_rate) * flt(self.exchange_rate), self.precision('base_hour_rate'))
+ self.set_net_total_in_words()
+
+ def calculate_component_amounts(self, component_type):
+ if not getattr(self, '_salary_structure_doc', None):
+ self._salary_structure_doc = frappe.get_doc('Salary Structure', self.salary_structure)
+
+ payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
+
+ self.add_structure_components(component_type)
+ self.add_additional_salary_components(component_type)
+ if component_type == "earnings":
+ self.add_employee_benefits(payroll_period)
+ else:
+ self.add_tax_components(payroll_period)
+
+ def add_structure_components(self, component_type):
+ data = self.get_data_for_eval()
+ for struct_row in self._salary_structure_doc.get(component_type):
+ amount = self.eval_condition_and_formula(struct_row, data)
+ if amount and struct_row.statistical_component == 0:
+ self.update_component_row(struct_row, amount, component_type)
+
+ def get_data_for_eval(self):
+ '''Returns data for evaluating formula'''
+ data = frappe._dict()
+ employee = frappe.get_doc("Employee", self.employee).as_dict()
+
+ start_date = getdate(self.start_date)
+ date_to_validate = (
+ employee.date_of_joining
+ if employee.date_of_joining > start_date
+ else start_date
+ )
+
+ salary_structure_assignment = frappe.get_value(
+ "Salary Structure Assignment",
+ {
+ "employee": self.employee,
+ "salary_structure": self.salary_structure,
+ "from_date": ("<=", date_to_validate),
+ "docstatus": 1,
+ },
+ "*",
+ order_by="from_date desc",
+ as_dict=True,
+ )
+
+ if not salary_structure_assignment:
+ frappe.throw(
+ _("Please assign a Salary Structure for Employee {0} "
+ "applicable from or before {1} first").format(
+ frappe.bold(self.employee_name),
+ frappe.bold(formatdate(date_to_validate)),
+ )
+ )
+
+ data.update(salary_structure_assignment)
+ data.update(employee)
+ data.update(self.as_dict())
+
+ # set values for components
+ salary_components = frappe.get_all("Salary Component", fields=["salary_component_abbr"])
+ for sc in salary_components:
+ data.setdefault(sc.salary_component_abbr, 0)
+
+ for key in ('earnings', 'deductions'):
+ for d in self.get(key):
+ data[d.abbr] = d.amount
+
+ return data
+
+ def eval_condition_and_formula(self, d, data):
+ try:
+ condition = d.condition.strip().replace("\n", " ") if d.condition else None
+ if condition:
+ if not frappe.safe_eval(condition, self.whitelisted_globals, data):
+ return None
+ amount = d.amount
+ if d.amount_based_on_formula:
+ formula = d.formula.strip().replace("\n", " ") if d.formula else None
+ if formula:
+ amount = flt(frappe.safe_eval(formula, self.whitelisted_globals, data), d.precision("amount"))
+ if amount:
+ data[d.abbr] = amount
+
+ return amount
+
+ except NameError as err:
+ frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
+ except SyntaxError as err:
+ frappe.throw(_("Syntax error in formula or condition: {0}").format(err))
+ except Exception as e:
+ frappe.throw(_("Error in formula or condition: {0}").format(e))
+ raise
+
+ def add_employee_benefits(self, payroll_period):
+ for struct_row in self._salary_structure_doc.get("earnings"):
+ if struct_row.is_flexible_benefit == 1:
+ if frappe.db.get_value("Salary Component", struct_row.salary_component, "pay_against_benefit_claim") != 1:
+ benefit_component_amount = get_benefit_component_amount(self.employee, self.start_date, self.end_date,
+ struct_row.salary_component, self._salary_structure_doc, self.payroll_frequency, payroll_period)
+ if benefit_component_amount:
+ self.update_component_row(struct_row, benefit_component_amount, "earnings")
+ else:
+ benefit_claim_amount = get_benefit_claim_amount(self.employee, self.start_date, self.end_date, struct_row.salary_component)
+ if benefit_claim_amount:
+ self.update_component_row(struct_row, benefit_claim_amount, "earnings")
+
+ self.adjust_benefits_in_last_payroll_period(payroll_period)
+
+ def adjust_benefits_in_last_payroll_period(self, payroll_period):
+ if payroll_period:
+ if (getdate(payroll_period.end_date) <= getdate(self.end_date)):
+ last_benefits = get_last_payroll_period_benefits(self.employee, self.start_date, self.end_date,
+ payroll_period, self._salary_structure_doc)
+ if last_benefits:
+ for last_benefit in last_benefits:
+ last_benefit = frappe._dict(last_benefit)
+ amount = last_benefit.amount
+ self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings")
+
+ def add_additional_salary_components(self, component_type):
+ additional_salaries = get_additional_salaries(self.employee,
+ self.start_date, self.end_date, component_type)
+
+ for additional_salary in additional_salaries:
+ self.update_component_row(
+ get_salary_component_data(additional_salary.component),
+ additional_salary.amount,
+ component_type,
+ additional_salary
+ )
+
+ def add_tax_components(self, payroll_period):
+ # Calculate variable_based_on_taxable_salary after all components updated in salary slip
+ tax_components, other_deduction_components = [], []
+ for d in self._salary_structure_doc.get("deductions"):
+ if d.variable_based_on_taxable_salary == 1 and not d.formula and not flt(d.amount):
+ tax_components.append(d.salary_component)
+ else:
+ other_deduction_components.append(d.salary_component)
+
+ if not tax_components:
+ tax_components = [d.name for d in frappe.get_all("Salary Component", filters={"variable_based_on_taxable_salary": 1})
+ if d.name not in other_deduction_components]
+
+ for d in tax_components:
+ tax_amount = self.calculate_variable_based_on_taxable_salary(d, payroll_period)
+ tax_row = get_salary_component_data(d)
+ self.update_component_row(tax_row, tax_amount, "deductions")
+
+ def update_component_row(self, component_data, amount, component_type, additional_salary=None):
+ component_row = None
+ for d in self.get(component_type):
+ if d.salary_component != component_data.salary_component:
+ continue
+
+ if (
+ (
+ not d.additional_salary
+ and (not additional_salary or additional_salary.overwrite)
+ ) or (
+ additional_salary
+ and additional_salary.name == d.additional_salary
+ )
+ ):
+ component_row = d
+ break
+
+ if additional_salary and additional_salary.overwrite:
+ # Additional Salary with overwrite checked, remove default rows of same component
+ self.set(component_type, [
+ d for d in self.get(component_type)
+ if d.salary_component != component_data.salary_component
+ or (d.additional_salary and additional_salary.name != d.additional_salary)
+ or d == component_row
+ ])
+
+ if not component_row:
+ if not amount:
+ return
+
+ component_row = self.append(component_type)
+ for attr in (
+ 'depends_on_payment_days', 'salary_component',
+ 'do_not_include_in_total', 'is_tax_applicable',
+ 'is_flexible_benefit', 'variable_based_on_taxable_salary',
+ 'exempted_from_income_tax'
+ ):
+ component_row.set(attr, component_data.get(attr))
+
+ abbr = component_data.get('abbr') or component_data.get('salary_component_abbr')
+ component_row.set('abbr', abbr)
+
+ if additional_salary:
+ if additional_salary.overwrite:
+ component_row.additional_amount = flt(flt(amount) - flt(component_row.get("default_amount", 0)),
+ component_row.precision("additional_amount"))
+ else:
+ component_row.default_amount = 0
+ component_row.additional_amount = amount
+
+ component_row.additional_salary = additional_salary.name
+ component_row.deduct_full_tax_on_selected_payroll_date = \
+ additional_salary.deduct_full_tax_on_selected_payroll_date
+ else:
+ component_row.default_amount = amount
+ component_row.additional_amount = 0
+ component_row.deduct_full_tax_on_selected_payroll_date = \
+ component_data.deduct_full_tax_on_selected_payroll_date
+
+ component_row.amount = amount
+
+ def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period):
+ if not payroll_period:
+ frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}.")
+ .format(tax_component))
+ return
+
+ # Deduct taxes forcefully for unsubmitted tax exemption proof and unclaimed benefits in the last period
+ if payroll_period.end_date <= getdate(self.end_date):
+ self.deduct_tax_for_unsubmitted_tax_exemption_proof = 1
+ self.deduct_tax_for_unclaimed_employee_benefits = 1
+
+ return self.calculate_variable_tax(payroll_period, tax_component)
+
+ def calculate_variable_tax(self, payroll_period, tax_component):
+ # get Tax slab from salary structure assignment for the employee and payroll period
+ tax_slab = self.get_income_tax_slabs(payroll_period)
+
+ # get remaining numbers of sub-period (period for which one salary is processed)
+ remaining_sub_periods = get_period_factor(self.employee,
+ self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
+ # get taxable_earnings, paid_taxes for previous period
+ previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
+ self.start_date, tax_slab.allow_tax_exemption)
+ previous_total_paid_taxes = self.get_tax_paid_in_period(payroll_period.start_date, self.start_date, tax_component)
+
+ # get taxable_earnings for current period (all days)
+ current_taxable_earnings = self.get_taxable_earnings(tax_slab.allow_tax_exemption)
+ future_structured_taxable_earnings = current_taxable_earnings.taxable_earnings * (math.ceil(remaining_sub_periods) - 1)
+
+ # get taxable_earnings, addition_earnings for current actual payment days
+ current_taxable_earnings_for_payment_days = self.get_taxable_earnings(tax_slab.allow_tax_exemption, based_on_payment_days=1)
+ current_structured_taxable_earnings = current_taxable_earnings_for_payment_days.taxable_earnings
+ current_additional_earnings = current_taxable_earnings_for_payment_days.additional_income
+ current_additional_earnings_with_full_tax = current_taxable_earnings_for_payment_days.additional_income_with_full_tax
+
+ # Get taxable unclaimed benefits
+ unclaimed_taxable_benefits = 0
+ if self.deduct_tax_for_unclaimed_employee_benefits:
+ unclaimed_taxable_benefits = self.calculate_unclaimed_taxable_benefits(payroll_period)
+ unclaimed_taxable_benefits += current_taxable_earnings_for_payment_days.flexi_benefits
+
+ # Total exemption amount based on tax exemption declaration
+ total_exemption_amount = self.get_total_exemption_amount(payroll_period, tax_slab)
+
+ #Employee Other Incomes
+ other_incomes = self.get_income_form_other_sources(payroll_period) or 0.0
+
+ # Total taxable earnings including additional and other incomes
+ total_taxable_earnings = previous_taxable_earnings + current_structured_taxable_earnings + future_structured_taxable_earnings \
+ + current_additional_earnings + other_incomes + unclaimed_taxable_benefits - total_exemption_amount
+
+ # Total taxable earnings without additional earnings with full tax
+ total_taxable_earnings_without_full_tax_addl_components = total_taxable_earnings - current_additional_earnings_with_full_tax
+
+ # Structured tax amount
+ total_structured_tax_amount = self.calculate_tax_by_tax_slab(
+ total_taxable_earnings_without_full_tax_addl_components, tax_slab)
+ current_structured_tax_amount = (total_structured_tax_amount - previous_total_paid_taxes) / remaining_sub_periods
+
+ # Total taxable earnings with additional earnings with full tax
+ full_tax_on_additional_earnings = 0.0
+ if current_additional_earnings_with_full_tax:
+ total_tax_amount = self.calculate_tax_by_tax_slab(total_taxable_earnings, tax_slab)
+ full_tax_on_additional_earnings = total_tax_amount - total_structured_tax_amount
+
+ current_tax_amount = current_structured_tax_amount + full_tax_on_additional_earnings
+ if flt(current_tax_amount) < 0:
+ current_tax_amount = 0
+
+ return current_tax_amount
+
+ def get_income_tax_slabs(self, payroll_period):
+ income_tax_slab, ss_assignment_name = frappe.db.get_value("Salary Structure Assignment",
+ {"employee": self.employee, "salary_structure": self.salary_structure, "docstatus": 1}, ["income_tax_slab", 'name'])
+
+ if not income_tax_slab:
+ frappe.throw(_("Income Tax Slab not set in Salary Structure Assignment: {0}").format(ss_assignment_name))
+
+ income_tax_slab_doc = frappe.get_doc("Income Tax Slab", income_tax_slab)
+ if income_tax_slab_doc.disabled:
+ frappe.throw(_("Income Tax Slab: {0} is disabled").format(income_tax_slab))
+
+ if getdate(income_tax_slab_doc.effective_from) > getdate(payroll_period.start_date):
+ frappe.throw(_("Income Tax Slab must be effective on or before Payroll Period Start Date: {0}")
+ .format(payroll_period.start_date))
+
+ return income_tax_slab_doc
+
+
+ def get_taxable_earnings_for_prev_period(self, start_date, end_date, allow_tax_exemption=False):
+ taxable_earnings = frappe.db.sql("""
+ select sum(sd.amount)
+ from
+ `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name
+ where
+ sd.parentfield='earnings'
+ and sd.is_tax_applicable=1
+ and is_flexible_benefit=0
+ and ss.docstatus=1
+ and ss.employee=%(employee)s
+ and ss.start_date between %(from_date)s and %(to_date)s
+ and ss.end_date between %(from_date)s and %(to_date)s
+ """, {
+ "employee": self.employee,
+ "from_date": start_date,
+ "to_date": end_date
+ })
+ taxable_earnings = flt(taxable_earnings[0][0]) if taxable_earnings else 0
+
+ exempted_amount = 0
+ if allow_tax_exemption:
+ exempted_amount = frappe.db.sql("""
+ select sum(sd.amount)
+ from
+ `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name
+ where
+ sd.parentfield='deductions'
+ and sd.exempted_from_income_tax=1
+ and is_flexible_benefit=0
+ and ss.docstatus=1
+ and ss.employee=%(employee)s
+ and ss.start_date between %(from_date)s and %(to_date)s
+ and ss.end_date between %(from_date)s and %(to_date)s
+ """, {
+ "employee": self.employee,
+ "from_date": start_date,
+ "to_date": end_date
+ })
+ exempted_amount = flt(exempted_amount[0][0]) if exempted_amount else 0
+
+ return taxable_earnings - exempted_amount
+
+ def get_tax_paid_in_period(self, start_date, end_date, tax_component):
+ # find total_tax_paid, tax paid for benefit, additional_salary
+ total_tax_paid = flt(frappe.db.sql("""
+ select
+ sum(sd.amount)
+ from
+ `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name
+ where
+ sd.parentfield='deductions'
+ and sd.salary_component=%(salary_component)s
+ and sd.variable_based_on_taxable_salary=1
+ and ss.docstatus=1
+ and ss.employee=%(employee)s
+ and ss.start_date between %(from_date)s and %(to_date)s
+ and ss.end_date between %(from_date)s and %(to_date)s
+ """, {
+ "salary_component": tax_component,
+ "employee": self.employee,
+ "from_date": start_date,
+ "to_date": end_date
+ })[0][0])
+
+ return total_tax_paid
+
+ def get_taxable_earnings(self, allow_tax_exemption=False, based_on_payment_days=0):
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+
+ if not relieving_date:
+ relieving_date = getdate(self.end_date)
+
+ if not joining_date:
+ frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
+
+ taxable_earnings = 0
+ additional_income = 0
+ additional_income_with_full_tax = 0
+ flexi_benefits = 0
+
+ for earning in self.earnings:
+ if based_on_payment_days:
+ amount, additional_amount = self.get_amount_based_on_payment_days(earning, joining_date, relieving_date)
+ else:
+ amount, additional_amount = earning.amount, earning.additional_amount
+
+ if earning.is_tax_applicable:
+ if additional_amount:
+ taxable_earnings += (amount - additional_amount)
+ additional_income += additional_amount
+ if earning.deduct_full_tax_on_selected_payroll_date:
+ additional_income_with_full_tax += additional_amount
+ continue
+
+ if earning.is_flexible_benefit:
+ flexi_benefits += amount
+ else:
+ taxable_earnings += amount
+
+ if allow_tax_exemption:
+ for ded in self.deductions:
+ if ded.exempted_from_income_tax:
+ amount = ded.amount
+ if based_on_payment_days:
+ amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0]
+ taxable_earnings -= flt(amount)
+
+ return frappe._dict({
+ "taxable_earnings": taxable_earnings,
+ "additional_income": additional_income,
+ "additional_income_with_full_tax": additional_income_with_full_tax,
+ "flexi_benefits": flexi_benefits
+ })
+
+ def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
+ amount, additional_amount = row.amount, row.additional_amount
+ if (self.salary_structure and
+ cint(row.depends_on_payment_days) and cint(self.total_working_days) and
+ (not self.salary_slip_based_on_timesheet or
+ getdate(self.start_date) < joining_date or
+ (relieving_date and getdate(self.end_date) > relieving_date)
+ )):
+ additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
+ / cint(self.total_working_days)), row.precision("additional_amount"))
+ amount = flt((flt(row.default_amount) * flt(self.payment_days)
+ / cint(self.total_working_days)), row.precision("amount")) + additional_amount
+
+ elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days):
+ amount, additional_amount = 0, 0
+ elif not row.amount:
+ amount = flt(row.default_amount) + flt(row.additional_amount)
+
+ # apply rounding
+ if frappe.get_cached_value("Salary Component", row.salary_component, "round_to_the_nearest_integer"):
+ amount, additional_amount = rounded(amount), rounded(additional_amount)
+
+ return amount, additional_amount
+
+ def calculate_unclaimed_taxable_benefits(self, payroll_period):
+ # get total sum of benefits paid
+ total_benefits_paid = flt(frappe.db.sql("""
+ select sum(sd.amount)
+ from `tabSalary Detail` sd join `tabSalary Slip` ss on sd.parent=ss.name
+ where
+ sd.parentfield='earnings'
+ and sd.is_tax_applicable=1
+ and is_flexible_benefit=1
+ and ss.docstatus=1
+ and ss.employee=%(employee)s
+ and ss.start_date between %(start_date)s and %(end_date)s
+ and ss.end_date between %(start_date)s and %(end_date)s
+ """, {
+ "employee": self.employee,
+ "start_date": payroll_period.start_date,
+ "end_date": self.start_date
+ })[0][0])
+
+ # get total benefits claimed
+ total_benefits_claimed = flt(frappe.db.sql("""
+ select sum(claimed_amount)
+ from `tabEmployee Benefit Claim`
+ where
+ docstatus=1
+ and employee=%s
+ and claim_date between %s and %s
+ """, (self.employee, payroll_period.start_date, self.end_date))[0][0])
+
+ return total_benefits_paid - total_benefits_claimed
+
+ def get_total_exemption_amount(self, payroll_period, tax_slab):
+ total_exemption_amount = 0
+ if tax_slab.allow_tax_exemption:
+ if self.deduct_tax_for_unsubmitted_tax_exemption_proof:
+ exemption_proof = frappe.db.get_value("Employee Tax Exemption Proof Submission",
+ {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1},
+ ["exemption_amount"])
+ if exemption_proof:
+ total_exemption_amount = exemption_proof
+ else:
+ declaration = frappe.db.get_value("Employee Tax Exemption Declaration",
+ {"employee": self.employee, "payroll_period": payroll_period.name, "docstatus": 1},
+ ["total_exemption_amount"])
+ if declaration:
+ total_exemption_amount = declaration
+
+ total_exemption_amount += flt(tax_slab.standard_tax_exemption_amount)
+
+ return total_exemption_amount
+
+ def get_income_form_other_sources(self, payroll_period):
+ return frappe.get_all("Employee Other Income",
+ filters={
+ "employee": self.employee,
+ "payroll_period": payroll_period.name,
+ "company": self.company,
+ "docstatus": 1
+ },
+ fields="SUM(amount) as total_amount"
+ )[0].total_amount
+
+ def calculate_tax_by_tax_slab(self, annual_taxable_earning, tax_slab):
+ data = self.get_data_for_eval()
+ data.update({"annual_taxable_earning": annual_taxable_earning})
+ tax_amount = 0
+ for slab in tax_slab.slabs:
+ if slab.condition and not self.eval_tax_slab_condition(slab.condition, data):
+ continue
+ if not slab.to_amount and annual_taxable_earning >= slab.from_amount:
+ tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
+ continue
+ if annual_taxable_earning >= slab.from_amount and annual_taxable_earning < slab.to_amount:
+ tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01
+ elif annual_taxable_earning >= slab.from_amount and annual_taxable_earning >= slab.to_amount:
+ tax_amount += (slab.to_amount - slab.from_amount + 1) * slab.percent_deduction * .01
+
+ # other taxes and charges on income tax
+ for d in tax_slab.other_taxes_and_charges:
+ if flt(d.min_taxable_income) and flt(d.min_taxable_income) > annual_taxable_earning:
+ continue
+
+ if flt(d.max_taxable_income) and flt(d.max_taxable_income) < annual_taxable_earning:
+ continue
+
+ tax_amount += tax_amount * flt(d.percent) / 100
+
+ return tax_amount
+
+ def eval_tax_slab_condition(self, condition, data):
+ try:
+ condition = condition.strip()
+ if condition:
+ return frappe.safe_eval(condition, self.whitelisted_globals, data)
+ except NameError as err:
+ frappe.throw(_("{0}
This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
+ except SyntaxError as err:
+ frappe.throw(_("Syntax error in condition: {0}").format(err))
+ except Exception as e:
+ frappe.throw(_("Error in formula or condition: {0}").format(e))
+ raise
+
+ def get_component_totals(self, component_type, depends_on_payment_days=0):
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+
+ total = 0.0
+ for d in self.get(component_type):
+ if not d.do_not_include_in_total:
+ if depends_on_payment_days:
+ amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
+ else:
+ amount = flt(d.amount, d.precision("amount"))
+ total += amount
+ return total
+
+ def set_component_amounts_based_on_payment_days(self):
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+
+ if not relieving_date:
+ relieving_date = getdate(self.end_date)
+
+ if not joining_date:
+ frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
+
+ for component_type in ("earnings", "deductions"):
+ for d in self.get(component_type):
+ d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
+
+ def set_loan_repayment(self):
+ self.total_loan_repayment = 0
+ self.total_interest_amount = 0
+ self.total_principal_amount = 0
+
+ if not self.get('loans'):
+ for loan in self.get_loan_details():
+
+ amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
+
+ if amounts['interest_amount'] or amounts['payable_principal_amount']:
+ self.append('loans', {
+ 'loan': loan.name,
+ 'total_payment': amounts['interest_amount'] + amounts['payable_principal_amount'],
+ 'interest_amount': amounts['interest_amount'],
+ 'principal_amount': amounts['payable_principal_amount'],
+ 'loan_account': loan.loan_account,
+ 'interest_income_account': loan.interest_income_account
+ })
+
+ for payment in self.get('loans'):
+ amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
+ total_amount = amounts['interest_amount'] + amounts['payable_principal_amount']
+ if payment.total_payment > total_amount:
+ frappe.throw(_("""Row {0}: Paid amount {1} is greater than pending accrued amount {2} against loan {3}""")
+ .format(payment.idx, frappe.bold(payment.total_payment),
+ frappe.bold(total_amount), frappe.bold(payment.loan)))
+
+ self.total_interest_amount += payment.interest_amount
+ self.total_principal_amount += payment.principal_amount
+
+ self.total_loan_repayment += payment.total_payment
+
+ def get_loan_details(self):
+ return frappe.get_all("Loan",
+ fields=["name", "interest_income_account", "loan_account", "loan_type"],
+ filters = {
+ "applicant": self.employee,
+ "docstatus": 1,
+ "repay_from_salary": 1,
+ "company": self.company
+ })
+
+ def make_loan_repayment_entry(self):
+ for loan in self.loans:
+ repayment_entry = create_repayment_entry(loan.loan, self.employee,
+ self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount,
+ loan.principal_amount, loan.total_payment)
+
+ repayment_entry.save()
+ repayment_entry.submit()
+
+ frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name)
+
+ def cancel_loan_repayment_entry(self):
+ for loan in self.loans:
+ if loan.loan_repayment_entry:
+ repayment_entry = frappe.get_doc("Loan Repayment", loan.loan_repayment_entry)
+ repayment_entry.cancel()
+
+ def email_salary_slip(self):
+ receiver = frappe.db.get_value("Employee", self.employee, "prefered_email")
+ payroll_settings = frappe.get_single("Payroll Settings")
+ message = "Please see attachment"
+ password = None
+ if payroll_settings.encrypt_salary_slips_in_emails:
+ password = generate_password_for_pdf(payroll_settings.password_policy, self.employee)
+ message += """
Note: Your salary slip is password protected,
+ the password to unlock the PDF is of the format {0}. """.format(payroll_settings.password_policy)
+
+ if receiver:
+ email_args = {
+ "recipients": [receiver],
+ "message": _(message),
+ "subject": 'Salary Slip - from {0} to {1}'.format(self.start_date, self.end_date),
+ "attachments": [frappe.attach_print(self.doctype, self.name, file_name=self.name, password=password)],
+ "reference_doctype": self.doctype,
+ "reference_name": self.name
+ }
+ if not frappe.flags.in_test:
+ enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
+ else:
+ frappe.sendmail(**email_args)
+ else:
+ msgprint(_("{0}: Employee email not found, hence email not sent").format(self.employee_name))
+
+ def update_status(self, salary_slip=None):
+ for data in self.timesheets:
+ if data.time_sheet:
+ timesheet = frappe.get_doc('Timesheet', data.time_sheet)
+ timesheet.salary_slip = salary_slip
+ timesheet.flags.ignore_validate_update_after_submit = True
+ timesheet.set_status()
+ timesheet.save()
+
+ def set_status(self, status=None):
+ '''Get and update status'''
+ if not status:
+ status = self.get_status()
+ self.db_set("status", status)
+
+
+ def process_salary_structure(self, for_preview=0):
+ '''Calculate salary after salary structure details have been updated'''
+ if not self.salary_slip_based_on_timesheet:
+ self.get_date_details()
+ self.pull_emp_details()
+ self.get_working_days_details(for_preview=for_preview)
+ self.calculate_net_pay()
+
+ def pull_emp_details(self):
+ emp = frappe.db.get_value("Employee", self.employee, ["bank_name", "bank_ac_no", "salary_mode"], as_dict=1)
+ if emp:
+ self.mode_of_payment = emp.salary_mode
+ self.bank_name = emp.bank_name
+ self.bank_account_no = emp.bank_ac_no
+
+ @frappe.whitelist()
+ def process_salary_based_on_working_days(self):
+ self.get_working_days_details(lwp=self.leave_without_pay)
+ self.calculate_net_pay()
+
+ @frappe.whitelist()
+ def set_totals(self):
+ self.gross_pay = 0.0
+ if self.salary_slip_based_on_timesheet == 1:
+ self.calculate_total_for_salary_slip_based_on_timesheet()
+ else:
+ self.total_deduction = 0.0
+ if hasattr(self, "earnings"):
+ for earning in self.earnings:
+ self.gross_pay += flt(earning.amount, earning.precision("amount"))
+ if hasattr(self, "deductions"):
+ for deduction in self.deductions:
+ self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
+ self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
+ self.set_base_totals()
+
+ def set_base_totals(self):
+ self.base_gross_pay = flt(self.gross_pay) * flt(self.exchange_rate)
+ self.base_total_deduction = flt(self.total_deduction) * flt(self.exchange_rate)
+ self.rounded_total = rounded(self.net_pay)
+ self.base_net_pay = flt(self.net_pay) * flt(self.exchange_rate)
+ self.base_rounded_total = rounded(self.base_net_pay)
+ self.set_net_total_in_words()
+
+ #calculate total working hours, earnings based on hourly wages and totals
+ def calculate_total_for_salary_slip_based_on_timesheet(self):
+ if self.timesheets:
+ self.total_working_hours = 0
+ for timesheet in self.timesheets:
+ if timesheet.working_hours:
+ self.total_working_hours += timesheet.working_hours
+
+ wages_amount = self.total_working_hours * self.hour_rate
+ self.base_hour_rate = flt(self.hour_rate) * flt(self.exchange_rate)
+ salary_component = frappe.db.get_value('Salary Structure', {'name': self.salary_structure}, 'salary_component')
+ if self.earnings:
+ for i, earning in enumerate(self.earnings):
+ if earning.salary_component == salary_component:
+ self.earnings[i].amount = wages_amount
+ self.gross_pay += self.earnings[i].amount
+ self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
+
+ def compute_year_to_date(self):
+ year_to_date = 0
+ period_start_date, period_end_date = self.get_year_to_date_period()
+
+ salary_slip_sum = frappe.get_list('Salary Slip',
+ fields = ['sum(net_pay) as net_sum', 'sum(gross_pay) as gross_sum'],
+ filters = {'employee_name' : self.employee_name,
+ 'start_date' : ['>=', period_start_date],
+ 'end_date' : ['<', period_end_date],
+ 'name': ['!=', self.name],
+ 'docstatus': 1
+ })
+
+ year_to_date = flt(salary_slip_sum[0].net_sum) if salary_slip_sum else 0.0
+ gross_year_to_date = flt(salary_slip_sum[0].gross_sum) if salary_slip_sum else 0.0
+
+ year_to_date += self.net_pay
+ gross_year_to_date += self.gross_pay
+ self.year_to_date = year_to_date
+ self.gross_year_to_date = gross_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],
+ 'name': ['!=', self.name],
+ 'docstatus': 1
+ })
+
+ 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 compute_component_wise_year_to_date(self):
+ period_start_date, period_end_date = self.get_year_to_date_period()
+
+ for key in ('earnings', 'deductions'):
+ for component in self.get(key):
+ year_to_date = 0
+ component_sum = frappe.db.sql("""
+ SELECT sum(detail.amount) as sum
+ FROM `tabSalary Detail` as detail
+ INNER JOIN `tabSalary Slip` as salary_slip
+ ON detail.parent = salary_slip.name
+ WHERE
+ salary_slip.employee_name = %(employee_name)s
+ AND detail.salary_component = %(component)s
+ AND salary_slip.start_date >= %(period_start_date)s
+ AND salary_slip.end_date < %(period_end_date)s
+ AND salary_slip.name != %(docname)s
+ AND salary_slip.docstatus = 1""",
+ {'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date,
+ 'period_end_date': period_end_date, 'docname': self.name}
+ )
+
+ year_to_date = flt(component_sum[0][0]) if component_sum else 0.0
+ year_to_date += component.amount
+ component.year_to_date = year_to_date
+
+ def get_year_to_date_period(self):
+ 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
+
+ return period_start_date, period_end_date
+
+ def add_leave_balances(self):
+ self.set('leave_details', [])
+
+ if frappe.db.get_single_value('Payroll Settings', 'show_leave_balances_in_salary_slip'):
+ from erpnext.hr.doctype.leave_application.leave_application import get_leave_details
+ leave_details = get_leave_details(self.employee, self.end_date)
+
+ for leave_type, leave_values in iteritems(leave_details['leave_allocation']):
+ self.append('leave_details', {
+ 'leave_type': leave_type,
+ 'total_allocated_leaves': flt(leave_values.get('total_leaves')),
+ 'expired_leaves': flt(leave_values.get('expired_leaves')),
+ 'used_leaves': flt(leave_values.get('leaves_taken')),
+ 'pending_leaves': flt(leave_values.get('pending_leaves')),
+ 'available_leaves': flt(leave_values.get('remaining_leaves'))
+ })
+
+def unlink_ref_doc_from_salary_slip(ref_no):
+ linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
+ where journal_entry=%s and docstatus < 2""", (ref_no))
+ if linked_ss:
+ for ss in linked_ss:
+ ss_doc = frappe.get_doc("Salary Slip", ss)
+ frappe.db.set_value("Salary Slip", ss_doc.name, "journal_entry", "")
+
+def generate_password_for_pdf(policy_template, employee):
+ employee = frappe.get_doc("Employee", employee)
+ return policy_template.format(**employee.as_dict())
+
+def get_salary_component_data(component):
+ return frappe.get_value(
+ "Salary Component",
+ component,
+ [
+ "name as salary_component",
+ "depends_on_payment_days",
+ "salary_component_abbr as abbr",
+ "do_not_include_in_total",
+ "is_tax_applicable",
+ "is_flexible_benefit",
+ "variable_based_on_taxable_salary",
+ ],
+ as_dict=1,
+ )