commit
046823a61a
|
|
@ -244,6 +244,7 @@ class employee_overtime_request(models.Model):
|
|||
'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)],
|
||||
'res_model': 'employee.overtime.request',
|
||||
'res_id': self.id
|
||||
|
||||
})
|
||||
record.account_id = account_debit_id.id
|
||||
record.journal_id = journal_id.id
|
||||
|
|
|
|||
|
|
@ -124,7 +124,7 @@ class HrPersonalPermission(models.Model):
|
|||
cal_hour_start = calendar.full_min_sign_in
|
||||
cal_hour_end = calendar.full_max_sign_out
|
||||
if hour_start > cal_hour_end or hour_end < cal_hour_start:
|
||||
raise exceptions.ValidationError(_('Sorry, Permission Must Be within The Attendance Hours'))
|
||||
raise ValidationError(_('Sorry, Permission Must Be within The Attendance Hours'))
|
||||
|
||||
|
||||
|
||||
|
|
@ -345,7 +345,7 @@ class HrPersonalPermission(models.Model):
|
|||
if item.duration <= 0.0:
|
||||
raise UserError(_('This Duration Must Be Greater Than Zero'))
|
||||
|
||||
if item.duration < item.balance:
|
||||
if item.duration > item.balance:
|
||||
raise UserError(_('This Duration must be less than or equal to the Permission Limit'))
|
||||
|
||||
if item.duration > item.permission_number:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
|
||||
|
|
@ -0,0 +1 @@
|
|||
from . import test_custody_receiving
|
||||
|
|
@ -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()
|
||||
|
|
@ -182,14 +182,14 @@ class HrPayslip(models.Model):
|
|||
current_leave_struct['number_of_days'] += hours / work_hours
|
||||
|
||||
# compute worked days
|
||||
work_data = contract.employee_id._get_work_days_data(day_from, day_to,
|
||||
work_data = contract.employee_id._get_work_days_data_batch(day_from, day_to,
|
||||
calendar=contract.resource_calendar_id)
|
||||
attendances = {
|
||||
'name': _("Normal Working Days paid at 100%"),
|
||||
'sequence': 1,
|
||||
'code': 'WORK100',
|
||||
'number_of_days': work_data['days'],
|
||||
'number_of_hours': work_data['hours'],
|
||||
'number_of_days': work_data.get('days', 0.0),
|
||||
'number_of_hours': work_data.get('hours', 0.0),
|
||||
'contract_id': contract.id,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,10 @@ class HrPayrollStructure(models.Model):
|
|||
rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id',
|
||||
string='Salary Rules')
|
||||
|
||||
@api.constrains('parent_id')
|
||||
def _check_parent_id(self):
|
||||
if not self._check_recursion():
|
||||
raise ValidationError(_('You cannot create a recursive salary structure.'))
|
||||
# @api.constrains('parent_id')
|
||||
# def _check_parent_id(self):
|
||||
# if not self._has_cycle():
|
||||
# raise ValidationError(_('You cannot create a recursive salary structure.'))
|
||||
|
||||
def copy(self, default=None):
|
||||
self.ensure_one()
|
||||
|
|
@ -85,7 +85,7 @@ class HrSalaryRuleCategory(models.Model):
|
|||
|
||||
@api.constrains('parent_id')
|
||||
def _check_parent_id(self):
|
||||
if not self._check_recursion():
|
||||
if not self._has_cycle():
|
||||
raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rule Category.'))
|
||||
|
||||
|
||||
|
|
@ -176,7 +176,7 @@ class HrSalaryRule(models.Model):
|
|||
|
||||
@api.constrains('parent_rule_id')
|
||||
def _check_parent_rule_id(self):
|
||||
if not self._check_recursion(parent='parent_rule_id'):
|
||||
if not self._has_cycle('parent_rule_id'):
|
||||
raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rules.'))
|
||||
|
||||
def _recursive_search_of_rules(self):
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import test_payroll_rules
|
||||
|
|
@ -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)
|
||||
|
|
@ -1175,7 +1175,7 @@ class HrOfficialMissionEmployee(models.Model):
|
|||
def check_dates(self):
|
||||
for rec in self:
|
||||
if rec.hour_from >= 24 or rec.hour_to >= 24:
|
||||
raise exceptions.ValidationError(_('Wrong Time Format.!'))
|
||||
raise ValidationError(_('Wrong Time Format.!'))
|
||||
date_from = datetime.strptime(str(rec.date_from), DEFAULT_SERVER_DATE_FORMAT).date()
|
||||
date_to = datetime.strptime(str(rec.date_to), DEFAULT_SERVER_DATE_FORMAT).date()
|
||||
delta = timedelta(days=1)
|
||||
|
|
@ -1190,7 +1190,7 @@ class HrOfficialMissionEmployee(models.Model):
|
|||
'&', ('hour_from', '>=', rec.hour_from), ('hour_to', '<=', rec.hour_to),
|
||||
])
|
||||
if missions_ids:
|
||||
raise exceptions.ValidationError(_('Sorry The Employee %s Actually On %s For this Time') %
|
||||
raise ValidationError(_('Sorry The Employee %s Actually On %s For this Time') %
|
||||
(rec.employee_id.name,
|
||||
missions_ids.official_mission_id.mission_type.name))
|
||||
date_from += delta
|
||||
|
|
@ -1203,7 +1203,7 @@ class HrOfficialMissionEmployee(models.Model):
|
|||
('official_mission_id.process_type', '=', 'training'),
|
||||
('official_mission_id.course_name.id', '=', item.official_mission_id.course_name.id)])
|
||||
if duplicated:
|
||||
raise exceptions.ValidationError(
|
||||
raise ValidationError(
|
||||
_("Employee %s has already take this course.") % (item.employee_id.name))
|
||||
if item.official_mission_id and item.official_mission_id.mission_type.duration_type == 'days' \
|
||||
and item.date_from and item.date_to:
|
||||
|
|
@ -1230,7 +1230,7 @@ class HrOfficialMissionEmployee(models.Model):
|
|||
if year_last_record == year_now_record:
|
||||
number_days = number_days + rec.days
|
||||
if number_days > days_per_year:
|
||||
raise exceptions.ValidationError(
|
||||
raise ValidationError(
|
||||
_("Sorry The Employee %s, The Number of Requests Cannot Exceed %s Maximum Days Per year.") % (
|
||||
rec.employee_id.name, days_per_year))
|
||||
####
|
||||
|
|
@ -1427,7 +1427,7 @@ class HrOfficialMissionEmployee(models.Model):
|
|||
def compute_number_of_hours(self):
|
||||
for item in self:
|
||||
if item.hour_from >= 24 or item.hour_to >= 24:
|
||||
raise exceptions.ValidationError(_('Wrong Time Format.!'))
|
||||
raise ValidationError(_('Wrong Time Format.!'))
|
||||
if item.official_mission_id.hour_to and item.official_mission_id.hour_from:
|
||||
if item.hour_from and item.hour_to:
|
||||
if (item.hour_to - item.hour_from) < 0:
|
||||
|
|
@ -1788,7 +1788,7 @@ class MissionTable(models.Model):
|
|||
date_to = rec.destination_id.date_to
|
||||
if date_from and date_to:
|
||||
if not (date_from <= rec.date <= date_to):
|
||||
raise exceptions.ValidationError(
|
||||
raise ValidationError(
|
||||
_("The mission date %(date)s must be between destination's date from %(date_from)s and date to %(date_to)s.",
|
||||
date=rec.date, date_from=date_from, date_to=date_to)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import test_official_mission
|
||||
|
|
@ -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()
|
||||
|
|
@ -66,4 +66,6 @@
|
|||
'installable': True,
|
||||
'auto_install': False,
|
||||
'application': True,
|
||||
'test_tags': ['standard', 'at_install'],
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -104,6 +104,7 @@ class SalaryRuleInput(models.Model):
|
|||
|
||||
def withdraw(self):
|
||||
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)])
|
||||
if self.number == payslip.number:
|
||||
if self.loan_ids:
|
||||
|
|
@ -852,7 +853,7 @@ class SalaryRuleInput(models.Model):
|
|||
d.amount = d.amount
|
||||
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),
|
||||
('request_type.refund_from', '=', 'salary'),
|
||||
('state', '=', 'pay')]).filtered(
|
||||
|
|
@ -2963,6 +2964,7 @@ class HrPayslipRun(models.Model):
|
|||
def withdraw(self):
|
||||
for line in self.slip_ids:
|
||||
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)])
|
||||
if line.number == payslip.number:
|
||||
if line.loan_ids:
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@ from datetime import datetime
|
|||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
# -*- coding: utf-8 -*-
|
||||
from datetime import datetime
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class HrContractSalaryScale(models.Model):
|
||||
_inherit = 'hr.contract'
|
||||
|
|
@ -25,74 +30,81 @@ class HrContractSalaryScale(models.Model):
|
|||
@api.constrains('advantages', 'salary', 'salary_group')
|
||||
def amount_constrains(self):
|
||||
for rec in self:
|
||||
localdict = dict(employee=rec.employee_id.id, contract=rec.env['hr.contract'].search([
|
||||
('employee_id', '=', rec.employee_id.id)]))
|
||||
localdict = dict(employee=rec.employee_id, contract=rec)
|
||||
|
||||
if rec.salary_group.gread_max > 0 and rec.salary_group.gread_min > 0:
|
||||
if rec.salary > rec.salary_group.gread_max or rec.salary < rec.salary_group.gread_min:
|
||||
raise UserError(_('The Basic Salary Is Greater Than Group Gread Max Or less than Gread Min'))
|
||||
for item in self.advantages:
|
||||
item.to_get_contract_id()
|
||||
if item.benefits_discounts._compute_rule(localdict)[0] < item.amount and item.type == 'exception':
|
||||
|
||||
for item in rec.advantages:
|
||||
if item.type == 'exception':
|
||||
rule_val = item.benefits_discounts._compute_rule(localdict)[0]
|
||||
if rule_val < item.amount:
|
||||
raise UserError(_(
|
||||
'The amount you put is greater than fact value of this Salary rule %s (%s).') % (
|
||||
item.benefits_discounts.name, item.benefits_discounts.code))
|
||||
|
||||
@api.depends('salary_scale.transfer_type')
|
||||
def compute_move_type(self):
|
||||
self.compute_function()
|
||||
# self.compute_function()
|
||||
if self.salary_scale.transfer_type == 'one_by_one':
|
||||
self.required_condition = True
|
||||
else:
|
||||
self.required_condition = False
|
||||
|
||||
@api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree','salary','advantages','house_allowance_temp','transport_allowance','total_deduction','salary_insurnce','total_allowance','state')
|
||||
@api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree', 'salary', 'advantages',
|
||||
'house_allowance_temp', 'transport_allowance', 'total_deduction', 'total_allowance', 'state')
|
||||
def compute_function(self):
|
||||
for item in self:
|
||||
item.house_allowance_temp = 0
|
||||
item.transport_allowance = 0
|
||||
item.total_net = 0
|
||||
contract = self.env['hr.contract'].search([('employee_id', '=', item.employee_id.id)])
|
||||
localdict = dict(employee=item.employee_id.id, contract=contract)
|
||||
current_date = datetime.now().date()
|
||||
|
||||
# customize type in advantages
|
||||
localdict = dict(employee=item.employee_id, contract=item)
|
||||
current_date = fields.Date.today()
|
||||
|
||||
allowance_customize_items = item.advantages.filtered(
|
||||
lambda key: key.type == 'customize' and key.out_rule is False and
|
||||
key.benefits_discounts.category_id.rule_type == 'allowance' and
|
||||
(datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date)
|
||||
>= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date())
|
||||
(key.date_to if key.date_to else current_date) >= current_date >= key.date_from
|
||||
)
|
||||
|
||||
allow_sum_custom = sum(x.amount for x in allowance_customize_items)
|
||||
for x in allowance_customize_items:
|
||||
if x.benefits_discounts.rules_type == 'house':
|
||||
item.house_allowance_temp += x.amount
|
||||
|
||||
if x.benefits_discounts.rules_type == 'transport':
|
||||
item.transport_allowance += x.amount
|
||||
# allow_custom_ids = [record.benefits_discounts.id for record in allowance_customize_items]
|
||||
|
||||
deduction_customize_items = item.advantages.filtered(
|
||||
lambda key: key.type == 'customize' and key.out_rule is False and
|
||||
key.benefits_discounts.category_id.rule_type == 'deduction' and
|
||||
(datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date)
|
||||
>= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date())
|
||||
(key.date_to if key.date_to else current_date) >= current_date >= key.date_from
|
||||
)
|
||||
|
||||
ded_sum_custom = sum(x.amount for x in deduction_customize_items)
|
||||
ded_custom_ids = [record.benefits_discounts.id for record in deduction_customize_items]
|
||||
ded_custom_ids = deduction_customize_items.mapped('benefits_discounts.id')
|
||||
|
||||
# exception type in advantages
|
||||
exception_items = item.advantages.filtered(lambda key: key.type == 'exception')
|
||||
|
||||
if exception_items:
|
||||
exception_items = exception_items.filtered(
|
||||
lambda key: (key.date_to.month if key.date_to else current_date.month)
|
||||
>= current_date.month >= key.date_from.month
|
||||
)
|
||||
|
||||
total_rule_result, sum_except, sum_customize_expect = 0.0, 0.0, 0.0
|
||||
|
||||
for x in exception_items:
|
||||
rule_result = x.benefits_discounts._compute_rule(localdict)[0]
|
||||
if x.date_from >= str(current_date):
|
||||
|
||||
if x.date_from >= current_date:
|
||||
total_rule_result = rule_result
|
||||
elif str(current_date) > x.date_from:
|
||||
if x.date_to and str(current_date) <= x.date_to:
|
||||
elif current_date > x.date_from:
|
||||
if x.date_to and current_date <= x.date_to:
|
||||
total_rule_result = rule_result - x.amount
|
||||
elif x.date_to and str(current_date) >= x.date_to:
|
||||
total_rule_result = 0 # rule_result
|
||||
elif x.date_to and current_date >= x.date_to:
|
||||
total_rule_result = 0
|
||||
elif not x.date_to:
|
||||
total_rule_result = rule_result - x.amount
|
||||
else:
|
||||
|
|
@ -107,85 +119,42 @@ class HrContractSalaryScale(models.Model):
|
|||
else:
|
||||
sum_except += total_rule_result
|
||||
|
||||
if exception_items:
|
||||
exception_items = item.advantages.filtered(
|
||||
lambda key: (datetime.strptime(str(key.date_to),
|
||||
"%Y-%m-%d").date().month if key.date_to else current_date.month)
|
||||
>= current_date.month >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date().month)
|
||||
|
||||
except_ids = [record.benefits_discounts.id for record in exception_items]
|
||||
except_ids = exception_items.mapped('benefits_discounts.id')
|
||||
|
||||
rule_ids = item.salary_scale.rule_ids.filtered(
|
||||
lambda key: key.id not in ded_custom_ids and key.id not in except_ids)
|
||||
|
||||
level_rule_ids = item.salary_level.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids)
|
||||
# key.id not in allow_custom_ids and key.id not in ded_custom_ids and
|
||||
if item.salary_level:
|
||||
rule_ids += item.salary_level.rule_ids.filtered(
|
||||
lambda key: key.id not in except_ids)
|
||||
|
||||
group_rule_ids = item.salary_group.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids)
|
||||
# key.id not in allow_custom_ids and key.id not in ded_custom_ids and
|
||||
if item.salary_group:
|
||||
rule_ids += item.salary_group.rule_ids.filtered(
|
||||
lambda key: key.id not in except_ids)
|
||||
|
||||
total_allowance = 0
|
||||
total_ded = 0
|
||||
|
||||
for line in rule_ids:
|
||||
try:
|
||||
amount = line._compute_rule(localdict)[0]
|
||||
except Exception:
|
||||
amount = 0.0
|
||||
|
||||
if line.category_id.rule_type == 'allowance':
|
||||
try:
|
||||
total_allowance += line._compute_rule(localdict)[0]
|
||||
except:
|
||||
total_allowance += 0
|
||||
|
||||
if line.category_id.rule_type == 'deduction':
|
||||
try:
|
||||
total_ded += line._compute_rule(localdict)[0]
|
||||
except:
|
||||
total_ded += 0
|
||||
|
||||
total_allowance += amount
|
||||
elif line.category_id.rule_type == 'deduction':
|
||||
total_ded += amount
|
||||
|
||||
if line.rules_type == 'house':
|
||||
item.house_allowance_temp += line._compute_rule(localdict)[0]
|
||||
item.house_allowance_temp += amount
|
||||
if line.rules_type == 'transport':
|
||||
item.transport_allowance += line._compute_rule(localdict)[0]
|
||||
item.transport_allowance += amount
|
||||
|
||||
item.total_allowance = total_allowance
|
||||
item.total_deduction = -total_ded
|
||||
|
||||
if item.salary_level:
|
||||
total_allowance = 0
|
||||
total_deduction = 0
|
||||
for line in level_rule_ids:
|
||||
if line.category_id.rule_type == 'allowance':
|
||||
try:
|
||||
total_allowance += line._compute_rule(localdict)[0]
|
||||
except:
|
||||
total_allowance += 0
|
||||
elif line.category_id.rule_type == 'deduction':
|
||||
try:
|
||||
total_deduction += line._compute_rule(localdict)[0]
|
||||
except:
|
||||
total_deduction += 0
|
||||
|
||||
item.total_allowance += total_allowance
|
||||
item.total_deduction += -total_deduction
|
||||
|
||||
if item.salary_group:
|
||||
total_allowance = 0
|
||||
total_deduction = 0
|
||||
for line in group_rule_ids:
|
||||
if line.category_id.rule_type == 'allowance':
|
||||
total_allowance += line._compute_rule(localdict)[0]
|
||||
elif line.category_id.rule_type == 'deduction':
|
||||
total_deduction += line._compute_rule(localdict)[0]
|
||||
|
||||
item.total_allowance += total_allowance
|
||||
item.total_deduction += -total_deduction
|
||||
|
||||
item.total_allowance += allow_sum_custom
|
||||
item.total_allowance += sum_customize_expect
|
||||
item.total_deduction += -ded_sum_custom
|
||||
item.total_deduction += -sum_except
|
||||
item.total_allowance = total_allowance + allow_sum_custom + sum_customize_expect
|
||||
item.total_deduction = -(total_ded + ded_sum_custom + sum_except)
|
||||
item.total_net = item.total_allowance + item.total_deduction
|
||||
|
||||
# filter salary_level,salary_group,salary_degree
|
||||
|
||||
@api.onchange('salary_scale')
|
||||
def onchange_salary_scale(self):
|
||||
for item in self:
|
||||
|
|
@ -207,8 +176,6 @@ class HrContractSalaryScale(models.Model):
|
|||
'salary_group': [('id', 'in', [])],
|
||||
'salary_degree': [('id', 'in', [])]}}
|
||||
|
||||
# filter depend on salary_level
|
||||
|
||||
@api.onchange('salary_level')
|
||||
def onchange_salary_level(self):
|
||||
for item in self:
|
||||
|
|
@ -221,7 +188,6 @@ class HrContractSalaryScale(models.Model):
|
|||
return {'domain': {'salary_group': [('id', 'in', [])],
|
||||
'salary_degree': [('id', 'in', [])]}}
|
||||
|
||||
# filter depend on salary_group
|
||||
|
||||
@api.onchange('salary_group')
|
||||
def onchange_salary_group(self):
|
||||
|
|
@ -232,29 +198,228 @@ class HrContractSalaryScale(models.Model):
|
|||
return {'domain': {'salary_degree': [('id', 'in', degree_ids.ids)]}}
|
||||
else:
|
||||
return {'domain': {'salary_degree': [('id', 'in', [])]}}
|
||||
|
||||
@api.depends('salary_degree')
|
||||
def _get_amount(self):
|
||||
for record in self:
|
||||
record.transport_allowance_temp = record.transport_allowance * record.wage / 100 \
|
||||
if record.transport_allowance_type == 'perc' else record.transport_allowance
|
||||
record.house_allowance_temp = record.house_allowance * record.wage / 100 \
|
||||
if record.house_allowance_type == 'perc' else record.house_allowance
|
||||
record.communication_allowance_temp = record.communication_allowance * record.wage / 100 \
|
||||
if record.communication_allowance_type == 'perc' else record.communication_allowance
|
||||
record.field_allowance_temp = record.field_allowance * record.wage / 100 \
|
||||
if record.field_allowance_type == 'perc' else record.field_allowance
|
||||
record.special_allowance_temp = record.special_allowance * record.wage / 100 \
|
||||
if record.special_allowance_type == 'perc' else record.special_allowance
|
||||
record.other_allowance_temp = record.other_allowance * record.wage / 100 \
|
||||
if record.other_allowance_type == 'perc' else record.other_allowance
|
||||
|
||||
@api.depends('contractor_type.salary_type')
|
||||
def compute_type(self):
|
||||
if self.contractor_type.salary_type == 'scale':
|
||||
self.hide = True
|
||||
for rec in self:
|
||||
if rec.contractor_type.salary_type == 'scale':
|
||||
rec.hide = True
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -79,8 +79,6 @@ class HrSalaryRules(models.Model):
|
|||
if rec.category_id.rule_type != 'deduction' and rec.rules_type == 'insurnce':
|
||||
raise UserError(_("The Salary Rule is Not Deduction"))
|
||||
|
||||
# Override function compute rule in hr salary rule
|
||||
|
||||
def _compute_rule(self, localdict):
|
||||
payslip = localdict.get('payslip')
|
||||
contract = localdict.get('contract')
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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())
|
||||
|
|
@ -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()
|
||||
|
|
@ -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")
|
||||
|
|
@ -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',
|
||||
'fixed_amount': 1000,
|
||||
'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',
|
||||
'fixed_amount': 500,
|
||||
'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,
|
||||
'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,
|
||||
'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.")
|
||||
|
|
@ -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(),
|
||||
})
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
from odoo import models, fields, api, _
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.exceptions import ValidationError,UserError
|
||||
|
||||
|
||||
class HRHolidays(models.Model):
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import calendar
|
|||
from dateutil.relativedelta import relativedelta
|
||||
from odoo.tools.translate import _
|
||||
from odoo import models, fields, api
|
||||
from odoo.exceptions import UserError
|
||||
from odoo.exceptions import UserError ,ValidationError
|
||||
|
||||
|
||||
class ReturnFromLeave(models.Model):
|
||||
|
|
@ -73,7 +73,7 @@ class ReturnFromLeave(models.Model):
|
|||
def _chick_leave_type(self):
|
||||
for rec in self:
|
||||
if rec.leave_request_id.holiday_status_id.leave_type == 'annual' and rec.decision == 'other':
|
||||
raise exceptions.ValidationError(_("Sorry Cannot be Create an Annual Leave from the same annual Leave"))
|
||||
raise ValidationError(_("Sorry Cannot be Create an Annual Leave from the same annual Leave"))
|
||||
|
||||
@api.depends('leave_request_id')
|
||||
def _compute_dates_of_leave(self):
|
||||
|
|
@ -132,7 +132,7 @@ class ReturnFromLeave(models.Model):
|
|||
else:
|
||||
request.diff_days = len(list(set([xd for xd in exceeded_dates if xd not in event_dates and xd not in wkns_dates])))
|
||||
else:
|
||||
raise exceptions.ValidationError(_("Sorry this leave ends by %s.\n"
|
||||
raise ValidationError(_("Sorry this leave ends by %s.\n"
|
||||
"If you plan for an early return kindly apply for leave "
|
||||
"cancellation.") % request.leave_request_id.date_to)
|
||||
else:
|
||||
|
|
@ -155,7 +155,7 @@ class ReturnFromLeave(models.Model):
|
|||
self.settling_leave_id.draft_state()
|
||||
self.settling_leave_id.unlink()
|
||||
else:
|
||||
raise exceptions.ValidationError(_("Sorry The link leave cannot be deleted %s After approved")
|
||||
raise ValidationError(_("Sorry The link leave cannot be deleted %s After approved")
|
||||
% self.settling_leave_id.holiday_status_id.name)
|
||||
self.state = 'draft'
|
||||
self.leave_request_id.return_from_leave = False
|
||||
|
|
@ -166,14 +166,14 @@ class ReturnFromLeave(models.Model):
|
|||
request_id = rec.leave_request_id
|
||||
if rec.decision == 'law': # create unpaid leave
|
||||
if not request_id.holiday_status_id.unpaid_holiday_id:
|
||||
raise exceptions.ValidationError(_("Sorry no unpaid leave is defined for %s leave kindly set one")
|
||||
raise ValidationError(_("Sorry no unpaid leave is defined for %s leave kindly set one")
|
||||
% request_id.holiday_status_id.name)
|
||||
status_id = request_id.holiday_status_id.unpaid_holiday_id.id
|
||||
elif rec.decision == 'deduct': # Deduct from leave balance
|
||||
status_id = request_id.holiday_status_id.id
|
||||
elif rec.decision == 'other': # create annual leave
|
||||
if not request_id.holiday_status_id.annual_holiday_id:
|
||||
raise exceptions.ValidationError(_("Sorry no annual leave is defined for %s leave kindly set one")
|
||||
raise ValidationError(_("Sorry no annual leave is defined for %s leave kindly set one")
|
||||
% request_id.holiday_status_id.name)
|
||||
status_id = request_id.holiday_status_id.annual_holiday_id.id
|
||||
|
||||
|
|
@ -183,7 +183,7 @@ class ReturnFromLeave(models.Model):
|
|||
('check_allocation_view', '=', 'balance')
|
||||
], order='id desc', limit=1).remaining_leaves or 0.0
|
||||
if balance < rec.diff_days:
|
||||
raise exceptions.ValidationError(
|
||||
raise ValidationError(
|
||||
_("Sorry your %s leave balance it is not enough to deduct from it, The balance is %s.")
|
||||
% (request_id.holiday_status_id.name, round(balance, 2)))
|
||||
|
||||
|
|
@ -225,7 +225,7 @@ class ReturnFromLeave(models.Model):
|
|||
if self.decision == 'deduct':
|
||||
self.settling_leave_id.financial_manager()
|
||||
elif self.leave_request_id.state != 'validate1':
|
||||
raise exceptions.ValidationError(
|
||||
raise ValidationError(
|
||||
_("Sorry %s leave is not approved yet. kindly approve it first") % (
|
||||
self.leave_request_id.display_name))
|
||||
self.leave_request_id.remove_delegated_access()
|
||||
|
|
@ -240,7 +240,7 @@ class ReturnFromLeave(models.Model):
|
|||
leave.settling_leave_id.draft_state()
|
||||
leave.settling_leave_id.unlink()
|
||||
else:
|
||||
raise exceptions.ValidationError(_("Sorry The link leave cannot be deleted %s After approved")
|
||||
raise ValidationError(_("Sorry The link leave cannot be deleted %s After approved")
|
||||
% leave.settling_leave_id.holiday_status_id.name)
|
||||
self.state = 'refuse'
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
from . import test_hr_holidays_custom
|
||||
|
|
@ -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)
|
||||
Loading…
Reference in New Issue