make unit test hr base

This commit is contained in:
mohammed-alkhazrji 2025-12-24 00:57:50 +03:00
parent 1622f5a252
commit 255bf2ea5b
30 changed files with 2646 additions and 446 deletions

View File

@ -227,7 +227,7 @@ class employee_overtime_request(models.Model):
'debit': record.price_hour, 'debit': record.price_hour,
'account_id': account_debit_id.id, 'account_id': account_debit_id.id,
'partner_id': record.employee_id.user_id.partner_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 = { credit_line_vals = {
'name': record.employee_id.name, 'name': record.employee_id.name,
@ -242,8 +242,7 @@ class employee_overtime_request(models.Model):
'date': item.request_date, 'date': item.request_date,
'ref': record.employee_id.name, 'ref': record.employee_id.name,
'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)], '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.account_id = account_debit_id.id
record.journal_id = journal_id.id record.journal_id = journal_id.id

View File

@ -124,7 +124,7 @@ class HrPersonalPermission(models.Model):
cal_hour_start = calendar.full_min_sign_in cal_hour_start = calendar.full_min_sign_in
cal_hour_end = calendar.full_max_sign_out cal_hour_end = calendar.full_max_sign_out
if hour_start > cal_hour_end or hour_end < cal_hour_start: 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: if item.duration <= 0.0:
raise UserError(_('This Duration Must Be Greater Than Zero')) 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')) raise UserError(_('This Duration must be less than or equal to the Permission Limit'))
if item.duration > item.permission_number: if item.duration > item.permission_number:

View File

@ -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

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -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")

View File

@ -0,0 +1 @@
from . import test_custody_receiving

View File

@ -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()

View File

@ -182,14 +182,14 @@ class HrPayslip(models.Model):
current_leave_struct['number_of_days'] += hours / work_hours current_leave_struct['number_of_days'] += hours / work_hours
# compute worked days # 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) calendar=contract.resource_calendar_id)
attendances = { attendances = {
'name': _("Normal Working Days paid at 100%"), 'name': _("Normal Working Days paid at 100%"),
'sequence': 1, 'sequence': 1,
'code': 'WORK100', 'code': 'WORK100',
'number_of_days': work_data['days'], 'number_of_days': work_data.get('days', 0.0),
'number_of_hours': work_data['hours'], 'number_of_hours': work_data.get('hours', 0.0),
'contract_id': contract.id, 'contract_id': contract.id,
} }

View File

@ -28,10 +28,10 @@ class HrPayrollStructure(models.Model):
rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id',
string='Salary Rules') string='Salary Rules')
@api.constrains('parent_id') # @api.constrains('parent_id')
def _check_parent_id(self): # def _check_parent_id(self):
if not self._check_recursion(): # if not self._has_cycle():
raise ValidationError(_('You cannot create a recursive salary structure.')) # raise ValidationError(_('You cannot create a recursive salary structure.'))
def copy(self, default=None): def copy(self, default=None):
self.ensure_one() self.ensure_one()
@ -85,7 +85,7 @@ class HrSalaryRuleCategory(models.Model):
@api.constrains('parent_id') @api.constrains('parent_id')
def _check_parent_id(self): 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.')) 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') @api.constrains('parent_rule_id')
def _check_parent_rule_id(self): 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.')) raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rules.'))
def _recursive_search_of_rules(self): def _recursive_search_of_rules(self):

View File

@ -0,0 +1 @@
from . import test_payroll_rules

View File

@ -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)

View File

