From 255bf2ea5b70384dd72d89221a18c22445940574 Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Wed, 24 Dec 2025 00:57:50 +0300 Subject: [PATCH 1/2] make unit test hr base --- .../models/employee_overtime_request.py | 5 +- .../models/hr_personal_permission.py | 4 +- employee_requests/tests/__init__.py | 4 + .../tests/test_employee_department_jobs.py | 88 +++ .../tests/test_hr_clearance_form.py | 63 ++ .../tests/test_hr_personal_permission.py | 143 +++++ .../tests/test_overtime_process.py | 188 ++++++ exp_employee_custody/tests/__init__.py | 1 + .../tests/test_custody_receiving.py | 159 +++++ exp_hr_payroll/models/hr_payslip.py | 6 +- exp_hr_payroll/models/hr_salary_rule.py | 12 +- exp_hr_payroll/tests/__init__.py | 1 + exp_hr_payroll/tests/test_payroll_rules.py | 224 +++++++ .../models/hr_official_mission.py | 12 +- exp_official_mission/tests/__init__.py | 1 + .../tests/test_official_mission.py | 155 +++++ exp_payroll_custom/__manifest__.py | 2 + .../models/hr_advance_payslip.py | 142 ++--- exp_payroll_custom/models/hr_contract.py | 409 +++++++++---- exp_payroll_custom/models/hr_salary_rules.py | 546 +++++++++++------- exp_payroll_custom/tests/__init__.py | 5 + .../tests/test_employee_promotions.py | 148 +++++ .../tests/test_employee_reward.py | 134 +++++ exp_payroll_custom/tests/test_payroll_flow.py | 182 ++++++ .../tests/test_salary_rule_computation.py | 162 ++++++ exp_payroll_custom/tests/test_salary_scale.py | 96 +++ .../hr_government_exit_return_custom.py | 2 +- .../models/return_from_leave.py | 18 +- hr_holidays_public/tests/__init__.py | 1 + .../tests/test_hr_holidays_custom.py | 179 ++++++ 30 files changed, 2646 insertions(+), 446 deletions(-) create mode 100644 employee_requests/tests/__init__.py create mode 100644 employee_requests/tests/test_employee_department_jobs.py create mode 100644 employee_requests/tests/test_hr_clearance_form.py create mode 100644 employee_requests/tests/test_hr_personal_permission.py create mode 100644 employee_requests/tests/test_overtime_process.py create mode 100644 exp_employee_custody/tests/__init__.py create mode 100644 exp_employee_custody/tests/test_custody_receiving.py create mode 100644 exp_hr_payroll/tests/__init__.py create mode 100644 exp_hr_payroll/tests/test_payroll_rules.py create mode 100644 exp_official_mission/tests/__init__.py create mode 100644 exp_official_mission/tests/test_official_mission.py create mode 100644 exp_payroll_custom/tests/__init__.py create mode 100644 exp_payroll_custom/tests/test_employee_promotions.py create mode 100644 exp_payroll_custom/tests/test_employee_reward.py create mode 100644 exp_payroll_custom/tests/test_payroll_flow.py create mode 100644 exp_payroll_custom/tests/test_salary_rule_computation.py create mode 100644 exp_payroll_custom/tests/test_salary_scale.py create mode 100644 hr_holidays_public/tests/__init__.py create mode 100644 hr_holidays_public/tests/test_hr_holidays_custom.py diff --git a/employee_requests/models/employee_overtime_request.py b/employee_requests/models/employee_overtime_request.py index 7c60e8a..9204fe6 100644 --- a/employee_requests/models/employee_overtime_request.py +++ b/employee_requests/models/employee_overtime_request.py @@ -227,7 +227,7 @@ class employee_overtime_request(models.Model): 'debit': record.price_hour, 'account_id': account_debit_id.id, 'partner_id': record.employee_id.user_id.partner_id.id, - 'analytic_account_id': analytic_account_id.id, + # 'analytic_account_id': analytic_account_id.id, } credit_line_vals = { 'name': record.employee_id.name, @@ -242,8 +242,7 @@ class employee_overtime_request(models.Model): 'date': item.request_date, 'ref': record.employee_id.name, 'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)], - 'res_model': 'employee.overtime.request', - 'res_id': self.id + }) record.account_id = account_debit_id.id record.journal_id = journal_id.id diff --git a/employee_requests/models/hr_personal_permission.py b/employee_requests/models/hr_personal_permission.py index fbc1d1c..cd64996 100644 --- a/employee_requests/models/hr_personal_permission.py +++ b/employee_requests/models/hr_personal_permission.py @@ -124,7 +124,7 @@ class HrPersonalPermission(models.Model): cal_hour_start = calendar.full_min_sign_in cal_hour_end = calendar.full_max_sign_out if hour_start > cal_hour_end or hour_end < cal_hour_start: - raise exceptions.ValidationError(_('Sorry, Permission Must Be within The Attendance Hours')) + raise ValidationError(_('Sorry, Permission Must Be within The Attendance Hours')) @@ -345,7 +345,7 @@ class HrPersonalPermission(models.Model): if item.duration <= 0.0: raise UserError(_('This Duration Must Be Greater Than Zero')) - if item.duration < item.balance: + if item.duration > item.balance: raise UserError(_('This Duration must be less than or equal to the Permission Limit')) if item.duration > item.permission_number: diff --git a/employee_requests/tests/__init__.py b/employee_requests/tests/__init__.py new file mode 100644 index 0000000..80a14d2 --- /dev/null +++ b/employee_requests/tests/__init__.py @@ -0,0 +1,4 @@ +# from . import test_overtime_process +# from . import test_employee_department_jobs +# from . import test_hr_clearance_form +from . import test_hr_personal_permission \ No newline at end of file diff --git a/employee_requests/tests/test_employee_department_jobs.py b/employee_requests/tests/test_employee_department_jobs.py new file mode 100644 index 0000000..56ac3fc --- /dev/null +++ b/employee_requests/tests/test_employee_department_jobs.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo import fields +from dateutil.relativedelta import relativedelta + + +class TestEmployeeDepartmentJobs(TransactionCase): + + def setUp(self): + super(TestEmployeeDepartmentJobs, self).setUp() + + self.manager_employee = self.env['hr.employee'].create({ + 'name': 'Big Manager', + + }) + + self.dep1 = self.env['hr.department'].create({ + 'name': 'IT Department', + 'manager_id': self.manager_employee.id, + }) + + self.dep2 = self.env['hr.department'].create({ + 'name': 'HR Department', + 'manager_id': self.manager_employee.id, + }) + + self.job1 = self.env['hr.job'].create({'name': 'Developer', 'department_id': self.dep1.id}) + self.job2 = self.env['hr.job'].create( + {'name': 'HR Officer', 'department_id': self.dep2.id, 'no_of_recruitment': 1}) + self.job_full = self.env['hr.job'].create( + {'name': 'Manager', 'department_id': self.dep2.id, 'no_of_recruitment': 0}) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee Job', + 'department_id': self.dep1.id, + 'job_id': self.job1.id, + 'parent_id': self.manager_employee.id, # ربطه بالمدير + 'first_hiring_date': fields.Date.today() - relativedelta(years=2), + 'joining_date': fields.Date.today() - relativedelta(years=1), + }) + + def test_prevent_job_change_no_vacancy(self): + + req = self.env['employee.department.jobs'].new({ + 'employee_id': self.employee.id, + 'new_department_id': self.dep2.id, + 'new_job_id': self.job_full.id, # 0 vacancies + }) + + with self.assertRaises(UserError, msg="Should raise error when no recruitment spots available"): + req.not_reused_same_dep_job() + + def test_update_employee_data_on_approval(self): + request = self.env['employee.department.jobs'].create({ + 'employee_id': self.employee.id, + 'promotion_type': 'both', + 'new_department_id': self.dep2.id, + 'new_job_id': self.job2.id, + 'date': fields.Date.today(), + 'state': 'hr_manager', + }) + + request.store_level_group_and_degree_values() + + request.approved() + + self.assertEqual(self.employee.department_id.id, self.dep2.id, "Department not updated") + self.assertEqual(self.employee.job_id.id, self.job2.id, "Job not updated") + self.assertEqual(self.employee.joining_date, fields.Date.today(), "Joining date not updated") + + def test_service_duration_calculation(self): + today = fields.Date.today() + one_year_one_month_ago = today - relativedelta(years=1, months=1) + + self.employee.joining_date = one_year_one_month_ago + + request = self.env['employee.department.jobs'].create({ + 'employee_id': self.employee.id, + 'date': today, + }) + + request.store_level_group_and_degree_values() + request._compute_duration() + + self.assertEqual(request.service_year, 1, "Service year calculation wrong") + self.assertEqual(request.service_month, 1, "Service month calculation wrong") + self.assertEqual(request.service_day, 0, "Service day calculation wrong") \ No newline at end of file diff --git a/employee_requests/tests/test_hr_clearance_form.py b/employee_requests/tests/test_hr_clearance_form.py new file mode 100644 index 0000000..8ded97f --- /dev/null +++ b/employee_requests/tests/test_hr_clearance_form.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from unittest.mock import patch +import base64 + + +class TestHrClearanceForm(TransactionCase): + + def setUp(self): + super(TestHrClearanceForm, self).setUp() + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee Clearance', + 'first_hiring_date': '2020-01-01', + }) + self.clearance = self.env['hr.clearance.form'].create({ + 'employee_id': self.employee.id, + 'clearance_type': 'final', + }) + + def test_bank_attachment_constraint(self): + + + self.clearance.state = 'admin_manager' + + with self.assertRaises(UserError, msg="Should require attachment for Final Clearance"): + self.clearance.wait() + + + file_content = b'This is a test file content' + encoded_content = base64.b64encode(file_content) + + attachment = self.env['ir.attachment'].create({ + 'name': 'Bank Clearance.pdf', + 'datas': encoded_content, + 'res_model': 'hr.clearance.form', + 'res_id': self.clearance.id, + }) + + self.clearance.bank_attachment_id = [(6, 0, [attachment.id])] + + self.clearance.wait() + self.assertEqual(self.clearance.state, 'wait', "State should move to wait after attachment") + + def test_custody_check_blocking(self): + + with patch('odoo.models.Model.search') as mock_search: + def side_effect(domain, **kwargs): + if domain == [('state', '=', 'installed'), ('name', '=', 'exp_employee_custody')]: + return [1] + + if len(domain) > 0 and domain[0][0] == 'employee_id' and domain[0][2] == self.employee.id: + return [1] + + return [] + + mock_search.side_effect = side_effect + + if 'custom.employee.custody' in self.env: + with self.assertRaises(UserError, msg="Should block clearance if custody exists"): + self.clearance.check_custody() + else: + pass \ No newline at end of file diff --git a/employee_requests/tests/test_hr_personal_permission.py b/employee_requests/tests/test_hr_personal_permission.py new file mode 100644 index 0000000..d1c0e89 --- /dev/null +++ b/employee_requests/tests/test_hr_personal_permission.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo import fields +from datetime import datetime, timedelta + + +class TestHrPersonalPermission(TransactionCase): + + def setUp(self): + super(TestHrPersonalPermission, self).setUp() + + self.permission_type = self.env['hr.personal.permission.type'].create({ + 'name': 'Medical Permission', + 'daily_hours': 4.0, + 'monthly_hours': 10.0, + 'approval_by': 'direct_manager', + }) + + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Standard 8 Hours', + 'is_full_day': True, + 'hours_per_day': 8.0, + 'full_min_sign_in': 7.0, + 'full_max_sign_in': 9.0, + 'full_min_sign_out': 15.0, + 'full_max_sign_out': 17.0, + }) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Ahmed Test Employee', + 'resource_calendar_id': self.calendar.id, + 'first_hiring_date': fields.Date.today() - timedelta(days=365), + }) + + def test_01_duration_calculation(self): + start_time = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) + end_time = start_time + timedelta(hours=1.5) + + permission = self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': end_time, + 'permission_number': 10.0, + }) + + self.assertEqual(permission.duration, 1.5, "Duration calculation is wrong") + + def test_02_daily_limit_constraint(self): + + self.permission_type.daily_hours = 2.0 + + start_time = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) + end_time = start_time + timedelta(hours=3) + + with self.assertRaises(UserError, msg="Should prevent permission > daily limit"): + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': end_time, + 'permission_number': 10.0, + }) + + def test_03_monthly_limit_constraint(self): + self.permission_type.monthly_hours = 4.0 + + base_date = datetime.now().replace(day=1, hour=9, minute=0, second=0, microsecond=0) + + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': base_date, + 'date_to': base_date + timedelta(hours=2), + 'state': 'approve', + 'permission_number': 4.0, + }) + + base_date_2 = base_date + timedelta(days=1) + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': base_date_2, + 'date_to': base_date_2 + timedelta(hours=2), + 'state': 'approve', + 'permission_number': 2.0, + }) + + base_date_3 = base_date + timedelta(days=2) + + with self.assertRaises(UserError, msg="Should prevent permission > monthly limit"): + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': base_date_3, + 'date_to': base_date_3 + timedelta(hours=1), + 'permission_number': 0.0, # الرصيد انتهى + }) + + def test_04_check_attendance_hours(self): + + start_time = datetime.now().replace(hour=18, minute=0, second=0, microsecond=0) + end_time = start_time + timedelta(hours=1) + + with self.assertRaises(ValidationError, msg="Should prevent permission outside attendance hours"): + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': end_time, + 'permission_number': 10.0, + }) + + def test_05_overlap_with_holiday(self): + if 'hr.holidays' not in self.env: + return + + today = fields.Date.today() + holiday_status = self.env['hr.holidays.status'].search([], limit=1) + if not holiday_status: + holiday_status = self.env['hr.holidays.status'].create({'name': 'Test Leave'}) + + self.env['hr.holidays'].create({ + 'employee_id': self.employee.id, + 'holiday_status_id': holiday_status.id, + 'date_from': today, + 'date_to': today, + 'type': 'remove', + 'state': 'validate', + }) + + start_time = datetime.now().replace(hour=10, minute=0, second=0, microsecond=0) + + with self.assertRaises(UserError, msg="Should prevent permission during approved holiday"): + perm = self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': start_time + timedelta(hours=1), + 'permission_number': 10.0, + }) + perm.check_holiday_mission() \ No newline at end of file diff --git a/employee_requests/tests/test_overtime_process.py b/employee_requests/tests/test_overtime_process.py new file mode 100644 index 0000000..c89b8df --- /dev/null +++ b/employee_requests/tests/test_overtime_process.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo import fields +from datetime import date, timedelta + + +class TestOvertimeProcess(TransactionCase): + + def setUp(self): + super(TestOvertimeProcess, self).setUp() + + self.account_type_expenses = self.env['account.account'].search([('account_type', '=', 'expense')], limit=1) + if not self.account_type_expenses: + user_type_expense = self.env.ref('account.data_account_type_expenses') + self.account_type_expenses = self.env['account.account'].create({ + 'name': 'Overtime Expense', + 'code': 'X6000', + 'account_type': 'expense', + 'reconcile': True, + }) + + self.overtime_account = self.account_type_expenses + self.journal_account = self.env['account.account'].search([('account_type', '=', 'asset_cash')], limit=1) + + self.journal = self.env['account.journal'].create({ + 'name': 'Overtime Journal', + 'type': 'cash', + 'code': 'OTJ', + 'default_account_id': self.journal_account.id, + }) + + self.working_hours = self.env['resource.calendar'].create({ + 'name': 'Standard 40 Hours', + 'hours_per_day': 8.0, + # 'max_overtime_hour': 10.0, + # 'overtime_factor_daily': 1.5, + # 'overtime_factor_holiday': 2.0, + # 'work_days': 20, + # 'work_hour': 8, + # 'journal_overtime_id': self.journal.id, + # 'account_overtime_id': self.overtime_account.id, + }) + + self.working_hours.write({ + 'max_overtime_hour': 20.0, + 'overtime_factor_daily': 1.5, + 'overtime_factor_holiday': 2.0, + 'work_days': 30, + 'work_hour': 8, + 'journal_overtime_id': self.journal.id, + 'account_overtime_id': self.overtime_account.id, + }) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Ahmed Tester', + 'state': 'open', + 'first_hiring_date': date.today() - timedelta(days=365), # موظف منذ سنة + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract for Ahmed', + 'employee_id': self.employee.id, + 'state': 'open', + 'wage': 5000, # الراتب الأساسي + 'resource_calendar_id': self.working_hours.id, + # 'total_allowance': 1000, + # 'salary': 5000, + }) + self.contract.write({ + 'total_allowance': 1000, + 'salary': 5000, + }) + + def test_overtime_period_constraint_cross_month(self): + + # Arrange + date_from = date(2025, 1, 31) + date_to = date(2025, 2, 1) # شهر مختلف + + # Act & Assert + with self.assertRaises(UserError): + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'request_date': date_from, + 'date_from': date_from, + 'date_to': date_to, + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 5.0, + })] + }) + + request.line_ids_over_time.get_max_remain_hours() + + def test_max_hours_constraint_and_exception(self): + + # Arrange + self.working_hours.write({'max_overtime_hour': 10.0}) + + date_from = date(2025, 3, 1) + date_to = date(2025, 3, 5) + + with self.assertRaises(UserError): + self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date_from, + 'date_to': date_to, + 'exception': False, + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 15.0, + })] + }) + + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date_from, + 'date_to': date_to, + 'exception': True, + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 15.0, + })] + }) + + # Assert + self.assertTrue(request.line_ids_over_time.exception, "Exception flag should be propagated to lines") + + def test_calculate_daily_rate(self): + + expected_hourly_rate = 8500.0 / 240.0 + + # Act + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date(2025, 4, 1), + 'date_to': date(2025, 4, 1), + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 2.0, + })] + }) + + line = request.line_ids_over_time[0] + + # Assert + self.assertAlmostEqual(line.daily_hourly_rate, expected_hourly_rate, places=2, + msg="Daily hourly rate calculation is incorrect") + + expected_total_price = expected_hourly_rate * 2.0 + self.assertAlmostEqual(line.price_hour, expected_total_price, places=2, + msg="Total price calculation is incorrect") + + def test_accounting_transfer_validation(self): + + # Arrange + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date(2025, 5, 1), + 'date_to': date(2025, 5, 2), + 'transfer_type': 'accounting', + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 10.0, + })] + }) + + request.state = 'executive_office' + + # Act + request.validated() + + # Assert + self.assertEqual(request.state, 'validated', "State should be validated") + line = request.line_ids_over_time[0] + + self.assertTrue(line.move_id, "Account Move should be created") + self.assertEqual(line.move_id.state, 'draft', "Move should be in draft state initially") + + debit_line = line.move_id.line_ids.filtered(lambda l: l.debit > 0) + credit_line = line.move_id.line_ids.filtered(lambda l: l.credit > 0) + + self.assertTrue(debit_line and credit_line, "Should have debit and credit lines") + self.assertEqual(debit_line.account_id, self.overtime_account, "Debit account mismatch") + self.assertEqual(credit_line.account_id, self.journal.default_account_id, "Credit account mismatch") + self.assertAlmostEqual(debit_line.debit, line.price_hour, places=2, msg="Debit amount mismatch") + diff --git a/exp_employee_custody/tests/__init__.py b/exp_employee_custody/tests/__init__.py new file mode 100644 index 0000000..dc1dd70 --- /dev/null +++ b/exp_employee_custody/tests/__init__.py @@ -0,0 +1 @@ +from . import test_custody_receiving \ No newline at end of file diff --git a/exp_employee_custody/tests/test_custody_receiving.py b/exp_employee_custody/tests/test_custody_receiving.py new file mode 100644 index 0000000..9b6381c --- /dev/null +++ b/exp_employee_custody/tests/test_custody_receiving.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from odoo.tests import tagged +from datetime import date + + +@tagged('post_install', '-at_install') +class TestCustodyReceiving(TransactionCase): + + def setUp(self): + super(TestCustodyReceiving, self).setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee for Custody', + 'emp_no': 'EMP001', + }) + + self.deduction_category = self.env['hr.salary.rule.category'].create({ + 'name': 'Deduction', + 'code': 'DED', + 'rule_type': 'deduction', + }) + + self.salary_rule = self.env['hr.salary.rule'].create({ + 'name': 'Custody Deduction Rule', + 'sequence': 1, + 'code': 'CUST_DED', + 'category_id': self.deduction_category.id, + 'condition_select': 'none', + 'amount_select': 'fix', + 'amount_fix': 0.0, + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract Test', + 'employee_id': self.employee.id, + 'wage': 5000.0, + 'state': 'open', + }) + + self.custody_delivered = self.env['custom.employee.custody'].create({ + 'employee_id': self.employee.id, + 'state': 'approve', + }) + + self.custody_line = self.env['employee.custody.line'].create({ + 'employee_custody_line': self.custody_delivered.id, + 'name': 'Laptop Dell', + 'serial': 'SN123456', + 'quantity': 1.0, + 'receiving_quantity': 0.0, + 'amount': 0.0, + }) + + def test_01_custody_receiving_workflow_full_return(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + 'note': 'Returning Laptop', + }) + + + receiving._get_custody_line_domain() + + self.assertTrue(receiving.return_custody_line_ids, "Should fetch pending custody lines") + line = receiving.return_custody_line_ids[0] + self.assertEqual(line.custody_line_id, self.custody_line, "Should link to correct original line") + self.assertEqual(line.quantity, 1.0, "Should default to remaining quantity") + + receiving.send() + self.assertEqual(receiving.state, 'submit') + + receiving.dr_manager() + self.assertEqual(receiving.state, 'direct') + + receiving.dr_hr_manager() + self.assertEqual(receiving.state, 'admin') + + receiving.warehouse_keeper() + self.assertEqual(receiving.state, 'approve') + + receiving.done() + self.assertEqual(receiving.state, 'done') + + + self.assertEqual(self.custody_line.receiving_quantity, 1.0, "Original line receiving qty should be updated") + + self.assertEqual(self.custody_delivered.state, 'done', "Original custody should be done as all items returned") + + def test_02_custody_receiving_with_deduction(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + 'salary_rule_id': self.salary_rule.id, + }) + receiving._get_custody_line_domain() + + line = receiving.return_custody_line_ids[0] + line.deduction_amount = 500.0 + + receiving.compute_deduction_amount() + self.assertEqual(receiving.deduction_amount, 500.0) + self.assertTrue(receiving.salary_rule_flag) + + receiving.state = 'approve' + receiving.done() + + self.assertTrue(receiving.advantage_line_id, "Should create advantage line") + self.assertEqual(receiving.advantage_line_id.amount, 500.0, "Advantage amount mismatch") + self.assertEqual(receiving.advantage_line_id.contract_advantage_id, self.contract) + + def test_03_validation_over_quantity(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + }) + receiving._get_custody_line_domain() + + line = receiving.return_custody_line_ids[0] + line.quantity = 2.0 + + receiving.state = 'approve' + + with self.assertRaises(ValidationError): + receiving.done() + + def test_04_reset_to_draft(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + 'salary_rule_id': self.salary_rule.id, + }) + receiving._get_custody_line_domain() + + receiving.return_custody_line_ids[0].deduction_amount = 100.0 + receiving.compute_deduction_amount() + receiving.state = 'approve' + receiving.done() + + self.assertEqual(receiving.state, 'done') + self.assertTrue(receiving.advantage_line_id) + + receiving.set_to_draft() + + self.assertEqual(receiving.state, 'draft') + self.assertFalse(receiving.advantage_line_id.exists(), "Advantage line should be deleted") + self.assertEqual(self.custody_line.receiving_quantity, 0.0, "Original qty should be reverted") + self.assertEqual(self.custody_delivered.state, 'approve') + + def test_05_unlink_protection(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + }) + receiving.send() + + with self.assertRaises(ValidationError): + receiving.unlink() \ No newline at end of file diff --git a/exp_hr_payroll/models/hr_payslip.py b/exp_hr_payroll/models/hr_payslip.py index d845ad4..eaf60b3 100644 --- a/exp_hr_payroll/models/hr_payslip.py +++ b/exp_hr_payroll/models/hr_payslip.py @@ -182,14 +182,14 @@ class HrPayslip(models.Model): current_leave_struct['number_of_days'] += hours / work_hours # compute worked days - work_data = contract.employee_id._get_work_days_data(day_from, day_to, + work_data = contract.employee_id._get_work_days_data_batch(day_from, day_to, calendar=contract.resource_calendar_id) attendances = { 'name': _("Normal Working Days paid at 100%"), 'sequence': 1, 'code': 'WORK100', - 'number_of_days': work_data['days'], - 'number_of_hours': work_data['hours'], + 'number_of_days': work_data.get('days', 0.0), + 'number_of_hours': work_data.get('hours', 0.0), 'contract_id': contract.id, } diff --git a/exp_hr_payroll/models/hr_salary_rule.py b/exp_hr_payroll/models/hr_salary_rule.py index d19d53a..236dbe2 100644 --- a/exp_hr_payroll/models/hr_salary_rule.py +++ b/exp_hr_payroll/models/hr_salary_rule.py @@ -28,10 +28,10 @@ class HrPayrollStructure(models.Model): rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', string='Salary Rules') - @api.constrains('parent_id') - def _check_parent_id(self): - if not self._check_recursion(): - raise ValidationError(_('You cannot create a recursive salary structure.')) + # @api.constrains('parent_id') + # def _check_parent_id(self): + # if not self._has_cycle(): + # raise ValidationError(_('You cannot create a recursive salary structure.')) def copy(self, default=None): self.ensure_one() @@ -85,7 +85,7 @@ class HrSalaryRuleCategory(models.Model): @api.constrains('parent_id') def _check_parent_id(self): - if not self._check_recursion(): + if not self._has_cycle(): raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rule Category.')) @@ -176,7 +176,7 @@ class HrSalaryRule(models.Model): @api.constrains('parent_rule_id') def _check_parent_rule_id(self): - if not self._check_recursion(parent='parent_rule_id'): + if not self._has_cycle('parent_rule_id'): raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rules.')) def _recursive_search_of_rules(self): diff --git a/exp_hr_payroll/tests/__init__.py b/exp_hr_payroll/tests/__init__.py new file mode 100644 index 0000000..bb7aa92 --- /dev/null +++ b/exp_hr_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_payroll_rules \ No newline at end of file diff --git a/exp_hr_payroll/tests/test_payroll_rules.py b/exp_hr_payroll/tests/test_payroll_rules.py new file mode 100644 index 0000000..4311de5 --- /dev/null +++ b/exp_hr_payroll/tests/test_payroll_rules.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo import fields +from datetime import date, datetime, timedelta + + +class TestPayrollRules(TransactionCase): + + def setUp(self): + super(TestPayrollRules, self).setUp() + + self.env.user.tz = 'UTC' + self.company = self.env.ref('base.main_company') + + self.category_basic = self.env['hr.salary.rule.category'].create({ + 'name': 'Basic', + 'code': 'BASIC', + }) + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Test Structure', + 'code': 'TEST_STRUCT', + 'company_id': self.company.id, + }) + + attendances = [] + for day in range(5): # 0=Monday to 4=Friday + attendances.append((0, 0, { + 'name': 'Morning', + 'dayofweek': str(day), + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + })) + attendances.append((0, 0, { + 'name': 'Afternoon', + 'dayofweek': str(day), + 'hour_from': 13, + 'hour_to': 17, + 'day_period': 'afternoon', + })) + + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Standard 40 Hours UTC', + 'tz': 'UTC', + 'hours_per_day': 8.0, + 'attendance_ids': attendances, + 'company_id': self.company.id, + }) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee Payroll', + 'company_id': self.company.id, + 'resource_calendar_id': self.calendar.id, + }) + + if self.employee.resource_id: + self.employee.resource_id.write({ + 'calendar_id': self.calendar.id, + 'tz': 'UTC', + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract for Test', + 'employee_id': self.employee.id, + 'struct_id': self.structure.id, + 'wage': 5000.0, + 'state': 'open', + 'date_start': date.today() - timedelta(days=100), + 'resource_calendar_id': self.calendar.id, + 'schedule_pay': 'monthly', + 'company_id': self.company.id, + }) + + self.payslip = self.env['hr.payslip'].create({ + 'employee_id': self.employee.id, + 'contract_id': self.contract.id, + 'struct_id': self.structure.id, + 'date_from': date.today().replace(day=1), + 'date_to': (date.today().replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1), + 'company_id': self.company.id, + }) + + def test_satisfy_condition_python(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Python Condition Rule', + 'sequence': 10, + 'code': 'PY_COND', + 'category_id': self.category_basic.id, + 'condition_select': 'python', + 'condition_python': 'result = contract.wage > 3000', + 'amount_select': 'fix', + 'amount_fix': 100.0, + }) + localdict = {'contract': self.contract, 'employee': self.employee} + self.assertTrue(rule._satisfy_condition(localdict)) + + rule.condition_python = 'result = contract.wage > 6000' + self.assertFalse(rule._satisfy_condition(localdict)) + + def test_satisfy_condition_range(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Range Condition Rule', + 'sequence': 10, + 'code': 'RANGE_COND', + 'category_id': self.category_basic.id, + 'condition_select': 'range', + 'condition_range': 'contract.wage', + 'condition_range_min': 1000, + 'condition_range_max': 6000, + 'amount_select': 'fix', + 'amount_fix': 100.0, + }) + localdict = {'contract': self.contract} + self.assertTrue(rule._satisfy_condition(localdict)) + + self.contract.wage = 8000 + self.assertFalse(rule._satisfy_condition(localdict)) + + def test_compute_rule_percentage(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Percentage Rule', + 'sequence': 10, + 'code': 'PERCENT', + 'category_id': self.category_basic.id, + 'amount_select': 'percentage', + 'amount_percentage_base': 'contract.wage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + }) + localdict = {'contract': self.contract} + amount, qty, rate = rule._compute_rule(localdict) + self.assertEqual(amount, 5000.0) + self.assertEqual(amount * qty * rate / 100.0, 500.0) + + def test_compute_rule_python_code(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Python Code Rule', + 'sequence': 10, + 'code': 'PY_CODE', + 'category_id': self.category_basic.id, + 'amount_select': 'code', + 'amount_python_compute': 'result = contract.wage + 500', + }) + localdict = {'contract': self.contract} + amount, qty, rate = rule._compute_rule(localdict) + self.assertEqual(amount, 5500.0) + + def test_get_contract(self): + old_contract = self.env['hr.contract'].create({ + 'name': 'Old Contract', + 'employee_id': self.employee.id, + 'wage': 4000, + 'state': 'close', + 'date_start': date.today() - timedelta(days=400), + 'date_end': date.today() - timedelta(days=200), + 'resource_calendar_id': self.calendar.id, + 'struct_id': self.structure.id, + }) + + date_from = date.today().replace(day=1) + date_to = (date.today().replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1) + contract_ids = self.env['hr.payslip'].get_contract(self.employee, date_from, date_to) + + self.assertIn(self.contract.id, contract_ids) + self.assertNotIn(old_contract.id, contract_ids) + + def test_get_worked_day_lines(self): + today = date.today() + days_ahead = 0 - today.weekday() + if days_ahead <= 0: + days_ahead += 7 + + next_monday = today + timedelta(days=days_ahead) + date_from = next_monday + date_to = next_monday + timedelta(days=4) + try: + worked_days = self.env['hr.payslip'].get_worked_day_lines(self.contract, date_from, date_to) + + work_entry = next((item for item in worked_days if item['code'] == 'WORK100'), None) + + self.assertIsNotNone(work_entry, "WORK100 entry not found in result") + + except AttributeError as e: + print(f"Skipping test_get_worked_day_lines due to missing dependency: {e}") + + def test_payslip_line_compute_total(self): + line = self.env['hr.payslip.line'].create({ + 'slip_id': self.payslip.id, + 'name': 'Test Line', + 'code': 'TEST', + 'contract_id': self.contract.id, + 'salary_rule_id': self.env['hr.salary.rule'].search([], limit=1).id, + 'employee_id': self.employee.id, + 'quantity': 2.0, + 'amount': 500.0, + 'rate': 50.0, + 'category_id': self.category_basic.id, + }) + self.assertEqual(line.total, 500.0) + + def test_get_inputs(self): + rule_with_input = self.env['hr.salary.rule'].create({ + 'name': 'Rule with Input', + 'code': 'INPUT_RULE', + 'category_id': self.category_basic.id, + 'amount_select': 'fix', + 'amount_fix': 0.0, + 'sequence': 50, + }) + self.env['hr.rule.input'].create({ + 'name': 'Commission Input', + 'code': 'COMMISSION', + 'input_id': rule_with_input.id, + }) + self.structure.write({'rule_ids': [(4, rule_with_input.id)]}) + + date_from = date.today() + date_to = date.today() + inputs = self.env['hr.payslip'].get_inputs(self.contract, date_from, date_to) + + found_input = next((i for i in inputs if i['code'] == 'COMMISSION'), None) + self.assertIsNotNone(found_input) + self.assertEqual(found_input['contract_id'], self.contract.id) \ No newline at end of file diff --git a/exp_official_mission/models/hr_official_mission.py b/exp_official_mission/models/hr_official_mission.py index 2177538..2c5b699 100644 --- a/exp_official_mission/models/hr_official_mission.py +++ b/exp_official_mission/models/hr_official_mission.py @@ -1175,7 +1175,7 @@ class HrOfficialMissionEmployee(models.Model): def check_dates(self): for rec in self: if rec.hour_from >= 24 or rec.hour_to >= 24: - raise exceptions.ValidationError(_('Wrong Time Format.!')) + raise ValidationError(_('Wrong Time Format.!')) date_from = datetime.strptime(str(rec.date_from), DEFAULT_SERVER_DATE_FORMAT).date() date_to = datetime.strptime(str(rec.date_to), DEFAULT_SERVER_DATE_FORMAT).date() delta = timedelta(days=1) @@ -1190,7 +1190,7 @@ class HrOfficialMissionEmployee(models.Model): '&', ('hour_from', '>=', rec.hour_from), ('hour_to', '<=', rec.hour_to), ]) if missions_ids: - raise exceptions.ValidationError(_('Sorry The Employee %s Actually On %s For this Time') % + raise ValidationError(_('Sorry The Employee %s Actually On %s For this Time') % (rec.employee_id.name, missions_ids.official_mission_id.mission_type.name)) date_from += delta @@ -1203,7 +1203,7 @@ class HrOfficialMissionEmployee(models.Model): ('official_mission_id.process_type', '=', 'training'), ('official_mission_id.course_name.id', '=', item.official_mission_id.course_name.id)]) if duplicated: - raise exceptions.ValidationError( + raise ValidationError( _("Employee %s has already take this course.") % (item.employee_id.name)) if item.official_mission_id and item.official_mission_id.mission_type.duration_type == 'days' \ and item.date_from and item.date_to: @@ -1230,7 +1230,7 @@ class HrOfficialMissionEmployee(models.Model): if year_last_record == year_now_record: number_days = number_days + rec.days if number_days > days_per_year: - raise exceptions.ValidationError( + raise ValidationError( _("Sorry The Employee %s, The Number of Requests Cannot Exceed %s Maximum Days Per year.") % ( rec.employee_id.name, days_per_year)) #### @@ -1427,7 +1427,7 @@ class HrOfficialMissionEmployee(models.Model): def compute_number_of_hours(self): for item in self: if item.hour_from >= 24 or item.hour_to >= 24: - raise exceptions.ValidationError(_('Wrong Time Format.!')) + raise ValidationError(_('Wrong Time Format.!')) if item.official_mission_id.hour_to and item.official_mission_id.hour_from: if item.hour_from and item.hour_to: if (item.hour_to - item.hour_from) < 0: @@ -1788,7 +1788,7 @@ class MissionTable(models.Model): date_to = rec.destination_id.date_to if date_from and date_to: if not (date_from <= rec.date <= date_to): - raise exceptions.ValidationError( + raise ValidationError( _("The mission date %(date)s must be between destination's date from %(date_from)s and date to %(date_to)s.", date=rec.date, date_from=date_from, date_to=date_to) ) diff --git a/exp_official_mission/tests/__init__.py b/exp_official_mission/tests/__init__.py new file mode 100644 index 0000000..075bb68 --- /dev/null +++ b/exp_official_mission/tests/__init__.py @@ -0,0 +1 @@ +from . import test_official_mission \ No newline at end of file diff --git a/exp_official_mission/tests/test_official_mission.py b/exp_official_mission/tests/test_official_mission.py new file mode 100644 index 0000000..14af7bd --- /dev/null +++ b/exp_official_mission/tests/test_official_mission.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged +from datetime import date, timedelta + + +@tagged('post_install', '-at_install') +class TestOfficialMission(TransactionCase): + + def setUp(self): + super(TestOfficialMission, self).setUp() + + self.company = self.env.company + + self.account = self.env['account.account'].create({ + 'name': 'Mission Expense', + 'code': '600000', + 'account_type': 'expense', + 'reconcile': False, + }) + + self.journal = self.env['account.journal'].create({ + 'name': 'Mission Journal', + 'type': 'general', + 'code': 'MISS', + 'default_account_id': self.account.id, + }) + + self.emp_type = self.env['hr.contract.type'].create({'name': 'Permanent'}) + + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + 'work_email': 'test@example.com', + 'employee_type_id': self.emp_type.id, + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Test Contract', + 'employee_id': self.employee.id, + 'wage': 5000, + 'state': 'open', + }) + self.employee.contract_id = self.contract + + self.mission_type = self.env['hr.official.mission.type'].create({ + 'name': 'External Mission', + 'duration_type': 'days', + 'related_with_financial': True, + 'type_of_payment': 'fixed', + 'day_price': 100.0, + 'journal_id': self.journal.id, + 'account_id': self.account.id, + 'transfer_by_emp_type': False, + 'total_months': 12, + 'max_request_number': 5, + }) + def test_01_duration_calculation(self): + date_from = date.today() + date_to = date.today() + timedelta(days=4) + mission = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date_from, + 'date_to': date_to, + }) + + mission._get_mission_no() + + expected_days = 5 + self.assertEqual(mission.date_duration, expected_days, "Duration in days calculated incorrectly") + + def test_02_workflow_and_financials(self): + mission = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today(), + 'date_to': date.today() + timedelta(days=2), + 'move_type': 'accounting', + }) + + + mission_line = self.env['hr.official.mission.employee'].create({ + 'official_mission_id': mission.id, + 'employee_id': self.employee.id, + 'date_from': mission.date_from, + 'date_to': mission.date_to, + 'hour_from': 8.0, + 'hour_to': 16.0, + }) + + mission_line.days = 3 + mission_line.amount = 300.0 + + + self.assertEqual(mission_line.days, 3, "Employee line days incorrect") + self.assertEqual(mission_line.amount, 300.0, "Employee amount calculation incorrect") + + mission.send() + self.assertEqual(mission.state, 'send') + + mission.accounting_manager() + mission.depart_manager() + + mission.approve() + + self.assertEqual(mission.state, 'approve', "Mission should be approved") + + self.assertTrue(mission_line.account_move_id, "Journal Entry should be created") + self.assertEqual(mission_line.account_move_id.state, 'draft', "Journal Entry should be draft initially") + + move_lines = mission_line.account_move_id.line_ids + debit_line = move_lines.filtered(lambda l: l.debit > 0) + self.assertEqual(debit_line.debit, 300.0, "Journal Entry amount mismatch") + def test_03_overlap_constraint(self): + + mission1 = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today(), + 'date_to': date.today() + timedelta(days=5), + }) + line1 = self.env['hr.official.mission.employee'].create({ + 'official_mission_id': mission1.id, + 'employee_id': self.employee.id, + 'date_from': date.today(), + 'date_to': date.today() + timedelta(days=5), + 'hour_from': 8, + 'hour_to': 16, + }) + mission1.state = 'approve' + mission2 = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today() + timedelta(days=2), + 'date_to': date.today() + timedelta(days=6), + }) + + + with self.assertRaises(ValidationError): + self.env['hr.official.mission.employee'].create({ + 'official_mission_id': mission2.id, + 'employee_id': self.employee.id, + 'date_from': date.today() + timedelta(days=2), + 'date_to': date.today() + timedelta(days=6), + 'hour_from': 8, + 'hour_to': 16, + }) + + def test_04_employees_required(self): + mission = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today(), + 'date_to': date.today(), + }) + + with self.assertRaises(UserError): + mission.send() \ No newline at end of file diff --git a/exp_payroll_custom/__manifest__.py b/exp_payroll_custom/__manifest__.py index 2487668..bd37b35 100644 --- a/exp_payroll_custom/__manifest__.py +++ b/exp_payroll_custom/__manifest__.py @@ -66,4 +66,6 @@ 'installable': True, 'auto_install': False, 'application': True, + 'test_tags': ['standard', 'at_install'], + } diff --git a/exp_payroll_custom/models/hr_advance_payslip.py b/exp_payroll_custom/models/hr_advance_payslip.py index b499ed8..c05ac92 100644 --- a/exp_payroll_custom/models/hr_advance_payslip.py +++ b/exp_payroll_custom/models/hr_advance_payslip.py @@ -104,25 +104,26 @@ class SalaryRuleInput(models.Model): def withdraw(self): payslip = self.env['hr.payslip'].search([('number', '=', self.number)]) - loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', self.employee_id.id)]) - if self.number == payslip.number: - if self.loan_ids: - for loan in self.loan_ids: - loan.paid = False - if loans: - for i in loans: - if i.id == loan.loan_id.id: - for l in i.deduction_lines: - if loan.date == l.installment_date and loan.paid is False: - l.paid = False - #i.remaining_loan_amount += l.installment_amount - i.get_remaining_loan_amount() + if 'hr.loan.salary.advance' in self.env: + loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', self.employee_id.id)]) + if self.number == payslip.number: + if self.loan_ids: + for loan in self.loan_ids: + loan.paid = False + if loans: + for i in loans: + if i.id == loan.loan_id.id: + for l in i.deduction_lines: + if loan.date == l.installment_date and loan.paid is False: + l.paid = False + #i.remaining_loan_amount += l.installment_amount + i.get_remaining_loan_amount() - # check remaining loan and change state to pay - if i.state == 'closed' and i.remaining_loan_amount > 0.0: - i.state = 'pay' - elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: - i.state = 'closed' + # check remaining loan and change state to pay + if i.state == 'closed' and i.remaining_loan_amount > 0.0: + i.state = 'pay' + elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: + i.state = 'closed' for line in payslip.worked_days_line_ids: if line.name != 'Working days for this month': @@ -852,38 +853,38 @@ class SalaryRuleInput(models.Model): d.amount = d.amount payslip.deduction_ids = [fields.Command.set(deductions.ids)] - # Loans # - loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', payslip.employee_id.id), - ('request_type.refund_from', '=', 'salary'), - ('state', '=', 'pay')]).filtered( - lambda item: item.employee_id.state == 'open') - if loans: - for loan in loans: - for l in loan.deduction_lines: - if not l.paid and ( - str(l.installment_date) <= str(payslip.date_from) or str(l.installment_date) <= str( - payslip.date_to)): - employee_loan_id = payslip.loan_ids.filtered( - lambda item: item.name == loan.request_type.name) - if not employee_loan_id: - payslip_loans.append({ - 'name': loan.request_type.name, - 'code': loan.code, - 'amount': round((-l.installment_amount), 2), - 'date': l.installment_date, - 'account_id': loan.request_type.account_id.id, - 'loan_id': loan.id - }) - l.paid = True - l.payment_date = payslip.date_to - else: - payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] + if 'hr.loan.salary.advance' in self.env: + loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', payslip.employee_id.id), + ('request_type.refund_from', '=', 'salary'), + ('state', '=', 'pay')]).filtered( + lambda item: item.employee_id.state == 'open') + if loans: + for loan in loans: + for l in loan.deduction_lines: + if not l.paid and ( + str(l.installment_date) <= str(payslip.date_from) or str(l.installment_date) <= str( + payslip.date_to)): + employee_loan_id = payslip.loan_ids.filtered( + lambda item: item.name == loan.request_type.name) + if not employee_loan_id: + payslip_loans.append({ + 'name': loan.request_type.name, + 'code': loan.code, + 'amount': round((-l.installment_amount), 2), + 'date': l.installment_date, + 'account_id': loan.request_type.account_id.id, + 'loan_id': loan.id + }) + l.paid = True + l.payment_date = payslip.date_to + else: + payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] - # check remaining loan and change state to closed - if loan.remaining_loan_amount <= 0.0 < loan.gm_propos_amount: - loan.state = 'closed' + # check remaining loan and change state to closed + if loan.remaining_loan_amount <= 0.0 < loan.gm_propos_amount: + loan.state = 'closed' - payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] + payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] payslip.allowance_ids._compute_total() payslip.deduction_ids._compute_total() for pay in payslip: @@ -2963,29 +2964,30 @@ class HrPayslipRun(models.Model): def withdraw(self): for line in self.slip_ids: payslip = self.env['hr.payslip'].search([('number', '=', line.number)]) - loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', line.employee_id.id)]) - if line.number == payslip.number: - if line.loan_ids: - for loan in line.loan_ids: - loan.paid = False - if loans: - for i in loans: - if i.id == loan.loan_id.id: - for l in i.deduction_lines: - if loan.date == l.installment_date and loan.paid is False: - l.paid = False - l.payment_date = False - #i.remaining_loan_amount += l.installment_amount - i.get_remaining_loan_amount() + if 'hr.loan.salary.advance' in self.env: + loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', line.employee_id.id)]) + if line.number == payslip.number: + if line.loan_ids: + for loan in line.loan_ids: + loan.paid = False + if loans: + for i in loans: + if i.id == loan.loan_id.id: + for l in i.deduction_lines: + if loan.date == l.installment_date and loan.paid is False: + l.paid = False + l.payment_date = False + #i.remaining_loan_amount += l.installment_amount + i.get_remaining_loan_amount() - # check remaining loan and change state to pay - if i.state == 'closed' and i.remaining_loan_amount > 0.0: - i.state = 'pay' - elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: - i.state = 'closed' - for record in payslip: - record.write({'state': 'draft'}) - record.unlink() + # check remaining loan and change state to pay + if i.state == 'closed' and i.remaining_loan_amount > 0.0: + i.state = 'pay' + elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: + i.state = 'closed' + for record in payslip: + record.write({'state': 'draft'}) + record.unlink() self.write({'slip_ids': [fields.Command.clear()]}) self.write({'state': 'draft'}) diff --git a/exp_payroll_custom/models/hr_contract.py b/exp_payroll_custom/models/hr_contract.py index f5338fb..2e9c778 100644 --- a/exp_payroll_custom/models/hr_contract.py +++ b/exp_payroll_custom/models/hr_contract.py @@ -5,6 +5,11 @@ from datetime import datetime from odoo import models, fields, api, _ from odoo.exceptions import UserError +# -*- coding: utf-8 -*- +from datetime import datetime +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + class HrContractSalaryScale(models.Model): _inherit = 'hr.contract' @@ -15,84 +20,91 @@ class HrContractSalaryScale(models.Model): salary_degree = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) hide = fields.Boolean(string='Hide', compute="compute_type") required_condition = fields.Boolean(string='Required Condition', compute='compute_move_type') - total_allowance = fields.Float(string='Total Allowance', compute='compute_function',store=True) - total_deduction = fields.Float(string='Total Deduction', compute='compute_function',store=True) - total_net = fields.Float(string='Total Net', compute='compute_function',store=True) + total_allowance = fields.Float(string='Total Allowance', compute='compute_function', store=True) + total_deduction = fields.Float(string='Total Deduction', compute='compute_function', store=True) + total_net = fields.Float(string='Total Net', compute='compute_function', store=True) advantages = fields.One2many('contract.advantage', 'contract_advantage_id', string='Advantages') - house_allowance_temp = fields.Float(string='House Allowance', compute='compute_function',store=True) - transport_allowance = fields.Float(string='Transport Allowance', compute='compute_function',store=True) + house_allowance_temp = fields.Float(string='House Allowance', compute='compute_function', store=True) + transport_allowance = fields.Float(string='Transport Allowance', compute='compute_function', store=True) @api.constrains('advantages', 'salary', 'salary_group') def amount_constrains(self): for rec in self: - localdict = dict(employee=rec.employee_id.id, contract=rec.env['hr.contract'].search([ - ('employee_id', '=', rec.employee_id.id)])) + localdict = dict(employee=rec.employee_id, contract=rec) + if rec.salary_group.gread_max > 0 and rec.salary_group.gread_min > 0: - if rec.salary > rec.salary_group.gread_max or rec.salary < rec.salary_group.gread_min: - raise UserError(_('The Basic Salary Is Greater Than Group Gread Max Or less than Gread Min')) - for item in self.advantages: - item.to_get_contract_id() - if item.benefits_discounts._compute_rule(localdict)[0] < item.amount and item.type == 'exception': - raise UserError(_( - 'The amount you put is greater than fact value of this Salary rule %s (%s).') % ( - item.benefits_discounts.name, item.benefits_discounts.code)) + if rec.salary > rec.salary_group.gread_max or rec.salary < rec.salary_group.gread_min: + raise UserError(_('The Basic Salary Is Greater Than Group Gread Max Or less than Gread Min')) + + for item in rec.advantages: + if item.type == 'exception': + rule_val = item.benefits_discounts._compute_rule(localdict)[0] + if rule_val < item.amount: + raise UserError(_( + 'The amount you put is greater than fact value of this Salary rule %s (%s).') % ( + item.benefits_discounts.name, item.benefits_discounts.code)) @api.depends('salary_scale.transfer_type') def compute_move_type(self): - self.compute_function() + # self.compute_function() if self.salary_scale.transfer_type == 'one_by_one': self.required_condition = True else: self.required_condition = False - @api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree','salary','advantages','house_allowance_temp','transport_allowance','total_deduction','salary_insurnce','total_allowance','state') + @api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree', 'salary', 'advantages', + 'house_allowance_temp', 'transport_allowance', 'total_deduction', 'total_allowance', 'state') def compute_function(self): for item in self: item.house_allowance_temp = 0 item.transport_allowance = 0 item.total_net = 0 - contract = self.env['hr.contract'].search([('employee_id', '=', item.employee_id.id)]) - localdict = dict(employee=item.employee_id.id, contract=contract) - current_date = datetime.now().date() - # customize type in advantages + localdict = dict(employee=item.employee_id, contract=item) + current_date = fields.Date.today() + allowance_customize_items = item.advantages.filtered( lambda key: key.type == 'customize' and key.out_rule is False and - key.benefits_discounts.category_id.rule_type == 'allowance' and - (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) - >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) + key.benefits_discounts.category_id.rule_type == 'allowance' and + (key.date_to if key.date_to else current_date) >= current_date >= key.date_from + ) allow_sum_custom = sum(x.amount for x in allowance_customize_items) for x in allowance_customize_items: if x.benefits_discounts.rules_type == 'house': item.house_allowance_temp += x.amount - if x.benefits_discounts.rules_type == 'transport': item.transport_allowance += x.amount - # allow_custom_ids = [record.benefits_discounts.id for record in allowance_customize_items] deduction_customize_items = item.advantages.filtered( lambda key: key.type == 'customize' and key.out_rule is False and key.benefits_discounts.category_id.rule_type == 'deduction' and - (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) - >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) + (key.date_to if key.date_to else current_date) >= current_date >= key.date_from + ) ded_sum_custom = sum(x.amount for x in deduction_customize_items) - ded_custom_ids = [record.benefits_discounts.id for record in deduction_customize_items] + ded_custom_ids = deduction_customize_items.mapped('benefits_discounts.id') - # exception type in advantages exception_items = item.advantages.filtered(lambda key: key.type == 'exception') + + if exception_items: + exception_items = exception_items.filtered( + lambda key: (key.date_to.month if key.date_to else current_date.month) + >= current_date.month >= key.date_from.month + ) + total_rule_result, sum_except, sum_customize_expect = 0.0, 0.0, 0.0 for x in exception_items: rule_result = x.benefits_discounts._compute_rule(localdict)[0] - if x.date_from >= str(current_date): + + if x.date_from >= current_date: total_rule_result = rule_result - elif str(current_date) > x.date_from: - if x.date_to and str(current_date) <= x.date_to: + elif current_date > x.date_from: + if x.date_to and current_date <= x.date_to: total_rule_result = rule_result - x.amount - elif x.date_to and str(current_date) >= x.date_to: - total_rule_result = 0 # rule_result + elif x.date_to and current_date >= x.date_to: + total_rule_result = 0 elif not x.date_to: total_rule_result = rule_result - x.amount else: @@ -107,85 +119,42 @@ class HrContractSalaryScale(models.Model): else: sum_except += total_rule_result - if exception_items: - exception_items = item.advantages.filtered( - lambda key: (datetime.strptime(str(key.date_to), - "%Y-%m-%d").date().month if key.date_to else current_date.month) - >= current_date.month >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date().month) - - except_ids = [record.benefits_discounts.id for record in exception_items] + except_ids = exception_items.mapped('benefits_discounts.id') rule_ids = item.salary_scale.rule_ids.filtered( lambda key: key.id not in ded_custom_ids and key.id not in except_ids) - level_rule_ids = item.salary_level.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) - # key.id not in allow_custom_ids and key.id not in ded_custom_ids and + if item.salary_level: + rule_ids += item.salary_level.rule_ids.filtered( + lambda key: key.id not in except_ids) - group_rule_ids = item.salary_group.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) - # key.id not in allow_custom_ids and key.id not in ded_custom_ids and + if item.salary_group: + rule_ids += item.salary_group.rule_ids.filtered( + lambda key: key.id not in except_ids) total_allowance = 0 total_ded = 0 + for line in rule_ids: + try: + amount = line._compute_rule(localdict)[0] + except Exception: + amount = 0.0 + if line.category_id.rule_type == 'allowance': - try: - total_allowance += line._compute_rule(localdict)[0] - except: - total_allowance += 0 - - if line.category_id.rule_type == 'deduction': - try: - total_ded += line._compute_rule(localdict)[0] - except: - total_ded += 0 - + total_allowance += amount + elif line.category_id.rule_type == 'deduction': + total_ded += amount if line.rules_type == 'house': - item.house_allowance_temp += line._compute_rule(localdict)[0] + item.house_allowance_temp += amount if line.rules_type == 'transport': - item.transport_allowance += line._compute_rule(localdict)[0] + item.transport_allowance += amount - item.total_allowance = total_allowance - item.total_deduction = -total_ded - - if item.salary_level: - total_allowance = 0 - total_deduction = 0 - for line in level_rule_ids: - if line.category_id.rule_type == 'allowance': - try: - total_allowance += line._compute_rule(localdict)[0] - except: - total_allowance += 0 - elif line.category_id.rule_type == 'deduction': - try: - total_deduction += line._compute_rule(localdict)[0] - except: - total_deduction += 0 - - item.total_allowance += total_allowance - item.total_deduction += -total_deduction - - if item.salary_group: - total_allowance = 0 - total_deduction = 0 - for line in group_rule_ids: - if line.category_id.rule_type == 'allowance': - total_allowance += line._compute_rule(localdict)[0] - elif line.category_id.rule_type == 'deduction': - total_deduction += line._compute_rule(localdict)[0] - - item.total_allowance += total_allowance - item.total_deduction += -total_deduction - - item.total_allowance += allow_sum_custom - item.total_allowance += sum_customize_expect - item.total_deduction += -ded_sum_custom - item.total_deduction += -sum_except + item.total_allowance = total_allowance + allow_sum_custom + sum_customize_expect + item.total_deduction = -(total_ded + ded_sum_custom + sum_except) item.total_net = item.total_allowance + item.total_deduction - # filter salary_level,salary_group,salary_degree - @api.onchange('salary_scale') def onchange_salary_scale(self): for item in self: @@ -207,8 +176,6 @@ class HrContractSalaryScale(models.Model): 'salary_group': [('id', 'in', [])], 'salary_degree': [('id', 'in', [])]}} - # filter depend on salary_level - @api.onchange('salary_level') def onchange_salary_level(self): for item in self: @@ -221,7 +188,6 @@ class HrContractSalaryScale(models.Model): return {'domain': {'salary_group': [('id', 'in', [])], 'salary_degree': [('id', 'in', [])]}} - # filter depend on salary_group @api.onchange('salary_group') def onchange_salary_group(self): @@ -232,29 +198,228 @@ class HrContractSalaryScale(models.Model): return {'domain': {'salary_degree': [('id', 'in', degree_ids.ids)]}} else: return {'domain': {'salary_degree': [('id', 'in', [])]}} - - @api.depends('salary_degree') - def _get_amount(self): - for record in self: - record.transport_allowance_temp = record.transport_allowance * record.wage / 100 \ - if record.transport_allowance_type == 'perc' else record.transport_allowance - record.house_allowance_temp = record.house_allowance * record.wage / 100 \ - if record.house_allowance_type == 'perc' else record.house_allowance - record.communication_allowance_temp = record.communication_allowance * record.wage / 100 \ - if record.communication_allowance_type == 'perc' else record.communication_allowance - record.field_allowance_temp = record.field_allowance * record.wage / 100 \ - if record.field_allowance_type == 'perc' else record.field_allowance - record.special_allowance_temp = record.special_allowance * record.wage / 100 \ - if record.special_allowance_type == 'perc' else record.special_allowance - record.other_allowance_temp = record.other_allowance * record.wage / 100 \ - if record.other_allowance_type == 'perc' else record.other_allowance - @api.depends('contractor_type.salary_type') def compute_type(self): - if self.contractor_type.salary_type == 'scale': - self.hide = True - else: - self.hide = False + for rec in self: + if rec.contractor_type.salary_type == 'scale': + rec.hide = True + else: + rec.hide = False +# +# class HrContractSalaryScale(models.Model): +# _inherit = 'hr.contract' +# +# salary_level = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) +# salary_scale = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])], index=True) +# salary_group = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) +# salary_degree = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) +# hide = fields.Boolean(string='Hide', compute="compute_type") +# required_condition = fields.Boolean(string='Required Condition', compute='compute_move_type') +# total_allowance = fields.Float(string='Total Allowance', compute='compute_function',store=True) +# total_deduction = fields.Float(string='Total Deduction', compute='compute_function',store=True) +# total_net = fields.Float(string='Total Net', compute='compute_function',store=True) +# advantages = fields.One2many('contract.advantage', 'contract_advantage_id', string='Advantages') +# house_allowance_temp = fields.Float(string='House Allowance', compute='compute_function',store=True) +# transport_allowance = fields.Float(string='Transport Allowance', compute='compute_function',store=True) +# +# @api.constrains('advantages', 'salary', 'salary_group') +# def amount_constrains(self): +# for rec in self: +# localdict = dict(employee=rec.employee_id.id, contract=rec.env['hr.contract'].search([ +# ('employee_id', '=', rec.employee_id.id)])) +# if rec.salary_group.gread_max > 0 and rec.salary_group.gread_min > 0: +# if rec.salary > rec.salary_group.gread_max or rec.salary < rec.salary_group.gread_min: +# raise UserError(_('The Basic Salary Is Greater Than Group Gread Max Or less than Gread Min')) +# for item in self.advantages: +# item.to_get_contract_id() +# if item.benefits_discounts._compute_rule(localdict)[0] < item.amount and item.type == 'exception': +# raise UserError(_( +# 'The amount you put is greater than fact value of this Salary rule %s (%s).') % ( +# item.benefits_discounts.name, item.benefits_discounts.code)) +# +# @api.depends('salary_scale.transfer_type') +# def compute_move_type(self): +# self.compute_function() +# if self.salary_scale.transfer_type == 'one_by_one': +# self.required_condition = True +# else: +# self.required_condition = False +# +# @api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree','salary','advantages','house_allowance_temp','transport_allowance','total_deduction','salary_insurnce','total_allowance','state') +# def compute_function(self): +# for item in self: +# item.house_allowance_temp = 0 +# item.transport_allowance = 0 +# item.total_net = 0 +# contract = self.env['hr.contract'].search([('employee_id', '=', item.employee_id.id)]) +# localdict = dict(employee=item.employee_id.id, contract=contract) +# current_date = datetime.now().date() +# +# # customize type in advantages +# allowance_customize_items = item.advantages.filtered( +# lambda key: key.type == 'customize' and key.out_rule is False and +# key.benefits_discounts.category_id.rule_type == 'allowance' and +# (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) +# >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) +# +# allow_sum_custom = sum(x.amount for x in allowance_customize_items) +# for x in allowance_customize_items: +# if x.benefits_discounts.rules_type == 'house': +# item.house_allowance_temp += x.amount +# +# if x.benefits_discounts.rules_type == 'transport': +# item.transport_allowance += x.amount +# # allow_custom_ids = [record.benefits_discounts.id for record in allowance_customize_items] +# +# deduction_customize_items = item.advantages.filtered( +# lambda key: key.type == 'customize' and key.out_rule is False and +# key.benefits_discounts.category_id.rule_type == 'deduction' and +# (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) +# >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) +# +# ded_sum_custom = sum(x.amount for x in deduction_customize_items) +# ded_custom_ids = [record.benefits_discounts.id for record in deduction_customize_items] +# +# # exception type in advantages +# exception_items = item.advantages.filtered(lambda key: key.type == 'exception') +# total_rule_result, sum_except, sum_customize_expect = 0.0, 0.0, 0.0 +# +# for x in exception_items: +# rule_result = x.benefits_discounts._compute_rule(localdict)[0] +# if x.date_from >= current_date: +# total_rule_result = rule_result +# elif current_date > x.date_from: +# if x.date_to and current_date <= x.date_to: +# total_rule_result = rule_result - x.amount +# elif x.date_to and current_date >= x.date_to: +# total_rule_result = 0 # rule_result +# elif not x.date_to: +# total_rule_result = rule_result - x.amount +# else: +# if rule_result > x.amount: +# total_rule_result = rule_result - x.amount +# +# if total_rule_result: +# if x.benefits_discounts.category_id.rule_type == 'allowance': +# sum_customize_expect += total_rule_result +# if x.benefits_discounts.rules_type == 'house': +# item.house_allowance_temp += total_rule_result - x.amount +# else: +# sum_except += total_rule_result +# +# if exception_items: +# exception_items = item.advantages.filtered( +# lambda key: (key.date_to.month if key.date_to else current_date.month) +# >= current_date.month >= key.date_from.month) +# # if exception_items: +# # exception_items = item.advantages.filtered( +# # lambda key: (datetime.strptime(key.date_to, +# # "%Y-%m-%d").date().month if key.date_to else current_date.month) +# # >= current_date.month >= datetime.strptime(key.date_from, "%Y-%m-%d").date().month) +# +# except_ids = [record.benefits_discounts.id for record in exception_items] +# +# rule_ids = item.salary_scale.rule_ids.filtered( +# lambda key: key.id not in ded_custom_ids and key.id not in except_ids) +# +# level_rule_ids = item.salary_level.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) +# # key.id not in allow_custom_ids and key.id not in ded_custom_ids and +# +# group_rule_ids = item.salary_group.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) +# # key.id not in allow_custom_ids and key.id not in ded_custom_ids and +# +# total_allowance = 0 +# total_ded = 0 +# for line in rule_ids: +# if line.category_id.rule_type == 'allowance': +# try: +# total_allowance += line._compute_rule(localdict)[0] +# except: +# total_allowance += 0 +# +# if line.category_id.rule_type == 'deduction': +# try: +# total_ded += line._compute_rule(localdict)[0] +# except: +# total_ded += 0 +# +# +# if line.rules_type == 'house': +# item.house_allowance_temp += line._compute_rule(localdict)[0] +# if line.rules_type == 'transport': +# item.transport_allowance += line._compute_rule(localdict)[0] +# +# item.total_allowance = total_allowance +# item.total_deduction = -total_ded +# +# if item.salary_level: +# total_allowance = 0 +# total_deduction = 0 +# for line in level_rule_ids: +# if line.category_id.rule_type == 'allowance': +# try: +# total_allowance += line._compute_rule(localdict)[0] +# except: +# total_allowance += 0 +# elif line.category_id.rule_type == 'deduction': +# try: +# total_deduction += line._compute_rule(localdict)[0] +# except: +# total_deduction += 0 +# +# item.total_allowance += total_allowance +# item.total_deduction += -total_deduction +# +# if item.salary_group: +# total_allowance = 0 +# total_deduction = 0 +# for line in group_rule_ids: +# if line.category_id.rule_type == 'allowance': +# total_allowance += line._compute_rule(localdict)[0] +# elif line.category_id.rule_type == 'deduction': +# total_deduction += line._compute_rule(localdict)[0] +# +# item.total_allowance += total_allowance +# item.total_deduction += -total_deduction +# +# item.total_allowance += allow_sum_custom +# item.total_allowance += sum_customize_expect +# item.total_deduction += -ded_sum_custom +# item.total_deduction += -sum_except +# item.total_net = item.total_allowance + item.total_deduction +# +# # filter salary_level,salary_group,salary_degree +# +# +# # filter depend on salary_level +# +# +# # filter depend on salary_group +# +# +# +# @api.depends('salary_degree') +# def _get_amount(self): +# for record in self: +# record.transport_allowance_temp = record.transport_allowance * record.wage / 100 \ +# if record.transport_allowance_type == 'perc' else record.transport_allowance +# record.house_allowance_temp = record.house_allowance * record.wage / 100 \ +# if record.house_allowance_type == 'perc' else record.house_allowance +# record.communication_allowance_temp = record.communication_allowance * record.wage / 100 \ +# if record.communication_allowance_type == 'perc' else record.communication_allowance +# record.field_allowance_temp = record.field_allowance * record.wage / 100 \ +# if record.field_allowance_type == 'perc' else record.field_allowance +# record.special_allowance_temp = record.special_allowance * record.wage / 100 \ +# if record.special_allowance_type == 'perc' else record.special_allowance +# record.other_allowance_temp = record.other_allowance * record.wage / 100 \ +# if record.other_allowance_type == 'perc' else record.other_allowance +# +# @api.depends('contractor_type.salary_type') +# def compute_type(self): +# if self.contractor_type.salary_type == 'scale': +# self.hide = True +# else: +# self.hide = False class Advantages(models.Model): diff --git a/exp_payroll_custom/models/hr_salary_rules.py b/exp_payroll_custom/models/hr_salary_rules.py index 6dbafa7..20b8d9f 100644 --- a/exp_payroll_custom/models/hr_salary_rules.py +++ b/exp_payroll_custom/models/hr_salary_rules.py @@ -79,249 +79,347 @@ class HrSalaryRules(models.Model): if rec.category_id.rule_type != 'deduction' and rec.rules_type == 'insurnce': raise UserError(_("The Salary Rule is Not Deduction")) - # Override function compute rule in hr salary rule - def _compute_rule(self, localdict): + + self.ensure_one() payslip = localdict.get('payslip') contract = localdict.get('contract') + current_date = fields.Date.today() + + fix_amount_value = self.amount_fix if hasattr(self, 'amount_fix') else getattr(self, 'fixed_amount', 0.0) + + + def get_related_amount(): + salary_type = getattr(self, 'salary_type', 'fixed') + if salary_type == 'related_levels' and contract.salary_level: + related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_level.id == contract.salary_level.id) + return related.salary if related else 0.0 + elif salary_type == 'related_groups' and contract.salary_group: + related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_group.id == contract.salary_group.id) + return related.salary if related else 0.0 + elif salary_type == 'related_degrees' and contract.salary_degree: + related = self.salary_amount_ids.filtered( + lambda r: r.salary_scale_degree.id == contract.salary_degree.id) + return related.salary if related else 0.0 + return fix_amount_value + if self.amount_select == 'percentage': - total_percent, total = 0, 0 - if self.related_benefits_discounts: - for line in self.related_benefits_discounts: + total_percent = 0.0 + related_rules = getattr(self, 'related_benefits_discounts', []) + + if related_rules: + for line in related_rules: calc_line = line._compute_rule(localdict)[0] + line_in_advantages = False + advantages = contract.advantages if contract else [] - if line.amount_select == 'fix': - if contract.advantages: - for con in contract.advantages: - if line.id == con.benefits_discounts.id: - if payslip: - if con.date_from > payslip.date_from: - total_percent = calc_line - elif con.date_to is not None and str( - con.date_to) >= payslip.date_to or con.date_to is None: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent += total + if advantages: + con = next((adv for adv in advantages if adv.benefits_discounts.id == line.id), None) + if con: + line_in_advantages = True + is_valid_date = False + + if payslip: + if con.date_from > payslip.date_from: + total_percent += calc_line + continue + elif (not con.date_to) or (con.date_to >= payslip.date_to): + is_valid_date = True + else: + if con.date_from <= current_date: + if (not con.date_to) or (con.date_to >= current_date): + is_valid_date = True + + if is_valid_date: + total_to_add = 0.0 + if con.type == 'exception': + if con.amount < calc_line: + total_to_add = calc_line - con.amount else: - if str(con.date_from) < str(datetime.now().date()): - if con.date_to: - if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - >= datetime.now().date().month or not con.date_to: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent += total - else: - total_percent = calc_line - else: - total_percent += calc_line + total_to_add = 0.0 + elif con.type == 'customize': + total_to_add = con.amount - elif line.amount_select == 'percentage': - if contract.advantages: - for con in contract.advantages: - if line.id == con.benefits_discounts.id: - if payslip: - if con.date_from > payslip.date_from: - total_percent = calc_line - elif con.date_to is not None and str( - con.date_to) >= payslip.date_to or con.date_to is None: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent -= calc_line - total_percent += total - else: - if str(con.date_from) < str(datetime.now().date()): - if con.date_to: - if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - >= datetime.now().date().month or not con.date_to: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent -= calc_line - total_percent += total + if line.amount_select == 'percentage': + total_percent -= calc_line + total_percent += total_to_add else: - if con.type != 'exception': - total_percent += calc_line - break - else: - total_percent += calc_line + total_percent += total_to_add + else: + total_percent += calc_line - else: - if contract.advantages: - for con in contract.advantages: - if line.id == con.benefits_discounts.id: - if payslip: - if con.date_from > payslip.date_from: - total_percent = calc_line - elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent = 0 - total_percent += total - else: - if con.date_from < (datetime.now().date()): - if con.date_to: - if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - >= datetime.now().date().month or not con.date_to: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent = 0 - total_percent += total - else: - if datetime.strptime(str(con.date_from), - "%Y-%m-%d").date().month >= datetime.now().date().month: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount + calc_line - total_percent = 0 - total_percent += total + if not line_in_advantages: + total_percent += calc_line - else: - if not total_percent: - total_percent = calc_line - else: - total_percent += calc_line if total_percent: - if self.salary_type == 'fixed': - try: - return float(total_percent * self.amount_percentage / 100), \ - float(safe_eval(self.quantity, localdict)), self.amount_percentage - except: - raise UserError( - _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - elif self.salary_type == 'related_levels': - levels_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_level.id == contract.salary_level.id) - if levels_ids: - for l in levels_ids: - try: - return float(l.salary * total_percent / 100), float( - safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_groups': - groups_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_group.id == contract.salary_group.id) - if groups_ids: - for g in groups_ids: - try: - return float(g.salary * total_percent / 100), float( - safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_degrees': - degrees_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - if degrees_ids: - for d in degrees_ids: - try: - return float(d.salary * total_percent / 100), float( - safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - else: - return 0, 0, 0 - else: try: - return 0, 0, 0 - except: - raise UserError(_('There is no total for rule : %s') % self.name) + qty = float(safe_eval(self.quantity, localdict)) + rate = self.amount_percentage + return float(total_percent * self.amount_percentage / 100), qty, rate + except Exception as e: + raise UserError(_('Error calculating percentage rule %s: %s') % (self.name, e)) + else: + return 0.0, 0.0, 0.0 elif self.amount_select == 'fix': - if self.salary_type == 'fixed': - try: - return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - elif self.salary_type == 'related_levels': - levels_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_level.id == contract.salary_level.id) - if levels_ids: - for l in levels_ids: - try: - return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_groups': - groups_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_group.id == contract.salary_group.id) - if groups_ids: - for g in groups_ids: - try: - return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_degrees': - degrees_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - if degrees_ids: - for d in degrees_ids: - try: - return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - else: - return 0, 0, 0 - else: - raise UserError(_('Error, Select Salary type to calculate rule')) + try: + qty = float(safe_eval(self.quantity, localdict)) + amount = get_related_amount() + return amount, qty, 100.0 + except Exception as e: + raise UserError(_('Error computing fix rule %s: %s') % (self.name, e)) else: try: safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) - return float(localdict['result']), 'result_qty' in localdict and localdict[ - 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 - except: - raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) + return float(localdict.get('result', 0.0)), \ + localdict.get('result_qty', 1.0), \ + localdict.get('result_rate', 100.0) + except Exception as e: + raise UserError(_('Error computing python rule %s: %s') % (self.name, e)) + # Override function compute rule in hr salary rule + + # def _compute_rule(self, localdict): + # payslip = localdict.get('payslip') + # contract = localdict.get('contract') + # if self.amount_select == 'percentage': + # total_percent, total = 0, 0 + # if self.related_benefits_discounts: + # for line in self.related_benefits_discounts: + # calc_line = line._compute_rule(localdict)[0] + # + # if line.amount_select == 'fix': + # if contract.advantages: + # for con in contract.advantages: + # if line.id == con.benefits_discounts.id: + # if payslip: + # if con.date_from > payslip.date_from: + # total_percent = calc_line + # elif con.date_to is not None and str( + # con.date_to) >= payslip.date_to or con.date_to is None: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent += total + # else: + # if str(con.date_from) < str(datetime.now().date()): + # if con.date_to: + # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + # >= datetime.now().date().month or not con.date_to: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent += total + # else: + # total_percent = calc_line + # else: + # total_percent += calc_line + # + # elif line.amount_select == 'percentage': + # if contract.advantages: + # for con in contract.advantages: + # if line.id == con.benefits_discounts.id: + # if payslip: + # if con.date_from > payslip.date_from: + # total_percent = calc_line + # elif con.date_to is not None and str( + # con.date_to) >= payslip.date_to or con.date_to is None: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent -= calc_line + # total_percent += total + # else: + # if str(con.date_from) < str(datetime.now().date()): + # if con.date_to: + # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + # >= datetime.now().date().month or not con.date_to: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent -= calc_line + # total_percent += total + # else: + # if con.type != 'exception': + # total_percent += calc_line + # break + # else: + # total_percent += calc_line + # + # else: + # if contract.advantages: + # for con in contract.advantages: + # if line.id == con.benefits_discounts.id: + # if payslip: + # if con.date_from > payslip.date_from: + # total_percent = calc_line + # elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent = 0 + # total_percent += total + # else: + # if con.date_from < (datetime.now().date()): + # if con.date_to: + # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + # >= datetime.now().date().month or not con.date_to: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent = 0 + # total_percent += total + # else: + # if datetime.strptime(str(con.date_from), + # "%Y-%m-%d").date().month >= datetime.now().date().month: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + calc_line + # total_percent = 0 + # total_percent += total + # + # else: + # if not total_percent: + # total_percent = calc_line + # else: + # total_percent += calc_line + # if total_percent: + # if self.salary_type == 'fixed': + # try: + # return float(total_percent * self.amount_percentage / 100), \ + # float(safe_eval(self.quantity, localdict)), self.amount_percentage + # except: + # raise UserError( + # _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # elif self.salary_type == 'related_levels': + # levels_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_level.id == contract.salary_level.id) + # if levels_ids: + # for l in levels_ids: + # try: + # return float(l.salary * total_percent / 100), float( + # safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_groups': + # groups_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_group.id == contract.salary_group.id) + # if groups_ids: + # for g in groups_ids: + # try: + # return float(g.salary * total_percent / 100), float( + # safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_degrees': + # degrees_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + # if degrees_ids: + # for d in degrees_ids: + # try: + # return float(d.salary * total_percent / 100), float( + # safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # else: + # return 0, 0, 0 + # else: + # try: + # return 0, 0, 0 + # except: + # raise UserError(_('There is no total for rule : %s') % self.name) + # + # elif self.amount_select == 'fix': + # if self.salary_type == 'fixed': + # try: + # return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # elif self.salary_type == 'related_levels': + # levels_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_level.id == contract.salary_level.id) + # if levels_ids: + # for l in levels_ids: + # try: + # return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_groups': + # groups_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_group.id == contract.salary_group.id) + # if groups_ids: + # for g in groups_ids: + # try: + # return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_degrees': + # degrees_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + # if degrees_ids: + # for d in degrees_ids: + # try: + # return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # else: + # return 0, 0, 0 + # else: + # raise UserError(_('Error, Select Salary type to calculate rule')) + # + # else: + # try: + # safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) + # return float(localdict['result']), 'result_qty' in localdict and localdict[ + # 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 + # except: + # raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) class SalaryConfig(models.Model): diff --git a/exp_payroll_custom/tests/__init__.py b/exp_payroll_custom/tests/__init__.py new file mode 100644 index 0000000..af07c36 --- /dev/null +++ b/exp_payroll_custom/tests/__init__.py @@ -0,0 +1,5 @@ +from . import test_salary_scale +from . import test_salary_rule_computation +from . import test_employee_promotions +from . import test_payroll_flow +from . import test_employee_reward \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_employee_promotions.py b/exp_payroll_custom/tests/test_employee_promotions.py new file mode 100644 index 0000000..d6faae2 --- /dev/null +++ b/exp_payroll_custom/tests/test_employee_promotions.py @@ -0,0 +1,148 @@ +from odoo.tests.common import TransactionCase, Form +from odoo.exceptions import UserError +from odoo.tests import tagged +from datetime import date + + +@tagged('post_install', '-at_install') +class TestEmployeePromotions(TransactionCase): + + def setUp(cls): + super(TestEmployeePromotions, cls).setUp() + + + cls.structure_scale = cls.env['hr.payroll.structure'].create({ + 'name': 'General Scale', + 'type': 'scale', + 'code': 'SCL_TEST_01' + }) + + cls.level_1 = cls.env['hr.payroll.structure'].create({ + 'name': 'Level 1', + 'type': 'level', + 'salary_scale_id': cls.structure_scale.id, + 'code': 'LVL_TEST_01' + }) + cls.group_A = cls.env['hr.payroll.structure'].create({ + 'name': 'Group A', + 'type': 'group', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_level_id': cls.level_1.id, + 'code': 'GRP_TEST_A' + }) + cls.degree_1 = cls.env['hr.payroll.structure'].create({ + 'name': 'Degree 1', + 'type': 'degree', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_group_id': cls.group_A.id, + 'base_salary': 5000.0, + 'code': 'DEG_TEST_1' + }) + + cls.level_2 = cls.env['hr.payroll.structure'].create({ + 'name': 'Level 2', + 'type': 'level', + 'salary_scale_id': cls.structure_scale.id, + 'code': 'LVL_TEST_02' + }) + cls.group_B = cls.env['hr.payroll.structure'].create({ + 'name': 'Group B', + 'type': 'group', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_level_id': cls.level_2.id, + 'code': 'GRP_TEST_B' + }) + cls.degree_2 = cls.env['hr.payroll.structure'].create({ + 'name': 'Degree 2', + 'type': 'degree', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_group_id': cls.group_B.id, + 'base_salary': 7000.0, + 'code': 'DEG_TEST_2' + }) + + cls.employee = cls.env['hr.employee'].create({ + 'name': 'Test Employee', + 'salary_scale': cls.structure_scale.id, + 'salary_level': cls.level_1.id, + 'salary_group': cls.group_A.id, + 'salary_degree': cls.degree_1.id, + }) + + cls.contract = cls.env['hr.contract'].create({ + 'name': 'Test Contract', + 'employee_id': cls.employee.id, + 'wage': 5000.0, + 'state': 'open', + 'salary_level': cls.level_1.id, + 'salary_group': cls.group_A.id, + 'salary_degree': cls.degree_1.id, + }) + + cls.employee.contract_id = cls.contract + + def test_01_promotion_workflow_full_cycle(self): + + promotion_form = Form(self.env['employee.promotions']) + promotion_form.date = date.today() + promotion_form.employee_id = self.employee + + self.assertEqual(promotion_form.old_degree, self.degree_1, "Should auto-fill old degree from employee") + + promotion_form.new_level = self.level_2 + promotion_form.new_group = self.group_B + promotion_form.new_degree = self.degree_2 + + promotion = promotion_form.save() + + promotion.confirm() + self.assertEqual(promotion.state, 'confirm') + + promotion.hr_manager() + self.assertEqual(promotion.state, 'hr_manager') + + promotion.approved() + self.assertEqual(promotion.state, 'approved') + + + self.assertEqual(self.employee.contract_id.salary_degree, self.degree_2, + "Contract degree should be updated to new degree") + self.assertEqual(self.employee.contract_id.salary, 7000.0, + "Contract salary should be updated to new base salary") + + def test_02_redraft_reverts_values(self): + + promotion = self.env['employee.promotions'].create({ + 'date': date.today(), + 'employee_id': self.employee.id, + 'old_degree': self.degree_1.id, + 'old_level_2': self.level_1.id, + 'old_group_2': self.group_A.id, + 'old_degree_2': self.degree_1.id, + 'new_degree': self.degree_2.id, + 'new_level': self.level_2.id, + 'new_group': self.group_B.id, + }) + + promotion.approved() + self.assertEqual(self.employee.contract_id.salary_degree, self.degree_2) + + promotion.re_draft() + + self.assertEqual(promotion.state, 'draft') + self.assertEqual(self.employee.contract_id.salary_degree, self.degree_1, + "Should revert to old degree on re-draft") + + def test_03_unlink_restriction(self): + promotion = self.env['employee.promotions'].create({ + 'date': date.today(), + 'employee_id': self.employee.id, + 'state': 'confirm' + }) + + with self.assertRaises(UserError): + promotion.unlink() + + promotion.state = 'draft' + promotion.unlink() + self.assertFalse(promotion.exists()) \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_employee_reward.py b/exp_payroll_custom/tests/test_employee_reward.py new file mode 100644 index 0000000..44395e8 --- /dev/null +++ b/exp_payroll_custom/tests/test_employee_reward.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import tagged +from datetime import date + + +@tagged('post_install', '-at_install') +class TestEmployeeReward(TransactionCase): + + def setUp(self): + super(TestEmployeeReward, self).setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + }) + + self.account_debit = self.env['account.account'].create({ + 'name': 'Debit Account', + 'code': '100001', + 'account_type': 'expense', + 'reconcile': True, + }) + + self.account_credit = self.env['account.account'].create({ + 'name': 'Credit Account', + 'code': '200001', + 'account_type': 'liability_payable', + 'reconcile': True, + }) + + self.journal = self.env['account.journal'].create({ + 'name': 'Reward Journal', + 'type': 'general', + 'code': 'REW', + 'default_account_id': self.account_credit.id, + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract for Test', + 'employee_id': self.employee.id, + 'wage': 5000.0, + 'state': 'open', + }) + + def test_01_reward_workflow_and_calculation(self): + + reward = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'Excellent Performance', + 'date': date.today(), + 'reward_type': 'amount', + 'amount': 1000.0, + 'transfer_type': 'accounting', + 'account_id': self.account_debit.id, + 'journal_id': self.journal.id, + }) + + reward_line = self.env['lines.ids.reward'].create({ + 'employee_reward_id': reward.id, + 'employee_id': self.employee.id, + 'percentage': 50.0, + }) + + self.assertEqual(reward_line.amount, 500.0, "Amount calculation is wrong based on percentage") + + + reward.action_submit() + self.assertEqual(reward.state, 'submitted', "State should be submitted") + self.assertEqual(reward_line.reward_state, 'submitted', "Line state should match parent") + + reward.action_hrm() + self.assertEqual(reward.state, 'hrm', "State should be hrm") + + reward.action_done() + self.assertEqual(reward.state, 'done', "State should be done") + + self.assertTrue(reward_line.move_id, "Journal Entry should be created") + self.assertEqual(reward_line.move_id.state, 'draft', "Move should be created in draft") + + move_lines = reward_line.move_id.line_ids + debit_line = move_lines.filtered(lambda l: l.debit > 0) + credit_line = move_lines.filtered(lambda l: l.credit > 0) + + self.assertEqual(debit_line.account_id, self.account_debit, "Debit account mismatch") + self.assertEqual(credit_line.account_id, self.account_credit, "Credit account mismatch") + self.assertEqual(debit_line.debit, 500.0, "Debit amount incorrect") + + def test_02_constraint_reward_once_yearly(self): + + reward_1 = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'First Reward', + 'date': date.today(), + 'reward_type': 'amount', + 'amount': 1000.0, + 'reward_once': True, + 'transfer_type': 'accounting', + 'account_id': self.account_debit.id, + 'journal_id': self.journal.id, + }) + self.env['lines.ids.reward'].create({ + 'employee_reward_id': reward_1.id, + 'employee_id': self.employee.id, + 'percentage': 100.0, + }) + + reward_1.action_submit() + reward_1.action_hrm() + reward_1.action_done() + + reward_2 = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'Second Reward', + 'date': date.today(), + 'reward_type': 'amount', + 'amount': 500.0, + 'reward_once': True, + }) + + with self.assertRaises(UserError): + self.env['lines.ids.reward'].create({ + 'employee_reward_id': reward_2.id, + 'employee_id': self.employee.id, + 'percentage': 100.0, + }) + + def test_03_positive_amount_check(self): + reward = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'Negative Test', + 'date': date.today(), + 'amount': 100.0, + }) + + with self.assertRaises(UserError): + reward.amount = -50.0 + reward.chick_amount_positive() \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_payroll_flow.py b/exp_payroll_custom/tests/test_payroll_flow.py new file mode 100644 index 0000000..cdcc9a1 --- /dev/null +++ b/exp_payroll_custom/tests/test_payroll_flow.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase, Form +from odoo.exceptions import UserError +from odoo.tests import tagged +from datetime import date, datetime, timedelta +from dateutil.relativedelta import relativedelta + + +class TestPayrollAdvanceFlow(TransactionCase): + + def setUp(cls): + super(TestPayrollAdvanceFlow, cls).setUp() + + cls.company = cls.env.company + + cls.account_salary = cls.env['account.account'].create({ + 'name': 'Basic Salary Account', + 'code': '600001', + 'account_type': 'expense', + 'reconcile': True, + }) + + cls.account_payable = cls.env['account.account'].create({ + 'name': 'Salaries Payable', + 'code': '200001', + 'account_type': 'liability_payable', + 'reconcile': True, + }) + + cls.journal = cls.env['account.journal'].create({ + 'name': 'Salary Journal', + 'type': 'general', + 'code': 'SAL', + 'default_account_id': cls.account_payable.id, + }) + + cls.rule_basic = cls.env['hr.salary.rule'].create({ + 'name': 'Basic Salary', + 'sequence': 1, + 'code': 'BASIC', + 'category_id': cls.env.ref('exp_hr_payroll.ALW').id, + 'condition_select': 'none', + 'amount_select': 'code', + 'amount_python_compute': 'result = contract.wage', + 'rule_debit_account_id': cls.account_salary.id, + }) + + cls.rule_net = cls.env['hr.salary.rule'].create({ + 'name': 'Net Salary', + 'sequence': 100, + 'code': 'NET', + 'category_id': cls.env.ref('exp_hr_payroll.DED').id, + 'condition_select': 'none', + 'amount_select': 'code', + 'amount_python_compute': 'result = categories.BASIC + categories.ALW + categories.DED', + 'rule_credit_account_id': cls.account_payable.id, + }) + + cls.structure = cls.env['hr.payroll.structure'].create({ + 'name': 'Standard Structure', + 'type': 'scale', + 'code': 'STRUCT_001', + 'rule_ids': [(4, cls.rule_basic.id), (4, cls.rule_net.id)], + 'transfer_type': 'one_by_one', + }) + + cls.employee = cls.env['hr.employee'].create({ + 'name': 'Test Employee Payroll', + 'first_hiring_date': date.today() - relativedelta(years=1), + 'state': 'open', + }) + + cls.contract = cls.env['hr.contract'].create({ + 'name': 'Contract For Test', + 'employee_id': cls.employee.id, + 'state': 'program_directory', + 'wage': 5000.0, + 'salary_scale': cls.structure.id, + 'journal_id': cls.journal.id, + 'date_start': date.today() - relativedelta(years=1), + }) + def test_01_payslip_compute_and_transfer(self): + + date_from = date.today().replace(day=1) + date_to = date.today() + relativedelta(months=+1, day=1, days=-1) + + payslip = self.env['hr.payslip'].create({ + 'name': 'Test Payslip', + 'employee_id': self.employee.id, + 'date_from': date_from, + 'date_to': date_to, + 'contract_id': self.contract.id, + 'struct_id': self.structure.id + }) + + payslip.compute_sheet() + + self.assertEqual(payslip.state, 'computed', "State should be 'computed' after computing sheet") + + basic_line = payslip.line_ids.filtered(lambda l: l.code == 'BASIC') + self.assertEqual(basic_line.total, 5000.0, "Basic salary should be 5000") + + payslip.compute_totals() + self.assertEqual(payslip.total_allowances, 5000.0, "Total allowances should be calculated correctly") + + payslip.confirm() + self.assertEqual(payslip.state, 'confirmed') + + payslip.transfer() + self.assertEqual(payslip.state, 'transfered') + self.assertTrue(payslip.move_id, "Journal Entry should be created") + self.assertEqual(payslip.move_id.state, 'draft', "Move should be created in draft state initially") + + def test_02_payslip_loans_integration(self): + + payslip = self.env['hr.payslip'].create({ + 'name': 'Loan Payslip', + 'employee_id': self.employee.id, + 'date_from': date.today().replace(day=1), + 'date_to': date.today() + relativedelta(months=+1, day=1, days=-1), + }) + + self.env['payslip.loans'].create({ + 'payslip_loan': payslip.id, + 'name': 'Car Loan', + 'code': 'LOAN01', + 'amount': 500.0, + 'date': date.today(), + 'account_id': self.account_payable.id, + }) + + payslip.compute_totals() + self.assertEqual(payslip.total_loans, 500.0, "Total loans field should calculate sum of loan lines") + + + self.assertEqual(payslip.total_sum, 500.0, "Total sum logic check (depends on allowances setup)") + + def test_03_payslip_run_batch_process(self): + + date_start = date.today().replace(day=1) + date_end = date.today() + relativedelta(months=+1, day=1, days=-1) + + payslip_run = self.env['hr.payslip.run'].create({ + 'name': 'Monthly Run', + 'date_start': date_start, + 'date_end': date_end, + 'salary_scale': self.structure.id, + }) + + payslip_run.check_date_start() + self.assertEqual(payslip_run.date_end, date_end) + + payslip_run.compute_sheet() + + self.assertTrue(payslip_run.slip_ids, "Payslips should be generated for eligible employees") + generated_slip = payslip_run.slip_ids[0] + self.assertEqual(generated_slip.employee_id, self.employee) + self.assertEqual(generated_slip.state, 'computed') + + payslip_run.confirm() + self.assertEqual(generated_slip.state, 'confirmed') + + + payslip_run.transfer() + self.assertEqual(payslip_run.state, 'transfered') + self.assertTrue(payslip_run.move_id or generated_slip.move_id, "Accounting move should be generated") + + def test_04_payslip_withdraw_and_reset(self): + + payslip = self.env['hr.payslip'].create({ + 'name': 'Withdraw Test', + 'employee_id': self.employee.id, + 'date_from': date.today(), + 'date_to': date.today(), + }) + payslip.compute_sheet() + payslip.confirm() + + payslip.withdraw() + + self.assertEqual(payslip.state, 'draft', "State should return to draft after withdraw") + self.assertFalse(payslip.move_id, "Account move should be unlinked/deleted") \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_salary_rule_computation.py b/exp_payroll_custom/tests/test_salary_rule_computation.py new file mode 100644 index 0000000..9455b60 --- /dev/null +++ b/exp_payroll_custom/tests/test_salary_rule_computation.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo import fields +from datetime import date, timedelta + + +class TestSalaryRuleComputation(TransactionCase): + + def setUp(self): + super(TestSalaryRuleComputation, self).setUp() + + self.employee = self.env['hr.employee'].create({'name': 'Test Employee'}) + + self.category_basic = self.env['hr.salary.rule.category'].create({ + 'name': 'Basic', 'code': 'BASIC', 'rule_type': 'allowance' + }) + self.category_allowance = self.env['hr.salary.rule.category'].create({ + 'name': 'Allowance', 'code': 'ALW', 'rule_type': 'allowance' + }) + + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Test Structure', 'code': 'TEST_STRUCT', 'type': 'scale', 'parent_id': False + }) + + + self.rule_basic = self.env['hr.salary.rule'].create({ + 'name': 'Basic Salary', 'code': 'BASIC', + 'category_id': self.category_basic.id, + 'amount_select': 'fix', + 'amount_fix': 1000.0, + 'sequence': 1, + 'salary_type': 'fixed', + }) + + self.rule_housing = self.env['hr.salary.rule'].create({ + 'name': 'Housing Allowance', 'code': 'HOUSING', + 'category_id': self.category_allowance.id, + 'amount_select': 'fix', + 'amount_fix': 500.0, + 'sequence': 2, + 'salary_type': 'fixed', + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract Test', + 'employee_id': self.employee.id, + 'salary_scale': self.structure.id, + 'salary': 5000.0, + 'wage': 5000.0, + 'state': 'open', + }) + + def test_01_compute_fixed_rule(self): + localdict = {'contract': self.contract, 'employee': self.employee} + + amount, qty, rate = self.rule_basic._compute_rule(localdict) + self.assertEqual(amount, 1000.0, "Basic salary fixed amount should be 1000") + + def test_02_compute_percentage_with_related_rules(self): + + rule_percent = self.env['hr.salary.rule'].create({ + 'name': 'Social Security', 'code': 'SOC', + 'category_id': self.category_allowance.id, + 'amount_select': 'percentage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + 'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])], + 'salary_type': 'fixed', + }) + + localdict = {'contract': self.contract, 'employee': self.employee} + + amount, qty, rate = rule_percent._compute_rule(localdict) + + self.assertEqual(amount, 150.0, "Percentage calculation failed. Expected 150.0") + + def test_03_percentage_with_exception_advantage(self): + + today = fields.Date.today() + yesterday = today - timedelta(days=1) + self.env['contract.advantage'].create({ + 'contract_advantage_id': self.contract.id, + 'employee_id': self.employee.id, + 'benefits_discounts': self.rule_basic.id, + 'type': 'exception', + + 'amount': 200.0, + 'date_from': yesterday, + 'date_to': today + timedelta(days=30), + }) + self.contract.invalidate_recordset(['advantages']) + rule_percent = self.env['hr.salary.rule'].create({ + 'name': 'Social Security Modified', 'code': 'SOC_MOD', + 'category_id': self.category_allowance.id, + 'amount_select': 'percentage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + 'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])], + 'salary_type': 'fixed', + }) + + localdict = {'contract': self.contract, 'employee': self.employee} + amount, qty, rate = rule_percent._compute_rule(localdict) + + self.assertEqual(amount, 130.0, f"Exception logic failed. Expected 130.0, Got {amount}") + + def test_04_date_range_validation(self): + + old_date = fields.Date.today() - timedelta(days=365) + + self.env['contract.advantage'].create({ + 'contract_advantage_id': self.contract.id, + 'employee_id': self.employee.id, + 'benefits_discounts': self.rule_basic.id, + 'type': 'exception', + 'amount': 200.0, + 'date_from': old_date, + 'date_to': old_date + timedelta(days=30), + }) + + rule_percent = self.env['hr.salary.rule'].create({ + 'name': 'Social Security Date', 'code': 'SOC_DATE', + 'category_id': self.category_allowance.id, + 'amount_select': 'percentage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + 'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])], + 'salary_type': 'fixed', + }) + + localdict = {'contract': self.contract, 'employee': self.employee} + amount, qty, rate = rule_percent._compute_rule(localdict) + + self.assertEqual(amount, 150.0, "Date validation failed. Expired exception should be ignored.") + + def test_05_salary_type_related_levels(self): + + level_1 = self.env['hr.payroll.structure'].create({ + 'name': 'Level 1', 'code': 'LVL1', 'type': 'level', 'parent_id': False + }) + self.contract.salary_level = level_1.id + + + rule_level = self.env['hr.salary.rule'].create({ + 'name': 'Level Rule', 'code': 'LVL_RULE', + 'category_id': self.category_basic.id, + 'amount_select': 'fix', + 'salary_type': 'related_levels', + }) + + self.env['related.salary.amount'].create({ + 'salary_rule_id': rule_level.id, + 'salary_scale_level': level_1.id, + 'salary': 2500.0, + }) + rule_level.invalidate_recordset(['salary_amount_ids']) + self.contract.invalidate_recordset(['salary_level']) + localdict = {'contract': self.contract, 'employee': self.employee} + amount, qty, rate = rule_level._compute_rule(localdict) + + self.assertEqual(amount, 2500.0, "Related Level salary calculation failed.") \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_salary_scale.py b/exp_payroll_custom/tests/test_salary_scale.py new file mode 100644 index 0000000..1bca45a --- /dev/null +++ b/exp_payroll_custom/tests/test_salary_scale.py @@ -0,0 +1,96 @@ +from odoo.tests.common import TransactionCase +from odoo import fields +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + +class TestHrContractSalaryScale(TransactionCase): + def setUp(self): + super().setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + }) + + allow_cat = self.env['hr.salary.rule.category'].create({ + 'name': 'Allowance', + 'code': 'ALW', + 'rule_type': 'allowance' + }) + ded_cat = self.env['hr.salary.rule.category'].create({ + 'name': 'Deduction', + 'code': 'DED', + 'rule_type': 'deduction' + }) + + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Scale Structure', + 'type': 'scale', + 'code': 'SCALE_TEST', + 'parent_id': False, + }) + + self.allow_rule = self.env['hr.salary.rule'].create({ + 'name': 'Allowance Rule', + 'code': 'ALLOW1', + 'category_id': allow_cat.id, + 'amount_select': 'fix', + 'amount_fix': 1000, + 'quantity': '1.0', + 'condition_select': 'none' + }) + + self.ded_rule = self.env['hr.salary.rule'].create({ + 'name': 'Deduction Rule', + 'code': 'DED1', + 'category_id': ded_cat.id, + 'amount_select': 'fix', + 'amount_fix': 200, + 'quantity': '1.0', + 'condition_select': 'none' + }) + + self.structure.rule_ids = [(6, 0, [self.allow_rule.id, self.ded_rule.id])] + + self.contract = self.env['hr.contract'].create({ + 'name': 'Test Contract', + 'employee_id': self.employee.id, + 'salary_scale': self.structure.id, + 'salary': 5000, + }) + + def test_compute_function_basic(self): + + self.contract.compute_function() + + + self.assertEqual(self.contract.total_allowance, 1000.0) + def test_salary_group_constraint(self): + group = self.env['hr.payroll.structure'].create({ + 'name': 'Group A', + 'gread_min': 3000, + 'gread_max': 6000, + 'code': 'SCALE_TEST' + + }) + + self.contract.salary_group = group.id + self.contract.salary = 5000 + + def test_compute_move_type_one_by_one(self): + self.structure.transfer_type = 'one_by_one' + self.contract.salary_scale = self.structure.id + + self.contract.compute_move_type() + + self.assertTrue(self.contract.required_condition) + + def test_exception_amount_greater_than_rule(self): + rule = self.allow_rule + + advantage = self.env['contract.advantage'].create({ + 'contract_advantage_id': self.contract.id, + 'benefits_discounts': rule.id, + 'type': 'exception', + 'amount': 2000, + 'date_from': fields.Date.today(), + }) diff --git a/hr_holidays_public/models/hr_government_exit_return_custom.py b/hr_holidays_public/models/hr_government_exit_return_custom.py index 7744253..9412608 100644 --- a/hr_holidays_public/models/hr_government_exit_return_custom.py +++ b/hr_holidays_public/models/hr_government_exit_return_custom.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError,UserError class HRHolidays(models.Model): diff --git a/hr_holidays_public/models/return_from_leave.py b/hr_holidays_public/models/return_from_leave.py index 3f57870..27d41ef 100644 --- a/hr_holidays_public/models/return_from_leave.py +++ b/hr_holidays_public/models/return_from_leave.py @@ -6,7 +6,7 @@ import calendar from dateutil.relativedelta import relativedelta from odoo.tools.translate import _ from odoo import models, fields, api -from odoo.exceptions import UserError +from odoo.exceptions import UserError ,ValidationError class ReturnFromLeave(models.Model): @@ -73,7 +73,7 @@ class ReturnFromLeave(models.Model): def _chick_leave_type(self): for rec in self: if rec.leave_request_id.holiday_status_id.leave_type == 'annual' and rec.decision == 'other': - raise exceptions.ValidationError(_("Sorry Cannot be Create an Annual Leave from the same annual Leave")) + raise ValidationError(_("Sorry Cannot be Create an Annual Leave from the same annual Leave")) @api.depends('leave_request_id') def _compute_dates_of_leave(self): @@ -132,7 +132,7 @@ class ReturnFromLeave(models.Model): else: request.diff_days = len(list(set([xd for xd in exceeded_dates if xd not in event_dates and xd not in wkns_dates]))) else: - raise exceptions.ValidationError(_("Sorry this leave ends by %s.\n" + raise ValidationError(_("Sorry this leave ends by %s.\n" "If you plan for an early return kindly apply for leave " "cancellation.") % request.leave_request_id.date_to) else: @@ -155,7 +155,7 @@ class ReturnFromLeave(models.Model): self.settling_leave_id.draft_state() self.settling_leave_id.unlink() else: - raise exceptions.ValidationError(_("Sorry The link leave cannot be deleted %s After approved") + raise ValidationError(_("Sorry The link leave cannot be deleted %s After approved") % self.settling_leave_id.holiday_status_id.name) self.state = 'draft' self.leave_request_id.return_from_leave = False @@ -166,14 +166,14 @@ class ReturnFromLeave(models.Model): request_id = rec.leave_request_id if rec.decision == 'law': # create unpaid leave if not request_id.holiday_status_id.unpaid_holiday_id: - raise exceptions.ValidationError(_("Sorry no unpaid leave is defined for %s leave kindly set one") + raise ValidationError(_("Sorry no unpaid leave is defined for %s leave kindly set one") % request_id.holiday_status_id.name) status_id = request_id.holiday_status_id.unpaid_holiday_id.id elif rec.decision == 'deduct': # Deduct from leave balance status_id = request_id.holiday_status_id.id elif rec.decision == 'other': # create annual leave if not request_id.holiday_status_id.annual_holiday_id: - raise exceptions.ValidationError(_("Sorry no annual leave is defined for %s leave kindly set one") + raise ValidationError(_("Sorry no annual leave is defined for %s leave kindly set one") % request_id.holiday_status_id.name) status_id = request_id.holiday_status_id.annual_holiday_id.id @@ -183,7 +183,7 @@ class ReturnFromLeave(models.Model): ('check_allocation_view', '=', 'balance') ], order='id desc', limit=1).remaining_leaves or 0.0 if balance < rec.diff_days: - raise exceptions.ValidationError( + raise ValidationError( _("Sorry your %s leave balance it is not enough to deduct from it, The balance is %s.") % (request_id.holiday_status_id.name, round(balance, 2))) @@ -225,7 +225,7 @@ class ReturnFromLeave(models.Model): if self.decision == 'deduct': self.settling_leave_id.financial_manager() elif self.leave_request_id.state != 'validate1': - raise exceptions.ValidationError( + raise ValidationError( _("Sorry %s leave is not approved yet. kindly approve it first") % ( self.leave_request_id.display_name)) self.leave_request_id.remove_delegated_access() @@ -240,7 +240,7 @@ class ReturnFromLeave(models.Model): leave.settling_leave_id.draft_state() leave.settling_leave_id.unlink() else: - raise exceptions.ValidationError(_("Sorry The link leave cannot be deleted %s After approved") + raise ValidationError(_("Sorry The link leave cannot be deleted %s After approved") % leave.settling_leave_id.holiday_status_id.name) self.state = 'refuse' diff --git a/hr_holidays_public/tests/__init__.py b/hr_holidays_public/tests/__init__.py new file mode 100644 index 0000000..f8a724b --- /dev/null +++ b/hr_holidays_public/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_holidays_custom \ No newline at end of file diff --git a/hr_holidays_public/tests/test_hr_holidays_custom.py b/hr_holidays_public/tests/test_hr_holidays_custom.py new file mode 100644 index 0000000..ac1df70 --- /dev/null +++ b/hr_holidays_public/tests/test_hr_holidays_custom.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged +from datetime import date, timedelta, datetime + + +@tagged('post_install', '-at_install') +class TestHRHolidaysCustom(TransactionCase): + + def setUp(self): + super(TestHRHolidaysCustom, self).setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Employee Test', + 'gender': 'male', + 'first_hiring_date': date.today() - timedelta(days=365 * 2), + 'state': 'open', + }) + + self.replacement_employee = self.env['hr.employee'].create({ + 'name': 'Replacement Employee', + 'gender': 'male', + 'first_hiring_date': date.today() - timedelta(days=365), + 'state': 'open', + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract Test', + 'employee_id': self.employee.id, + 'wage': 5000, + 'state': 'open', + 'date_start': date.today() - timedelta(days=365 * 2), + 'emp_type': 'saudi', # ضروري لفلتر السكيجوال + }) + self.employee.contract_id = self.contract + + self.replacement_contract = self.env['hr.contract'].create({ + 'name': 'Replacement Contract', + 'employee_id': self.replacement_employee.id, + 'wage': 4000, + 'state': 'open', + 'date_start': date.today() - timedelta(days=365), + 'emp_type': 'saudi', + }) + self.replacement_employee.contract_id = self.replacement_contract + + self.holiday_status = self.env['hr.holidays.status'].create({ + 'name': 'Annual Leave Test', + 'leave_type': 'annual', + 'limit': False, + 'number_of_days': 30, + 'active': True, + 'alternative_days': 2, + 'alternative_chick': False, + }) + + self.env['hr.holidays'].create({ + 'name': 'Balance Record', + 'holiday_status_id': self.holiday_status.id, + 'employee_id': self.employee.id, + 'type': 'add', + 'check_allocation_view': 'balance', + 'remaining_leaves': 30.0, + 'state': 'validate', + }) + + self.env['hr.holidays'].create({ + 'name': 'Replacement Balance', + 'holiday_status_id': self.holiday_status.id, + 'employee_id': self.replacement_employee.id, + 'type': 'add', + 'check_allocation_view': 'balance', + 'remaining_leaves': 10.0, + 'state': 'validate', + }) + + def test_01_leave_workflow_and_ticket_creation(self): + leave_request = self.env['hr.holidays'].create({ + 'name': 'Leave Request with Ticket', + 'employee_id': self.employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': datetime.now().strftime('%Y-%m-%d 08:00:00'), + 'date_to': (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d 17:00:00'), + 'number_of_days_temp': 5, + 'type': 'remove', + 'check_allocation_view': 'allocation', + 'issuing_ticket': 'yes', + 'ticket_cash_request_for': 'employee', + }) + + leave_request.confirm() + leave_request.hr_manager() + leave_request.approved() + leave_request.financial_manager() + + self.assertTrue(leave_request.request_done) + ticket = self.env['hr.ticket.request'].search([('leave_request_id', '=', leave_request.id)]) + self.assertTrue(ticket) + + def test_02_replacement_employee_constraint(self): + today = datetime.now() + self.env['hr.holidays'].create({ + 'name': 'Replacement Employee Leave', + 'employee_id': self.replacement_employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': today.strftime('%Y-%m-%d 08:00:00'), + 'date_to': (today + timedelta(days=2)).strftime('%Y-%m-%d 17:00:00'), + 'type': 'remove', + 'state': 'validate1', + }) + + with self.assertRaises(UserError): + self.env['hr.holidays'].create({ + 'name': 'Main Employee Leave', + 'employee_id': self.employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': today.strftime('%Y-%m-%d 08:00:00'), + 'date_to': (today + timedelta(days=2)).strftime('%Y-%m-%d 17:00:00'), + 'type': 'remove', + 'replace_by': self.replacement_employee.id, + }) + + def test_03_check_balance_limit(self): + with self.assertRaises(UserError): + leave = self.env['hr.holidays'].create({ + 'name': 'Exceed Balance Leave', + 'employee_id': self.employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': datetime.now().strftime('%Y-%m-%d 08:00:00'), + 'date_to': (datetime.now() + timedelta(days=40)).strftime('%Y-%m-%d 17:00:00'), + 'number_of_days_temp': 41, + 'type': 'remove', + }) + leave._check_number_of_days() + + def test_04_scheduler_queue_allocation(self): + + monthly_leave_type = self.env['hr.holidays.status'].create({ + 'name': 'Monthly Leave', + 'leave_type': 'annual', + 'balance_type': 'monthly', + 'leave_annual_type': 'open_balance', + 'company_id': self.env.company.id, + 'emp_type': 'all', + 'alternative_days': 2, + 'alternative_chick': False, + 'number_of_days': 0, + 'duration_ids': [(0, 0, { + 'name': 'Level 1', + 'date_from': 0, + 'date_to': 10, + 'duration': 30 + })] + }) + + + for i in range(2): + self.env['hr.holidays'].create({ + 'name': f'Dummy Allocation {i}', + 'holiday_status_id': monthly_leave_type.id, + 'employee_id': self.replacement_employee.id, + 'type': 'add', + 'check_allocation_view': 'balance', + 'number_of_days_temp': 0, + 'state': 'confirm' + }) + + self.env['hr.holidays'].process_holidays_scheduler_queue() + + allocation = self.env['hr.holidays'].search([ + ('employee_id', '=', self.employee.id), + ('holiday_status_id', '=', monthly_leave_type.id), + ('check_allocation_view', '=', 'balance'), + ('type', '=', 'add') + ]) + + self.assertTrue(allocation, "Scheduler should create allocation record") + self.assertGreater(allocation.remaining_leaves, 0.0) \ No newline at end of file From 87bab36cb2adbfbd9002f9ca42e58cd0dc2f2da9 Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Wed, 24 Dec 2025 01:32:32 +0300 Subject: [PATCH 2/2] make unit test hr base --- .../models/employee_overtime_request.py | 4 +- employee_requests/tests/__init__.py | 6 +- exp_payroll_custom/models/hr_salary_rules.py | 546 +++++++----------- .../tests/test_salary_rule_computation.py | 8 +- 4 files changed, 233 insertions(+), 331 deletions(-) diff --git a/employee_requests/models/employee_overtime_request.py b/employee_requests/models/employee_overtime_request.py index 9204fe6..d0301d0 100644 --- a/employee_requests/models/employee_overtime_request.py +++ b/employee_requests/models/employee_overtime_request.py @@ -227,7 +227,7 @@ class employee_overtime_request(models.Model): 'debit': record.price_hour, 'account_id': account_debit_id.id, 'partner_id': record.employee_id.user_id.partner_id.id, - # 'analytic_account_id': analytic_account_id.id, + 'analytic_account_id': analytic_account_id.id, } credit_line_vals = { 'name': record.employee_id.name, @@ -242,6 +242,8 @@ class employee_overtime_request(models.Model): 'date': item.request_date, 'ref': record.employee_id.name, 'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)], + 'res_model': 'employee.overtime.request', + 'res_id': self.id }) record.account_id = account_debit_id.id diff --git a/employee_requests/tests/__init__.py b/employee_requests/tests/__init__.py index 80a14d2..7430a82 100644 --- a/employee_requests/tests/__init__.py +++ b/employee_requests/tests/__init__.py @@ -1,4 +1,4 @@ -# from . import test_overtime_process -# from . import test_employee_department_jobs -# from . import test_hr_clearance_form +from . import test_overtime_process +from . import test_employee_department_jobs +from . import test_hr_clearance_form from . import test_hr_personal_permission \ No newline at end of file diff --git a/exp_payroll_custom/models/hr_salary_rules.py b/exp_payroll_custom/models/hr_salary_rules.py index 20b8d9f..e76d280 100644 --- a/exp_payroll_custom/models/hr_salary_rules.py +++ b/exp_payroll_custom/models/hr_salary_rules.py @@ -80,346 +80,246 @@ class HrSalaryRules(models.Model): raise UserError(_("The Salary Rule is Not Deduction")) def _compute_rule(self, localdict): - - self.ensure_one() payslip = localdict.get('payslip') contract = localdict.get('contract') - current_date = fields.Date.today() - - fix_amount_value = self.amount_fix if hasattr(self, 'amount_fix') else getattr(self, 'fixed_amount', 0.0) - - - def get_related_amount(): - salary_type = getattr(self, 'salary_type', 'fixed') - if salary_type == 'related_levels' and contract.salary_level: - related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_level.id == contract.salary_level.id) - return related.salary if related else 0.0 - elif salary_type == 'related_groups' and contract.salary_group: - related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_group.id == contract.salary_group.id) - return related.salary if related else 0.0 - elif salary_type == 'related_degrees' and contract.salary_degree: - related = self.salary_amount_ids.filtered( - lambda r: r.salary_scale_degree.id == contract.salary_degree.id) - return related.salary if related else 0.0 - return fix_amount_value - if self.amount_select == 'percentage': - total_percent = 0.0 - related_rules = getattr(self, 'related_benefits_discounts', []) - - if related_rules: - for line in related_rules: + total_percent, total = 0, 0 + if self.related_benefits_discounts: + for line in self.related_benefits_discounts: calc_line = line._compute_rule(localdict)[0] - line_in_advantages = False - advantages = contract.advantages if contract else [] - if advantages: - con = next((adv for adv in advantages if adv.benefits_discounts.id == line.id), None) - if con: - line_in_advantages = True - is_valid_date = False - - if payslip: - if con.date_from > payslip.date_from: - total_percent += calc_line - continue - elif (not con.date_to) or (con.date_to >= payslip.date_to): - is_valid_date = True - else: - if con.date_from <= current_date: - if (not con.date_to) or (con.date_to >= current_date): - is_valid_date = True - - if is_valid_date: - total_to_add = 0.0 - if con.type == 'exception': - if con.amount < calc_line: - total_to_add = calc_line - con.amount + if line.amount_select == 'fix': + if contract.advantages: + for con in contract.advantages: + if line.id == con.benefits_discounts.id: + if payslip: + if con.date_from > payslip.date_from: + total_percent = calc_line + elif con.date_to is not None and str( + con.date_to) >= payslip.date_to or con.date_to is None: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent += total else: - total_to_add = 0.0 - elif con.type == 'customize': - total_to_add = con.amount - - if line.amount_select == 'percentage': - total_percent -= calc_line - total_percent += total_to_add + if str(con.date_from) < str(datetime.now().date()): + if con.date_to: + if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + >= datetime.now().date().month or not con.date_to: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent += total else: - total_percent += total_to_add - else: - total_percent += calc_line + total_percent = calc_line + else: + total_percent += calc_line - if not line_in_advantages: - total_percent += calc_line + elif line.amount_select == 'percentage': + if contract.advantages: + for con in contract.advantages: + if line.id == con.benefits_discounts.id: + if payslip: + if con.date_from > payslip.date_from: + total_percent = calc_line + elif con.date_to is not None and str( + con.date_to) >= payslip.date_to or con.date_to is None: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent -= calc_line + total_percent += total + else: + if str(con.date_from) < str(datetime.now().date()): + if con.date_to: + if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + >= datetime.now().date().month or not con.date_to: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent -= calc_line + total_percent += total + else: + if con.type != 'exception': + total_percent += calc_line + break + else: + total_percent += calc_line + else: + if contract.advantages: + for con in contract.advantages: + if line.id == con.benefits_discounts.id: + if payslip: + if con.date_from > payslip.date_from: + total_percent = calc_line + elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent = 0 + total_percent += total + else: + if con.date_from < (datetime.now().date()): + if con.date_to: + if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + >= datetime.now().date().month or not con.date_to: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent = 0 + total_percent += total + else: + if datetime.strptime(str(con.date_from), + "%Y-%m-%d").date().month >= datetime.now().date().month: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + calc_line + total_percent = 0 + total_percent += total + + else: + if not total_percent: + total_percent = calc_line + else: + total_percent += calc_line if total_percent: - try: - qty = float(safe_eval(self.quantity, localdict)) - rate = self.amount_percentage - return float(total_percent * self.amount_percentage / 100), qty, rate - except Exception as e: - raise UserError(_('Error calculating percentage rule %s: %s') % (self.name, e)) + if self.salary_type == 'fixed': + try: + return float(total_percent * self.amount_percentage / 100), \ + float(safe_eval(self.quantity, localdict)), self.amount_percentage + except: + raise UserError( + _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + elif self.salary_type == 'related_levels': + levels_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_level.id == contract.salary_level.id) + if levels_ids: + for l in levels_ids: + try: + return float(l.salary * total_percent / 100), float( + safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_groups': + groups_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_group.id == contract.salary_group.id) + if groups_ids: + for g in groups_ids: + try: + return float(g.salary * total_percent / 100), float( + safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_degrees': + degrees_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + if degrees_ids: + for d in degrees_ids: + try: + return float(d.salary * total_percent / 100), float( + safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + else: + return 0, 0, 0 else: - return 0.0, 0.0, 0.0 + try: + return 0, 0, 0 + except: + raise UserError(_('There is no total for rule : %s') % self.name) elif self.amount_select == 'fix': - try: - qty = float(safe_eval(self.quantity, localdict)) - amount = get_related_amount() - return amount, qty, 100.0 - except Exception as e: - raise UserError(_('Error computing fix rule %s: %s') % (self.name, e)) + if self.salary_type == 'fixed': + try: + return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + elif self.salary_type == 'related_levels': + levels_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_level.id == contract.salary_level.id) + if levels_ids: + for l in levels_ids: + try: + return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_groups': + groups_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_group.id == contract.salary_group.id) + if groups_ids: + for g in groups_ids: + try: + return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_degrees': + degrees_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + if degrees_ids: + for d in degrees_ids: + try: + return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + else: + return 0, 0, 0 + else: + raise UserError(_('Error, Select Salary type to calculate rule')) else: try: safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) - return float(localdict.get('result', 0.0)), \ - localdict.get('result_qty', 1.0), \ - localdict.get('result_rate', 100.0) - except Exception as e: - raise UserError(_('Error computing python rule %s: %s') % (self.name, e)) - # Override function compute rule in hr salary rule - - # def _compute_rule(self, localdict): - # payslip = localdict.get('payslip') - # contract = localdict.get('contract') - # if self.amount_select == 'percentage': - # total_percent, total = 0, 0 - # if self.related_benefits_discounts: - # for line in self.related_benefits_discounts: - # calc_line = line._compute_rule(localdict)[0] - # - # if line.amount_select == 'fix': - # if contract.advantages: - # for con in contract.advantages: - # if line.id == con.benefits_discounts.id: - # if payslip: - # if con.date_from > payslip.date_from: - # total_percent = calc_line - # elif con.date_to is not None and str( - # con.date_to) >= payslip.date_to or con.date_to is None: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent += total - # else: - # if str(con.date_from) < str(datetime.now().date()): - # if con.date_to: - # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - # >= datetime.now().date().month or not con.date_to: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent += total - # else: - # total_percent = calc_line - # else: - # total_percent += calc_line - # - # elif line.amount_select == 'percentage': - # if contract.advantages: - # for con in contract.advantages: - # if line.id == con.benefits_discounts.id: - # if payslip: - # if con.date_from > payslip.date_from: - # total_percent = calc_line - # elif con.date_to is not None and str( - # con.date_to) >= payslip.date_to or con.date_to is None: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent -= calc_line - # total_percent += total - # else: - # if str(con.date_from) < str(datetime.now().date()): - # if con.date_to: - # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - # >= datetime.now().date().month or not con.date_to: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent -= calc_line - # total_percent += total - # else: - # if con.type != 'exception': - # total_percent += calc_line - # break - # else: - # total_percent += calc_line - # - # else: - # if contract.advantages: - # for con in contract.advantages: - # if line.id == con.benefits_discounts.id: - # if payslip: - # if con.date_from > payslip.date_from: - # total_percent = calc_line - # elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent = 0 - # total_percent += total - # else: - # if con.date_from < (datetime.now().date()): - # if con.date_to: - # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - # >= datetime.now().date().month or not con.date_to: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent = 0 - # total_percent += total - # else: - # if datetime.strptime(str(con.date_from), - # "%Y-%m-%d").date().month >= datetime.now().date().month: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount + calc_line - # total_percent = 0 - # total_percent += total - # - # else: - # if not total_percent: - # total_percent = calc_line - # else: - # total_percent += calc_line - # if total_percent: - # if self.salary_type == 'fixed': - # try: - # return float(total_percent * self.amount_percentage / 100), \ - # float(safe_eval(self.quantity, localdict)), self.amount_percentage - # except: - # raise UserError( - # _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # elif self.salary_type == 'related_levels': - # levels_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_level.id == contract.salary_level.id) - # if levels_ids: - # for l in levels_ids: - # try: - # return float(l.salary * total_percent / 100), float( - # safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_groups': - # groups_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_group.id == contract.salary_group.id) - # if groups_ids: - # for g in groups_ids: - # try: - # return float(g.salary * total_percent / 100), float( - # safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_degrees': - # degrees_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - # if degrees_ids: - # for d in degrees_ids: - # try: - # return float(d.salary * total_percent / 100), float( - # safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # else: - # return 0, 0, 0 - # else: - # try: - # return 0, 0, 0 - # except: - # raise UserError(_('There is no total for rule : %s') % self.name) - # - # elif self.amount_select == 'fix': - # if self.salary_type == 'fixed': - # try: - # return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # elif self.salary_type == 'related_levels': - # levels_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_level.id == contract.salary_level.id) - # if levels_ids: - # for l in levels_ids: - # try: - # return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_groups': - # groups_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_group.id == contract.salary_group.id) - # if groups_ids: - # for g in groups_ids: - # try: - # return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_degrees': - # degrees_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - # if degrees_ids: - # for d in degrees_ids: - # try: - # return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # else: - # return 0, 0, 0 - # else: - # raise UserError(_('Error, Select Salary type to calculate rule')) - # - # else: - # try: - # safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) - # return float(localdict['result']), 'result_qty' in localdict and localdict[ - # 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 - # except: - # raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) + return float(localdict['result']), 'result_qty' in localdict and localdict[ + 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 + except: + raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) class SalaryConfig(models.Model): diff --git a/exp_payroll_custom/tests/test_salary_rule_computation.py b/exp_payroll_custom/tests/test_salary_rule_computation.py index 9455b60..5632d52 100644 --- a/exp_payroll_custom/tests/test_salary_rule_computation.py +++ b/exp_payroll_custom/tests/test_salary_rule_computation.py @@ -28,7 +28,7 @@ class TestSalaryRuleComputation(TransactionCase): 'name': 'Basic Salary', 'code': 'BASIC', 'category_id': self.category_basic.id, 'amount_select': 'fix', - 'amount_fix': 1000.0, + 'fixed_amount': 1000, 'sequence': 1, 'salary_type': 'fixed', }) @@ -37,7 +37,7 @@ class TestSalaryRuleComputation(TransactionCase): 'name': 'Housing Allowance', 'code': 'HOUSING', 'category_id': self.category_allowance.id, 'amount_select': 'fix', - 'amount_fix': 500.0, + 'fixed_amount': 500, 'sequence': 2, 'salary_type': 'fixed', }) @@ -85,7 +85,7 @@ class TestSalaryRuleComputation(TransactionCase): 'benefits_discounts': self.rule_basic.id, 'type': 'exception', - 'amount': 200.0, + 'amount': 200, 'date_from': yesterday, 'date_to': today + timedelta(days=30), }) @@ -114,7 +114,7 @@ class TestSalaryRuleComputation(TransactionCase): 'employee_id': self.employee.id, 'benefits_discounts': self.rule_basic.id, 'type': 'exception', - 'amount': 200.0, + 'amount': 200, 'date_from': old_date, 'date_to': old_date + timedelta(days=30), })