From f152a40b2876fb30fc2d47a9944300c03a28776e Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 24 Dec 2021 16:23:33 +0530 Subject: [PATCH 1/4] feat: Provision to assign multiple payroll cost centers against a single employee --- erpnext/hr/doctype/department/department.js | 9 ++++ erpnext/hr/doctype/employee/employee.js | 9 ++++ erpnext/hr/doctype/employee/employee.py | 18 +++++--- erpnext/patches.txt | 3 +- .../patches/v14_0/set_payroll_cost_centers.py | 31 +++++++++++++ .../doctype/employee_cost_center/__init__.py | 0 .../employee_cost_center.json | 43 ++++++++++++++++++ .../employee_cost_center.py | 8 ++++ .../doctype/payroll_entry/payroll_entry.py | 45 ++++++++++++++++--- .../payroll_entry/test_payroll_entry.py | 33 ++++++++++---- .../doctype/salary_slip/salary_slip.json | 12 +---- .../salary_structure/salary_structure.py | 5 +-- .../salary_structure_assignment.js | 25 ++++++----- .../salary_structure_assignment.json | 25 +++++++++-- .../salary_structure_assignment.py | 31 ++++++++++++- 15 files changed, 244 insertions(+), 53 deletions(-) create mode 100644 erpnext/patches/v14_0/set_payroll_cost_centers.py create mode 100644 erpnext/payroll/doctype/employee_cost_center/__init__.py create mode 100644 erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json create mode 100644 erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py diff --git a/erpnext/hr/doctype/department/department.js b/erpnext/hr/doctype/department/department.js index 7db8cfbd60..eb759a323e 100644 --- a/erpnext/hr/doctype/department/department.js +++ b/erpnext/hr/doctype/department/department.js @@ -6,6 +6,15 @@ frappe.ui.form.on('Department', { frm.set_query("parent_department", function(){ return {"filters": [["Department", "is_group", "=", 1]]}; }); + + frm.set_query("payroll_cost_center", function() { + return { + filters: { + "company": frm.doc.company, + "is_group": 0 + } + } + }); }, refresh: function(frm) { // read-only for root department diff --git a/erpnext/hr/doctype/employee/employee.js b/erpnext/hr/doctype/employee/employee.js index 13b33e2e74..4181e0f8bb 100755 --- a/erpnext/hr/doctype/employee/employee.js +++ b/erpnext/hr/doctype/employee/employee.js @@ -47,6 +47,15 @@ frappe.ui.form.on('Employee', { } }; }); + + frm.set_query("payroll_cost_center", function() { + return { + filters: { + "company": frm.doc.company, + "is_group": 0 + } + } + }); }, onload: function (frm) { frm.set_query("department", function() { diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 88e5ca9d4c..a2df26c3e2 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -68,12 +68,18 @@ class Employee(NestedSet): self.employee_name = ' '.join(filter(lambda x: x, [self.first_name, self.middle_name, self.last_name])) def validate_user_details(self): - data = frappe.db.get_value('User', - self.user_id, ['enabled', 'user_image'], as_dict=1) - if data.get("user_image") and self.image == '': - self.image = data.get("user_image") - self.validate_for_enabled_user_id(data.get("enabled", 0)) - self.validate_duplicate_user_id() + if self.user_id: + data = frappe.db.get_value('User', + self.user_id, ['enabled', 'user_image'], as_dict=1) + + if not data: + self.user_id = None + return + + if data.get("user_image") and self.image == '': + self.image = data.get("user_image") + self.validate_for_enabled_user_id(data.get("enabled", 0)) + self.validate_duplicate_user_id() def update_nsm_model(self): frappe.utils.nestedset.update_nsm(self) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d9cedab52a..ff8b6b3503 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -317,4 +317,5 @@ erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.migrate_crm_settings erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 -erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template \ No newline at end of file +erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template +erpnext.patches.v14_0.set_payroll_cost_centers \ No newline at end of file diff --git a/erpnext/patches/v14_0/set_payroll_cost_centers.py b/erpnext/patches/v14_0/set_payroll_cost_centers.py new file mode 100644 index 0000000000..db5baa5d20 --- /dev/null +++ b/erpnext/patches/v14_0/set_payroll_cost_centers.py @@ -0,0 +1,31 @@ +import frappe + +def execute(): + frappe.reload_doc('payroll', 'doctype', 'employee_cost_center') + frappe.reload_doc('payroll', 'doctype', 'salary_structure_assignment') + + employees = frappe.get_all("Employee", fields=["department", "payroll_cost_center", "name"]) + + employee_cost_center = {} + for d in employees: + cost_center = d.payroll_cost_center + if not cost_center and d.department: + cost_center = frappe.get_cached_value("Department", d.department, "payroll_cost_center") + + if cost_center: + employee_cost_center.setdefault(d.name, cost_center) + + salary_structure_assignments = frappe.get_all("Salary Structure Assignment", + filters = {"docstatus": ["!=", 2]}, + fields=["name", "employee"]) + + for d in salary_structure_assignments: + cost_center = employee_cost_center.get(d.employee) + if cost_center: + assignment = frappe.get_doc("Salary Structure Assignment", d.name) + if not assignment.get("payroll_cost_centers"): + assignment.append("payroll_cost_centers", { + "cost_center": cost_center, + "percentage": 100 + }) + assignment.save() \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_cost_center/__init__.py b/erpnext/payroll/doctype/employee_cost_center/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json new file mode 100644 index 0000000000..8fed9f7752 --- /dev/null +++ b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.json @@ -0,0 +1,43 @@ +{ + "actions": [], + "creation": "2021-12-23 12:44:38.389283", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "cost_center", + "percentage" + ], + "fields": [ + { + "allow_on_submit": 1, + "fieldname": "cost_center", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Cost Center", + "options": "Cost Center", + "reqd": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "percentage", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Percentage (%)", + "non_negative": 1, + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2021-12-23 17:39:03.410924", + "modified_by": "Administrator", + "module": "Payroll", + "name": "Employee Cost Center", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py new file mode 100644 index 0000000000..91bcc21d18 --- /dev/null +++ b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py @@ -0,0 +1,8 @@ +# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + +class EmployeeCostCenter(Document): + pass diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 84c59a2c2b..6aa11bc117 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -158,7 +158,7 @@ class PayrollEntry(Document): """ ss_list = frappe.db.sql(""" - select t1.name, t1.salary_structure, t1.payroll_cost_center from `tabSalary Slip` t1 + select t1.name, t1.salary_structure from `tabSalary Slip` t1 where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict) @@ -192,9 +192,15 @@ class PayrollEntry(Document): salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True) if salary_slips: salary_components = frappe.db.sql(""" - select ssd.salary_component, ssd.amount, ssd.parentfield, ss.payroll_cost_center - from `tabSalary Slip` ss, `tabSalary Detail` ssd - where ss.name = ssd.parent and ssd.parentfield = '%s' and ss.name in (%s) + SELECT + ssd.salary_component, ssd.amount, ssd.parentfield, ss.salary_structure, ss.employee + FROM + `tabSalary Slip` ss, + `tabSalary Detail` ssd + WHERE + ss.name = ssd.parent + and ssd.parentfield = '%s' + and ss.name in (%s) """ % (component_type, ', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=True) @@ -204,18 +210,43 @@ class PayrollEntry(Document): salary_components = self.get_salary_components(component_type) if salary_components: component_dict = {} + self.employee_cost_centers = {} for item in salary_components: + employee_cost_centers = self.get_payroll_cost_centers_for_employee(item.employee, item.salary_structure) + add_component_to_accrual_jv_entry = True if component_type == "earnings": - is_flexible_benefit, only_tax_impact = frappe.db.get_value("Salary Component", item['salary_component'], ['is_flexible_benefit', 'only_tax_impact']) + is_flexible_benefit, only_tax_impact = \ + frappe.get_cached_value("Salary Component",item['salary_component'], ['is_flexible_benefit', 'only_tax_impact']) if is_flexible_benefit == 1 and only_tax_impact ==1: add_component_to_accrual_jv_entry = False + if add_component_to_accrual_jv_entry: - component_dict[(item.salary_component, item.payroll_cost_center)] \ - = component_dict.get((item.salary_component, item.payroll_cost_center), 0) + flt(item.amount) + for cost_center, percentage in employee_cost_centers.items(): + amount_against_cost_center = flt(item.amount) * percentage / 100 + component_dict[(item.salary_component, cost_center)] \ + = component_dict.get((item.salary_component, cost_center), 0) + amount_against_cost_center + account_details = self.get_account(component_dict = component_dict) return account_details + def get_payroll_cost_centers_for_employee(self, employee, salary_structure): + if not self.employee_cost_centers.get(employee): + ss_assignment_name = frappe.db.get_value("Salary Structure Assignment", + {"employee": employee, "salary_structure": salary_structure, "docstatus": 1}, 'name') + + if ss_assignment_name: + cost_centers = dict(frappe.get_all("Employee Cost Center", {"parent": ss_assignment_name}, + ["cost_center", "percentage"], as_list=1)) + if not cost_centers: + cost_centers = { + self.cost_center: 100 + } + + self.employee_cost_centers.setdefault(employee, cost_centers) + + return self.employee_cost_centers.get(employee, {}) + def get_account(self, component_dict = None): account_dict = {} for key, amount in component_dict.items(): diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index c6f3897288..a3bc164950 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -120,8 +120,7 @@ class TestPayrollEntry(unittest.TestCase): employee1 = make_employee("test_employee1@example.com", payroll_cost_center="_Test Cost Center - _TC", department="cc - _TC", company="_Test Company") - employee2 = make_employee("test_employee2@example.com", payroll_cost_center="_Test Cost Center 2 - _TC", - department="cc - _TC", company="_Test Company") + employee2 = make_employee("test_employee2@example.com", department="cc - _TC", company="_Test Company") if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"): create_account(account_name="_Test Payroll Payable", @@ -132,8 +131,26 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC") currency=frappe.db.get_value("Company", "_Test Company", "default_currency") - make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) - make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) + + ss1 = make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) + ss2 = make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) + + # update cost centers in salary structure assignment for employee2 + ssa = frappe.db.get_value("Salary Structure Assignment", + {"employee": employee2, "salary_structure": ss2.name, "docstatus": 1}, 'name') + + ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa) + ssa_doc.payroll_cost_centers = [] + ssa_doc.append("payroll_cost_centers", { + "cost_center": "_Test Cost Center - _TC", + "percentage": 60 + }) + ssa_doc.append("payroll_cost_centers", { + "cost_center": "_Test Cost Center 2 - _TC", + "percentage": 40 + }) + + ssa_doc.save() dates = get_start_end_dates('Monthly', nowdate()) if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}): @@ -148,10 +165,10 @@ class TestPayrollEntry(unittest.TestCase): """, je) expected_je = ( ('_Test Payroll Payable - _TC', 'Main - _TC', 0.0, 155600.0), - ('Salary - _TC', '_Test Cost Center - _TC', 78000.0, 0.0), - ('Salary - _TC', '_Test Cost Center 2 - _TC', 78000.0, 0.0), - ('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 200.0), - ('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 200.0) + ('Salary - _TC', '_Test Cost Center - _TC', 124800.0, 0.0), + ('Salary - _TC', '_Test Cost Center 2 - _TC', 31200.0, 0.0), + ('Salary Deductions - _TC', '_Test Cost Center - _TC', 0.0, 320.0), + ('Salary Deductions - _TC', '_Test Cost Center 2 - _TC', 0.0, 80.0) ) self.assertEqual(je_entries, expected_je) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json index 7a80e69374..4e40e13be0 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.json +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json @@ -12,7 +12,6 @@ "department", "designation", "branch", - "payroll_cost_center", "column_break1", "status", "journal_entry", @@ -462,15 +461,6 @@ "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", @@ -647,7 +637,7 @@ "idx": 9, "is_submittable": 1, "links": [], - "modified": "2021-10-08 11:47:47.098248", + "modified": "2021-12-23 11:47:47.098248", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Slip", diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py index ae83c046a5..4cbf9484bd 100644 --- a/erpnext/payroll/doctype/salary_structure/salary_structure.py +++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py @@ -167,15 +167,12 @@ def make_salary_slip(source_name, target_doc = None, employee = None, as_print = def postprocess(source, target): if employee: employee_details = frappe.db.get_value("Employee", employee, - ["employee_name", "branch", "designation", "department", "payroll_cost_center"], as_dict=1) + ["employee_name", "branch", "designation", "department"], as_dict=1) target.employee = employee target.employee_name = employee_details.employee_name target.branch = employee_details.branch target.designation = employee_details.designation target.department = employee_details.department - target.payroll_cost_center = employee_details.payroll_cost_center - if not target.payroll_cost_center and target.department: - target.payroll_cost_center = frappe.db.get_value("Department", target.department, "payroll_cost_center") target.run_method('process_salary_structure', for_preview=for_preview) diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index 6cd897e95d..f6cb503881 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -40,28 +40,29 @@ frappe.ui.form.on('Salary Structure Assignment', { } } }); + + frm.set_query("cost_center", "payroll_cost_centers", function() { + return { + filters: { + "company": frm.doc.company, + "is_group": 0 + } + } + }); }, employee: function(frm) { if(frm.doc.employee){ frappe.call({ - method: "frappe.client.get_value", - args:{ - doctype: "Employee", - fieldname: "company", - filters:{ - name: frm.doc.employee - } - }, + method: "set_payroll_cost_centers", + doc: frm.doc, callback: function(data) { - if(data.message){ - frm.set_value("company", data.message.company); - } + refresh_field("payroll_cost_centers"); } }); } else{ - frm.set_value("company", null); + frm.set_value("payroll_cost_centers", []); } }, diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json index c8b98e5aaf..197ab5f25b 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json @@ -22,7 +22,9 @@ "base", "column_break_9", "variable", - "amended_from" + "amended_from", + "section_break_17", + "payroll_cost_centers" ], "fields": [ { @@ -90,7 +92,8 @@ }, { "fieldname": "section_break_7", - "fieldtype": "Section Break" + "fieldtype": "Section Break", + "label": "Base & Variable" }, { "fieldname": "base", @@ -141,14 +144,29 @@ "fieldtype": "Link", "label": "Payroll Payable Account", "options": "Account" + }, + { + "collapsible": 1, + "depends_on": "employee", + "fieldname": "section_break_17", + "fieldtype": "Section Break", + "label": "Payroll Cost Centers" + }, + { + "allow_on_submit": 1, + "fieldname": "payroll_cost_centers", + "fieldtype": "Table", + "label": "Cost Centers", + "options": "Employee Cost Center" } ], "is_submittable": 1, "links": [], - "modified": "2021-03-31 22:44:46.267974", + "modified": "2021-12-23 17:28:09.794444", "modified_by": "Administrator", "module": "Payroll", "name": "Salary Structure Assignment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -193,6 +211,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index e1ff9ca9f0..a7cee453ac 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate +from frappe.utils import getdate, flt class DuplicateAssignment(frappe.ValidationError): pass @@ -15,6 +15,10 @@ class SalaryStructureAssignment(Document): self.validate_dates() self.validate_income_tax_slab() self.set_payroll_payable_account() + if not self.get("payroll_cost_centers"): + self.set_payroll_cost_centers() + + self.validate_cost_center_distribution() def validate_dates(self): joining_date, relieving_date = frappe.db.get_value("Employee", self.employee, @@ -50,6 +54,31 @@ class SalaryStructureAssignment(Document): "account_name": _("Payroll Payable"), "company": self.company, "account_currency": frappe.db.get_value( "Company", self.company, "default_currency"), "is_group": 0}) self.payroll_payable_account = payroll_payable_account + + @frappe.whitelist() + def set_payroll_cost_centers(self): + self.payroll_cost_centers = [] + default_payroll_cost_center = self.get_payroll_cost_center() + if default_payroll_cost_center: + self.append("payroll_cost_centers", { + "cost_center": default_payroll_cost_center, + "percentage": 100 + }) + + def get_payroll_cost_center(self): + payroll_cost_center = frappe.db.get_value("Employee", self.employee, "payroll_cost_center") + if not payroll_cost_center and self.department: + payroll_cost_center = frappe.db.get_value("Department", self.department, "payroll_cost_center") + + return payroll_cost_center + + def validate_cost_center_distribution(self): + if self.get("payroll_cost_centers"): + total_percentage = sum([flt(d.percentage) for d in self.get("payroll_cost_centers", [])]) + if total_percentage != 100: + frappe.throw(_("Total percentage against cost centers should be 100")) + + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: From f867f1974a7b34afda4d44e74297fb1dc3f09ca1 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 24 Dec 2021 17:34:34 +0530 Subject: [PATCH 2/4] fix: get fallback cost center from Employee/Department --- erpnext/hr/doctype/department/department.js | 2 +- erpnext/hr/doctype/employee/employee.js | 2 +- erpnext/payroll/doctype/payroll_entry/payroll_entry.py | 8 +++++++- .../payroll/doctype/payroll_entry/test_payroll_entry.py | 6 +++--- .../salary_structure_assignment.js | 6 +++--- 5 files changed, 15 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/department/department.js b/erpnext/hr/doctype/department/department.js index eb759a323e..46cfbdad56 100644 --- a/erpnext/hr/doctype/department/department.js +++ b/erpnext/hr/doctype/department/department.js @@ -13,7 +13,7 @@ frappe.ui.form.on('Department', { "company": frm.doc.company, "is_group": 0 } - } + }; }); }, refresh: function(frm) { diff --git a/erpnext/hr/doctype/employee/employee.js b/erpnext/hr/doctype/employee/employee.js index 4181e0f8bb..8c73e9c9c5 100755 --- a/erpnext/hr/doctype/employee/employee.js +++ b/erpnext/hr/doctype/employee/employee.js @@ -54,7 +54,7 @@ frappe.ui.form.on('Employee', { "company": frm.doc.company, "is_group": 0 } - } + }; }); }, onload: function (frm) { diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 6aa11bc117..f61e68896b 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -239,8 +239,14 @@ class PayrollEntry(Document): cost_centers = dict(frappe.get_all("Employee Cost Center", {"parent": ss_assignment_name}, ["cost_center", "percentage"], as_list=1)) if not cost_centers: + default_cost_center, department = frappe.get_cached_value("Employee", employee, ["payroll_cost_center", "department"]) + if not default_cost_center and department: + default_cost_center = frappe.get_cached_value("Department", department, "payroll_cost_center") + if not default_cost_center: + default_cost_center = self.cost_center + cost_centers = { - self.cost_center: 100 + default_cost_center: 100 } self.employee_cost_centers.setdefault(employee, cost_centers) diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index a3bc164950..e88a2ca9ed 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -132,12 +132,12 @@ class TestPayrollEntry(unittest.TestCase): "_Test Payroll Payable - _TC") currency=frappe.db.get_value("Company", "_Test Company", "default_currency") - ss1 = make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) - ss2 = make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) + ss = make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) # update cost centers in salary structure assignment for employee2 ssa = frappe.db.get_value("Salary Structure Assignment", - {"employee": employee2, "salary_structure": ss2.name, "docstatus": 1}, 'name') + {"employee": employee2, "salary_structure": ss.name, "docstatus": 1}, 'name') ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa) ssa_doc.payroll_cost_centers = [] diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js index f6cb503881..220bfbfd65 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.js @@ -47,12 +47,12 @@ frappe.ui.form.on('Salary Structure Assignment', { "company": frm.doc.company, "is_group": 0 } - } + }; }); }, employee: function(frm) { - if(frm.doc.employee){ + if (frm.doc.employee) { frappe.call({ method: "set_payroll_cost_centers", doc: frm.doc, @@ -61,7 +61,7 @@ frappe.ui.form.on('Salary Structure Assignment', { } }); } - else{ + else { frm.set_value("payroll_cost_centers", []); } }, From 8226ea65c81b432dcd71e9eaa872e15c7c729cde Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 28 Dec 2021 08:54:05 +0530 Subject: [PATCH 3/4] fix: linter issues --- .../patches/v14_0/set_payroll_cost_centers.py | 49 ++++++++++--------- .../employee_cost_center.py | 1 + .../doctype/payroll_entry/payroll_entry.py | 4 +- .../payroll_entry/test_payroll_entry.py | 4 +- .../salary_structure_assignment.py | 10 ++-- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/erpnext/patches/v14_0/set_payroll_cost_centers.py b/erpnext/patches/v14_0/set_payroll_cost_centers.py index db5baa5d20..89b305bb6f 100644 --- a/erpnext/patches/v14_0/set_payroll_cost_centers.py +++ b/erpnext/patches/v14_0/set_payroll_cost_centers.py @@ -1,31 +1,32 @@ import frappe + def execute(): - frappe.reload_doc('payroll', 'doctype', 'employee_cost_center') - frappe.reload_doc('payroll', 'doctype', 'salary_structure_assignment') + frappe.reload_doc('payroll', 'doctype', 'employee_cost_center') + frappe.reload_doc('payroll', 'doctype', 'salary_structure_assignment') + + employees = frappe.get_all("Employee", fields=["department", "payroll_cost_center", "name"]) - employees = frappe.get_all("Employee", fields=["department", "payroll_cost_center", "name"]) + employee_cost_center = {} + for d in employees: + cost_center = d.payroll_cost_center + if not cost_center and d.department: + cost_center = frappe.get_cached_value("Department", d.department, "payroll_cost_center") - employee_cost_center = {} - for d in employees: - cost_center = d.payroll_cost_center - if not cost_center and d.department: - cost_center = frappe.get_cached_value("Department", d.department, "payroll_cost_center") + if cost_center: + employee_cost_center.setdefault(d.name, cost_center) - if cost_center: - employee_cost_center.setdefault(d.name, cost_center) - - salary_structure_assignments = frappe.get_all("Salary Structure Assignment", - filters = {"docstatus": ["!=", 2]}, - fields=["name", "employee"]) + salary_structure_assignments = frappe.get_all("Salary Structure Assignment", + filters = {"docstatus": ["!=", 2]}, + fields=["name", "employee"]) - for d in salary_structure_assignments: - cost_center = employee_cost_center.get(d.employee) - if cost_center: - assignment = frappe.get_doc("Salary Structure Assignment", d.name) - if not assignment.get("payroll_cost_centers"): - assignment.append("payroll_cost_centers", { - "cost_center": cost_center, - "percentage": 100 - }) - assignment.save() \ No newline at end of file + for d in salary_structure_assignments: + cost_center = employee_cost_center.get(d.employee) + if cost_center: + assignment = frappe.get_doc("Salary Structure Assignment", d.name) + if not assignment.get("payroll_cost_centers"): + assignment.append("payroll_cost_centers", { + "cost_center": cost_center, + "percentage": 100 + }) + assignment.save() \ No newline at end of file diff --git a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py index 91bcc21d18..6c5be9744b 100644 --- a/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py +++ b/erpnext/payroll/doctype/employee_cost_center/employee_cost_center.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class EmployeeCostCenter(Document): pass diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index f61e68896b..a9a95546fa 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -234,7 +234,7 @@ class PayrollEntry(Document): if not self.employee_cost_centers.get(employee): ss_assignment_name = frappe.db.get_value("Salary Structure Assignment", {"employee": employee, "salary_structure": salary_structure, "docstatus": 1}, 'name') - + if ss_assignment_name: cost_centers = dict(frappe.get_all("Employee Cost Center", {"parent": ss_assignment_name}, ["cost_center", "percentage"], as_list=1)) @@ -244,7 +244,7 @@ class PayrollEntry(Document): default_cost_center = frappe.get_cached_value("Department", department, "payroll_cost_center") if not default_cost_center: default_cost_center = self.cost_center - + cost_centers = { default_cost_center: 100 } diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py index e88a2ca9ed..4f097fa2c3 100644 --- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py @@ -131,14 +131,14 @@ class TestPayrollEntry(unittest.TestCase): frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC") currency=frappe.db.get_value("Company", "_Test Company", "default_currency") - + make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False) ss = make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False) # update cost centers in salary structure assignment for employee2 ssa = frappe.db.get_value("Salary Structure Assignment", {"employee": employee2, "salary_structure": ss.name, "docstatus": 1}, 'name') - + ssa_doc = frappe.get_doc("Salary Structure Assignment", ssa) ssa_doc.payroll_cost_centers = [] ssa_doc.append("payroll_cost_centers", { diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py index a7cee453ac..8359478d0b 100644 --- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py +++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import getdate, flt +from frappe.utils import flt, getdate class DuplicateAssignment(frappe.ValidationError): pass @@ -54,7 +54,7 @@ class SalaryStructureAssignment(Document): "account_name": _("Payroll Payable"), "company": self.company, "account_currency": frappe.db.get_value( "Company", self.company, "default_currency"), "is_group": 0}) self.payroll_payable_account = payroll_payable_account - + @frappe.whitelist() def set_payroll_cost_centers(self): self.payroll_cost_centers = [] @@ -69,7 +69,7 @@ class SalaryStructureAssignment(Document): payroll_cost_center = frappe.db.get_value("Employee", self.employee, "payroll_cost_center") if not payroll_cost_center and self.department: payroll_cost_center = frappe.db.get_value("Department", self.department, "payroll_cost_center") - + return payroll_cost_center def validate_cost_center_distribution(self): @@ -77,8 +77,7 @@ class SalaryStructureAssignment(Document): total_percentage = sum([flt(d.percentage) for d in self.get("payroll_cost_centers", [])]) if total_percentage != 100: frappe.throw(_("Total percentage against cost centers should be 100")) - - + def get_assigned_salary_structure(employee, on_date): if not employee or not on_date: @@ -93,6 +92,7 @@ def get_assigned_salary_structure(employee, on_date): }) return salary_structure[0][0] if salary_structure else None + @frappe.whitelist() def get_employee_currency(employee): employee_currency = frappe.db.get_value('Salary Structure Assignment', {'employee': employee}, 'currency') From baa12bcee613b7bf03853fa78abc1eef91094917 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Tue, 28 Dec 2021 10:04:14 +0530 Subject: [PATCH 4/4] fix: convert raw queries with frappe ORM --- .../doctype/payroll_entry/payroll_entry.py | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index a9a95546fa..5bb32cf909 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -7,6 +7,7 @@ from dateutil.relativedelta import relativedelta from frappe import _ from frappe.desk.reportview import get_filters_cond, get_match_cond from frappe.model.document import Document +from frappe.query_builder.functions import Coalesce from frappe.utils import ( DATE_FORMAT, add_days, @@ -157,11 +158,20 @@ class PayrollEntry(Document): Returns list of salary slips based on selected criteria """ - ss_list = frappe.db.sql(""" - select t1.name, t1.salary_structure from `tabSalary Slip` t1 - where t1.docstatus = %s and t1.start_date >= %s and t1.end_date <= %s and t1.payroll_entry = %s - and (t1.journal_entry is null or t1.journal_entry = "") and ifnull(salary_slip_based_on_timesheet,0) = %s - """, (ss_status, self.start_date, self.end_date, self.name, self.salary_slip_based_on_timesheet), as_dict=as_dict) + ss = frappe.qb.DocType("Salary Slip") + ss_list = ( + frappe.qb.from_(ss) + .select(ss.name, ss.salary_structure) + .where( + (ss.docstatus == ss_status) + & (ss.start_date >= self.start_date) + & (ss.end_date <= self.end_date) + & (ss.payroll_entry == self.name) + & ((ss.journal_entry.isnull()) | (ss.journal_entry == "")) + & (Coalesce(ss.salary_slip_based_on_timesheet, 0) == self.salary_slip_based_on_timesheet) + ) + ).run(as_dict=as_dict) + return ss_list @frappe.whitelist() @@ -190,19 +200,20 @@ class PayrollEntry(Document): def get_salary_components(self, component_type): salary_slips = self.get_sal_slip_list(ss_status = 1, as_dict = True) + if salary_slips: - salary_components = frappe.db.sql(""" - SELECT - ssd.salary_component, ssd.amount, ssd.parentfield, ss.salary_structure, ss.employee - FROM - `tabSalary Slip` ss, - `tabSalary Detail` ssd - WHERE - ss.name = ssd.parent - and ssd.parentfield = '%s' - and ss.name in (%s) - """ % (component_type, ', '.join(['%s']*len(salary_slips))), - tuple([d.name for d in salary_slips]), as_dict=True) + ss = frappe.qb.DocType("Salary Slip") + ssd = frappe.qb.DocType("Salary Detail") + salary_components = ( + frappe.qb.from_(ss) + .join(ssd) + .on(ss.name == ssd.parent) + .select(ssd.salary_component, ssd.amount, ssd.parentfield, ss.salary_structure, ss.employee) + .where( + (ssd.parentfield == component_type) + & (ss.name.isin(tuple([d.name for d in salary_slips]))) + ) + ).run(as_dict=True) return salary_components