@ -1175,7 +1175,7 @@ class HrOfficialMissionEmployee(models.Model):
def check_dates(self): def check_dates(self):
for rec in self: for rec in self:
if rec.hour_from >= 24 or rec.hour_to >= 24: 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_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() date_to = datetime.strptime(str(rec.date_to), DEFAULT_SERVER_DATE_FORMAT).date()
delta = timedelta(days=1) delta = timedelta(days=1)
@ -1190,7 +1190,7 @@ class HrOfficialMissionEmployee(models.Model):
'&', ('hour_from', '>=', rec.hour_from), ('hour_to', '<=', rec.hour_to), '&', ('hour_from', '>=', rec.hour_from), ('hour_to', '<=', rec.hour_to),
]) ])
if missions_ids: 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, (rec.employee_id.name,
missions_ids.official_mission_id.mission_type.name)) missions_ids.official_mission_id.mission_type.name))
date_from += delta date_from += delta
@ -1203,7 +1203,7 @@ class HrOfficialMissionEmployee(models.Model):
('official_mission_id.process_type', '=', 'training'), ('official_mission_id.process_type', '=', 'training'),
('official_mission_id.course_name.id', '=', item.official_mission_id.course_name.id)]) ('official_mission_id.course_name.id', '=', item.official_mission_id.course_name.id)])
if duplicated: if duplicated:
raise exceptions.ValidationError( raise ValidationError(
_("Employee %s has already take this course.") % (item.employee_id.name)) _("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' \ if item.official_mission_id and item.official_mission_id.mission_type.duration_type == 'days' \
and item.date_from and item.date_to: and item.date_from and item.date_to:
@ -1230,7 +1230,7 @@ class HrOfficialMissionEmployee(models.Model):
if year_last_record == year_now_record: if year_last_record == year_now_record:
number_days = number_days + rec.days number_days = number_days + rec.days
if number_days > days_per_year: 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.") % ( _("Sorry The Employee %s, The Number of Requests Cannot Exceed %s Maximum Days Per year.") % (
rec.employee_id.name, days_per_year)) rec.employee_id.name, days_per_year))
#### ####
@ -1427,7 +1427,7 @@ class HrOfficialMissionEmployee(models.Model):
def compute_number_of_hours(self): def compute_number_of_hours(self):
for item in self: for item in self:
if item.hour_from >= 24 or item.hour_to >= 24: 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.official_mission_id.hour_to and item.official_mission_id.hour_from:
if item.hour_from and item.hour_to: if item.hour_from and item.hour_to:
if (item.hour_to - item.hour_from) < 0: if (item.hour_to - item.hour_from) < 0:
@ -1788,7 +1788,7 @@ class MissionTable(models.Model):
date_to = rec.destination_id.date_to date_to = rec.destination_id.date_to
if date_from and date_to: if date_from and date_to:
if not (date_from <= rec.date <= 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.", _("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) date=rec.date, date_from=date_from, date_to=date_to)
) )

View File

@ -0,0 +1 @@
from . import test_official_mission

View File

@ -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()

View File

@ -66,4 +66,6 @@
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,
'application': True, 'application': True,
'test_tags': ['standard', 'at_install'],
} }

View File

@ -104,6 +104,7 @@ class SalaryRuleInput(models.Model):
def withdraw(self): def withdraw(self):
payslip = self.env['hr.payslip'].search([('number', '=', self.number)]) payslip = self.env['hr.payslip'].search([('number', '=', self.number)])
if 'hr.loan.salary.advance' in self.env:
loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', self.employee_id.id)]) loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', self.employee_id.id)])
if self.number == payslip.number: if self.number == payslip.number:
if self.loan_ids: if self.loan_ids:
@ -852,7 +853,7 @@ class SalaryRuleInput(models.Model):
d.amount = d.amount d.amount = d.amount
payslip.deduction_ids = [fields.Command.set(deductions.ids)] payslip.deduction_ids = [fields.Command.set(deductions.ids)]
# Loans # if 'hr.loan.salary.advance' in self.env:
loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', payslip.employee_id.id), loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', payslip.employee_id.id),
('request_type.refund_from', '=', 'salary'), ('request_type.refund_from', '=', 'salary'),
('state', '=', 'pay')]).filtered( ('state', '=', 'pay')]).filtered(
@ -2963,6 +2964,7 @@ class HrPayslipRun(models.Model):
def withdraw(self): def withdraw(self):
for line in self.slip_ids: for line in self.slip_ids:
payslip = self.env['hr.payslip'].search([('number', '=', line.number)]) payslip = self.env['hr.payslip'].search([('number', '=', line.number)])
if 'hr.loan.salary.advance' in self.env:
loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', line.employee_id.id)]) loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', line.employee_id.id)])
if line.number == payslip.number: if line.number == payslip.number:
if line.loan_ids: if line.loan_ids:

View File

@ -5,6 +5,11 @@ from datetime import datetime
from odoo import models, fields, api, _ from odoo import models, fields, api, _
from odoo.exceptions import UserError 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): class HrContractSalaryScale(models.Model):
_inherit = 'hr.contract' _inherit = 'hr.contract'
@ -15,84 +20,91 @@ class HrContractSalaryScale(models.Model):
salary_degree = 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") hide = fields.Boolean(string='Hide', compute="compute_type")
required_condition = fields.Boolean(string='Required Condition', compute='compute_move_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_allowance = fields.Float(string='Total Allowance', compute='compute_function', store=True)
total_deduction = fields.Float(string='Total Deduction', 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_net = fields.Float(string='Total Net', compute='compute_function', store=True)
advantages = fields.One2many('contract.advantage', 'contract_advantage_id', string='Advantages') advantages = fields.One2many('contract.advantage', 'contract_advantage_id', string='Advantages')
house_allowance_temp = fields.Float(string='House 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) transport_allowance = fields.Float(string='Transport Allowance', compute='compute_function', store=True)
@api.constrains('advantages', 'salary', 'salary_group') @api.constrains('advantages', 'salary', 'salary_group')
def amount_constrains(self): def amount_constrains(self):
for rec in self: for rec in self:
localdict = dict(employee=rec.employee_id.id, contract=rec.env['hr.contract'].search([ localdict = dict(employee=rec.employee_id, contract=rec)
('employee_id', '=', rec.employee_id.id)]))
if rec.salary_group.gread_max > 0 and rec.salary_group.gread_min > 0: 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: 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')) 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() for item in rec.advantages:
if item.benefits_discounts._compute_rule(localdict)[0] < item.amount and item.type == 'exception': if item.type == 'exception':
rule_val = item.benefits_discounts._compute_rule(localdict)[0]
if rule_val < item.amount:
raise UserError(_( raise UserError(_(
'The amount you put is greater than fact value of this Salary rule %s (%s).') % ( 'The amount you put is greater than fact value of this Salary rule %s (%s).') % (
item.benefits_discounts.name, item.benefits_discounts.code)) item.benefits_discounts.name, item.benefits_discounts.code))
@api.depends('salary_scale.transfer_type') @api.depends('salary_scale.transfer_type')
def compute_move_type(self): def compute_move_type(self):
self.compute_function() # self.compute_function()
if self.salary_scale.transfer_type == 'one_by_one': if self.salary_scale.transfer_type == 'one_by_one':
self.required_condition = True self.required_condition = True
else: else:
self.required_condition = False 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): def compute_function(self):
for item in self: for item in self:
item.house_allowance_temp = 0 item.house_allowance_temp = 0
item.transport_allowance = 0 item.transport_allowance = 0
item.total_net = 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( allowance_customize_items = item.advantages.filtered(
lambda key: key.type == 'customize' and key.out_rule is False and lambda key: key.type == 'customize' and key.out_rule is False and
key.benefits_discounts.category_id.rule_type == 'allowance' 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) (key.date_to if key.date_to else current_date) >= current_date >= key.date_from
>= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) )
allow_sum_custom = sum(x.amount for x in allowance_customize_items) allow_sum_custom = sum(x.amount for x in allowance_customize_items)
for x in allowance_customize_items: for x in allowance_customize_items:
if x.benefits_discounts.rules_type == 'house': if x.benefits_discounts.rules_type == 'house':
item.house_allowance_temp += x.amount item.house_allowance_temp += x.amount
if x.benefits_discounts.rules_type == 'transport': if x.benefits_discounts.rules_type == 'transport':
item.transport_allowance += x.amount item.transport_allowance += x.amount
# allow_custom_ids = [record.benefits_discounts.id for record in allowance_customize_items]
deduction_customize_items = item.advantages.filtered( deduction_customize_items = item.advantages.filtered(
lambda key: key.type == 'customize' and key.out_rule is False and lambda key: key.type == 'customize' and key.out_rule is False and
key.benefits_discounts.category_id.rule_type == 'deduction' 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) (key.date_to if key.date_to else current_date) >= current_date >= key.date_from
>= 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_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') 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 total_rule_result, sum_except, sum_customize_expect = 0.0, 0.0, 0.0
for x in exception_items: for x in exception_items:
rule_result = x.benefits_discounts._compute_rule(localdict)[0] 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 total_rule_result = rule_result
elif str(current_date) > x.date_from: elif current_date > x.date_from:
if x.date_to and str(current_date) <= x.date_to: if x.date_to and current_date <= x.date_to:
total_rule_result = rule_result - x.amount total_rule_result = rule_result - x.amount
elif x.date_to and str(current_date) >= x.date_to: elif x.date_to and current_date >= x.date_to:
total_rule_result = 0 # rule_result total_rule_result = 0
elif not x.date_to: elif not x.date_to:
total_rule_result = rule_result - x.amount total_rule_result = rule_result - x.amount
else: else:
@ -107,85 +119,42 @@ class HrContractSalaryScale(models.Model):
else: else:
sum_except += total_rule_result sum_except += total_rule_result
if exception_items: except_ids = exception_items.mapped('benefits_discounts.id')
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]
rule_ids = item.salary_scale.rule_ids.filtered( rule_ids = item.salary_scale.rule_ids.filtered(
lambda key: key.id not in ded_custom_ids and key.id not in except_ids) 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) if item.salary_level:
# key.id not in allow_custom_ids and key.id not in ded_custom_ids and 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) if item.salary_group:
# key.id not in allow_custom_ids and key.id not in ded_custom_ids and rule_ids += item.salary_group.rule_ids.filtered(
lambda key: key.id not in except_ids)
total_allowance = 0 total_allowance = 0
total_ded = 0 total_ded = 0
for line in rule_ids: for line in rule_ids:
try:
amount = line._compute_rule(localdict)[0]
except Exception:
amount = 0.0
if line.category_id.rule_type == 'allowance': if line.category_id.rule_type == 'allowance':
try: total_allowance += amount
total_allowance += line._compute_rule(localdict)[0] elif line.category_id.rule_type == 'deduction':
except: total_ded += amount
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': if line.rules_type == 'house':
item.house_allowance_temp += line._compute_rule(localdict)[0] item.house_allowance_temp += amount
if line.rules_type == 'transport': if line.rules_type == 'transport':
item.transport_allowance += line._compute_rule(localdict)[0] item.transport_allowance += amount
item.total_allowance = total_allowance item.total_allowance = total_allowance + allow_sum_custom + sum_customize_expect
item.total_deduction = -total_ded item.total_deduction = -(total_ded + ded_sum_custom + sum_except)
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 item.total_net = item.total_allowance + item.total_deduction
# filter salary_level,salary_group,salary_degree
@api.onchange('salary_scale') @api.onchange('salary_scale')
def onchange_salary_scale(self): def onchange_salary_scale(self):
for item in self: for item in self:
@ -207,8 +176,6 @@ class HrContractSalaryScale(models.Model):
'salary_group': [('id', 'in', [])], 'salary_group': [('id', 'in', [])],
'salary_degree': [('id', 'in', [])]}} 'salary_degree': [('id', 'in', [])]}}
# filter depend on salary_level
@api.onchange('salary_level') @api.onchange('salary_level')
def onchange_salary_level(self): def onchange_salary_level(self):
for item in self: for item in self:
@ -221,7 +188,6 @@ class HrContractSalaryScale(models.Model):
return {'domain': {'salary_group': [('id', 'in', [])], return {'domain': {'salary_group': [('id', 'in', [])],
'salary_degree': [('id', 'in', [])]}} 'salary_degree': [('id', 'in', [])]}}
# filter depend on salary_group
@api.onchange('salary_group') @api.onchange('salary_group')
def onchange_salary_group(self): def onchange_salary_group(self):
@ -232,29 +198,228 @@ class HrContractSalaryScale(models.Model):
return {'domain': {'salary_degree': [('id', 'in', degree_ids.ids)]}} return {'domain': {'salary_degree': [('id', 'in', degree_ids.ids)]}}
else: else:
return {'domain': {'salary_degree': [('id', 'in', [])]}} 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') @api.depends('contractor_type.salary_type')
def compute_type(self): def compute_type(self):
if self.contractor_type.salary_type == 'scale': for rec in self:
self.hide = True if rec.contractor_type.salary_type == 'scale':
rec.hide = True
else: else:
self.hide = False 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): class Advantages(models.Model):

View File

@ -79,249 +79,347 @@ class HrSalaryRules(models.Model):
if rec.category_id.rule_type != 'deduction' and rec.rules_type == 'insurnce': if rec.category_id.rule_type != 'deduction' and rec.rules_type == 'insurnce':
raise UserError(_("The Salary Rule is Not Deduction")) raise UserError(_("The Salary Rule is Not Deduction"))
# Override function compute rule in hr salary rule
def _compute_rule(self, localdict): def _compute_rule(self, localdict):
self.ensure_one()
payslip = localdict.get('payslip') payslip = localdict.get('payslip')
contract = localdict.get('contract') 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': if self.amount_select == 'percentage':
total_percent, total = 0, 0 total_percent = 0.0
if self.related_benefits_discounts: related_rules = getattr(self, 'related_benefits_discounts', [])
for line in self.related_benefits_discounts:
if related_rules:
for line in related_rules:
calc_line = line._compute_rule(localdict)[0] 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 line.amount_select == 'fix':
if contract.advantages:
for con in contract.advantages:
if line.id == con.benefits_discounts.id:
if payslip: if payslip:
if con.date_from > payslip.date_from: 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 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
elif line.amount_select == 'percentage': if is_valid_date:
if contract.advantages: total_to_add = 0.0
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.type == 'exception':
if con.amount > calc_line or con.amount == calc_line: if con.amount < calc_line:
pass total_to_add = calc_line - con.amount
elif con.amount < calc_line: else:
total = calc_line - con.amount total_to_add = 0.0
elif con.type == 'customize': elif con.type == 'customize':
total = con.amount total_to_add = con.amount
if line.amount_select == 'percentage':
total_percent -= calc_line total_percent -= calc_line
total_percent += total total_percent += total_to_add
else: else:
if str(con.date_from) < str(datetime.now().date()): total_percent += total_to_add
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: else:
total_percent += calc_line total_percent += calc_line
else: if not line_in_advantages:
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 total_percent += calc_line
if total_percent: if total_percent:
if self.salary_type == 'fixed':
try: try:
return float(total_percent * self.amount_percentage / 100), \ qty = float(safe_eval(self.quantity, localdict))
float(safe_eval(self.quantity, localdict)), self.amount_percentage rate = self.amount_percentage
except: return float(total_percent * self.amount_percentage / 100), qty, rate
raise UserError( except Exception as e:
_('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( raise UserError(_('Error calculating percentage rule %s: %s') % (self.name, e))
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: else:
return 0, 0, 0 return 0.0, 0.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': elif self.amount_select == 'fix':
if self.salary_type == 'fixed':
try: try:
return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 qty = float(safe_eval(self.quantity, localdict))
except: amount = get_related_amount()
raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) return amount, qty, 100.0
elif self.salary_type == 'related_levels': except Exception as e:
levels_ids = self.salary_amount_ids.filtered( raise UserError(_('Error computing fix rule %s: %s') % (self.name, e))
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: else:
try: try:
safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True)
return float(localdict['result']), 'result_qty' in localdict and localdict[ return float(localdict.get('result', 0.0)), \
'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 localdict.get('result_qty', 1.0), \
except: localdict.get('result_rate', 100.0)
raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) 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): class SalaryConfig(models.Model):

View File

@ -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

View File

@ -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())

View File

@ -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()

View File

@ -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")

View File

@ -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.")

View File

@ -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(),
})

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from odoo import models, fields, api, _ from odoo import models, fields, api, _
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError,UserError
class HRHolidays(models.Model): class HRHolidays(models.Model):

View File

@ -6,7 +6,7 @@ import calendar
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from odoo.tools.translate import _ from odoo.tools.translate import _
from odoo import models, fields, api from odoo import models, fields, api
from odoo.exceptions import UserError from odoo.exceptions import UserError ,ValidationError
class ReturnFromLeave(models.Model): class ReturnFromLeave(models.Model):
@ -73,7 +73,7 @@ class ReturnFromLeave(models.Model):
def _chick_leave_type(self): def _chick_leave_type(self):
for rec in self: for rec in self:
if rec.leave_request_id.holiday_status_id.leave_type == 'annual' and rec.decision == 'other': 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') @api.depends('leave_request_id')
def _compute_dates_of_leave(self): def _compute_dates_of_leave(self):
@ -132,7 +132,7 @@ class ReturnFromLeave(models.Model):
else: 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]))) 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: 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 " "If you plan for an early return kindly apply for leave "
"cancellation.") % request.leave_request_id.date_to) "cancellation.") % request.leave_request_id.date_to)
else: else:
@ -155,7 +155,7 @@ class ReturnFromLeave(models.Model):
self.settling_leave_id.draft_state() self.settling_leave_id.draft_state()
self.settling_leave_id.unlink() self.settling_leave_id.unlink()
else: 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.settling_leave_id.holiday_status_id.name)
self.state = 'draft' self.state = 'draft'
self.leave_request_id.return_from_leave = False self.leave_request_id.return_from_leave = False
@ -166,14 +166,14 @@ class ReturnFromLeave(models.Model):
request_id = rec.leave_request_id request_id = rec.leave_request_id
if rec.decision == 'law': # create unpaid leave if rec.decision == 'law': # create unpaid leave
if not request_id.holiday_status_id.unpaid_holiday_id: 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) % request_id.holiday_status_id.name)
status_id = request_id.holiday_status_id.unpaid_holiday_id.id status_id = request_id.holiday_status_id.unpaid_holiday_id.id
elif rec.decision == 'deduct': # Deduct from leave balance elif rec.decision == 'deduct': # Deduct from leave balance
status_id = request_id.holiday_status_id.id status_id = request_id.holiday_status_id.id
elif rec.decision == 'other': # create annual leave elif rec.decision == 'other': # create annual leave
if not request_id.holiday_status_id.annual_holiday_id: 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) % request_id.holiday_status_id.name)
status_id = request_id.holiday_status_id.annual_holiday_id.id status_id = request_id.holiday_status_id.annual_holiday_id.id
@ -183,7 +183,7 @@ class ReturnFromLeave(models.Model):
('check_allocation_view', '=', 'balance') ('check_allocation_view', '=', 'balance')
], order='id desc', limit=1).remaining_leaves or 0.0 ], order='id desc', limit=1).remaining_leaves or 0.0
if balance < rec.diff_days: 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.") _("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))) % (request_id.holiday_status_id.name, round(balance, 2)))
@ -225,7 +225,7 @@ class ReturnFromLeave(models.Model):
if self.decision == 'deduct': if self.decision == 'deduct':
self.settling_leave_id.financial_manager() self.settling_leave_id.financial_manager()
elif self.leave_request_id.state != 'validate1': elif self.leave_request_id.state != 'validate1':
raise exceptions.ValidationError( raise ValidationError(
_("Sorry %s leave is not approved yet. kindly approve it first") % ( _("Sorry %s leave is not approved yet. kindly approve it first") % (
self.leave_request_id.display_name)) self.leave_request_id.display_name))
self.leave_request_id.remove_delegated_access() 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.draft_state()
leave.settling_leave_id.unlink() leave.settling_leave_id.unlink()
else: 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) % leave.settling_leave_id.holiday_status_id.name)
self.state = 'refuse' self.state = 'refuse'

View File

@ -0,0 +1 @@
from . import test_hr_holidays_custom

View File

@ -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)