From 255bf2ea5b70384dd72d89221a18c22445940574 Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Wed, 24 Dec 2025 00:57:50 +0300 Subject: [PATCH 1/3] make unit test hr base --- .../models/employee_overtime_request.py | 5 +- .../models/hr_personal_permission.py | 4 +- employee_requests/tests/__init__.py | 4 + .../tests/test_employee_department_jobs.py | 88 +++ .../tests/test_hr_clearance_form.py | 63 ++ .../tests/test_hr_personal_permission.py | 143 +++++ .../tests/test_overtime_process.py | 188 ++++++ exp_employee_custody/tests/__init__.py | 1 + .../tests/test_custody_receiving.py | 159 +++++ exp_hr_payroll/models/hr_payslip.py | 6 +- exp_hr_payroll/models/hr_salary_rule.py | 12 +- exp_hr_payroll/tests/__init__.py | 1 + exp_hr_payroll/tests/test_payroll_rules.py | 224 +++++++ .../models/hr_official_mission.py | 12 +- exp_official_mission/tests/__init__.py | 1 + .../tests/test_official_mission.py | 155 +++++ exp_payroll_custom/__manifest__.py | 2 + .../models/hr_advance_payslip.py | 142 ++--- exp_payroll_custom/models/hr_contract.py | 409 +++++++++---- exp_payroll_custom/models/hr_salary_rules.py | 546 +++++++++++------- exp_payroll_custom/tests/__init__.py | 5 + .../tests/test_employee_promotions.py | 148 +++++ .../tests/test_employee_reward.py | 134 +++++ exp_payroll_custom/tests/test_payroll_flow.py | 182 ++++++ .../tests/test_salary_rule_computation.py | 162 ++++++ exp_payroll_custom/tests/test_salary_scale.py | 96 +++ .../hr_government_exit_return_custom.py | 2 +- .../models/return_from_leave.py | 18 +- hr_holidays_public/tests/__init__.py | 1 + .../tests/test_hr_holidays_custom.py | 179 ++++++ 30 files changed, 2646 insertions(+), 446 deletions(-) create mode 100644 employee_requests/tests/__init__.py create mode 100644 employee_requests/tests/test_employee_department_jobs.py create mode 100644 employee_requests/tests/test_hr_clearance_form.py create mode 100644 employee_requests/tests/test_hr_personal_permission.py create mode 100644 employee_requests/tests/test_overtime_process.py create mode 100644 exp_employee_custody/tests/__init__.py create mode 100644 exp_employee_custody/tests/test_custody_receiving.py create mode 100644 exp_hr_payroll/tests/__init__.py create mode 100644 exp_hr_payroll/tests/test_payroll_rules.py create mode 100644 exp_official_mission/tests/__init__.py create mode 100644 exp_official_mission/tests/test_official_mission.py create mode 100644 exp_payroll_custom/tests/__init__.py create mode 100644 exp_payroll_custom/tests/test_employee_promotions.py create mode 100644 exp_payroll_custom/tests/test_employee_reward.py create mode 100644 exp_payroll_custom/tests/test_payroll_flow.py create mode 100644 exp_payroll_custom/tests/test_salary_rule_computation.py create mode 100644 exp_payroll_custom/tests/test_salary_scale.py create mode 100644 hr_holidays_public/tests/__init__.py create mode 100644 hr_holidays_public/tests/test_hr_holidays_custom.py diff --git a/employee_requests/models/employee_overtime_request.py b/employee_requests/models/employee_overtime_request.py index 7c60e8a..9204fe6 100644 --- a/employee_requests/models/employee_overtime_request.py +++ b/employee_requests/models/employee_overtime_request.py @@ -227,7 +227,7 @@ class employee_overtime_request(models.Model): 'debit': record.price_hour, 'account_id': account_debit_id.id, 'partner_id': record.employee_id.user_id.partner_id.id, - 'analytic_account_id': analytic_account_id.id, + # 'analytic_account_id': analytic_account_id.id, } credit_line_vals = { 'name': record.employee_id.name, @@ -242,8 +242,7 @@ class employee_overtime_request(models.Model): 'date': item.request_date, 'ref': record.employee_id.name, 'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)], - 'res_model': 'employee.overtime.request', - 'res_id': self.id + }) record.account_id = account_debit_id.id record.journal_id = journal_id.id diff --git a/employee_requests/models/hr_personal_permission.py b/employee_requests/models/hr_personal_permission.py index fbc1d1c..cd64996 100644 --- a/employee_requests/models/hr_personal_permission.py +++ b/employee_requests/models/hr_personal_permission.py @@ -124,7 +124,7 @@ class HrPersonalPermission(models.Model): cal_hour_start = calendar.full_min_sign_in cal_hour_end = calendar.full_max_sign_out if hour_start > cal_hour_end or hour_end < cal_hour_start: - raise exceptions.ValidationError(_('Sorry, Permission Must Be within The Attendance Hours')) + raise ValidationError(_('Sorry, Permission Must Be within The Attendance Hours')) @@ -345,7 +345,7 @@ class HrPersonalPermission(models.Model): if item.duration <= 0.0: raise UserError(_('This Duration Must Be Greater Than Zero')) - if item.duration < item.balance: + if item.duration > item.balance: raise UserError(_('This Duration must be less than or equal to the Permission Limit')) if item.duration > item.permission_number: diff --git a/employee_requests/tests/__init__.py b/employee_requests/tests/__init__.py new file mode 100644 index 0000000..80a14d2 --- /dev/null +++ b/employee_requests/tests/__init__.py @@ -0,0 +1,4 @@ +# from . import test_overtime_process +# from . import test_employee_department_jobs +# from . import test_hr_clearance_form +from . import test_hr_personal_permission \ No newline at end of file diff --git a/employee_requests/tests/test_employee_department_jobs.py b/employee_requests/tests/test_employee_department_jobs.py new file mode 100644 index 0000000..56ac3fc --- /dev/null +++ b/employee_requests/tests/test_employee_department_jobs.py @@ -0,0 +1,88 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo import fields +from dateutil.relativedelta import relativedelta + + +class TestEmployeeDepartmentJobs(TransactionCase): + + def setUp(self): + super(TestEmployeeDepartmentJobs, self).setUp() + + self.manager_employee = self.env['hr.employee'].create({ + 'name': 'Big Manager', + + }) + + self.dep1 = self.env['hr.department'].create({ + 'name': 'IT Department', + 'manager_id': self.manager_employee.id, + }) + + self.dep2 = self.env['hr.department'].create({ + 'name': 'HR Department', + 'manager_id': self.manager_employee.id, + }) + + self.job1 = self.env['hr.job'].create({'name': 'Developer', 'department_id': self.dep1.id}) + self.job2 = self.env['hr.job'].create( + {'name': 'HR Officer', 'department_id': self.dep2.id, 'no_of_recruitment': 1}) + self.job_full = self.env['hr.job'].create( + {'name': 'Manager', 'department_id': self.dep2.id, 'no_of_recruitment': 0}) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee Job', + 'department_id': self.dep1.id, + 'job_id': self.job1.id, + 'parent_id': self.manager_employee.id, # ربطه بالمدير + 'first_hiring_date': fields.Date.today() - relativedelta(years=2), + 'joining_date': fields.Date.today() - relativedelta(years=1), + }) + + def test_prevent_job_change_no_vacancy(self): + + req = self.env['employee.department.jobs'].new({ + 'employee_id': self.employee.id, + 'new_department_id': self.dep2.id, + 'new_job_id': self.job_full.id, # 0 vacancies + }) + + with self.assertRaises(UserError, msg="Should raise error when no recruitment spots available"): + req.not_reused_same_dep_job() + + def test_update_employee_data_on_approval(self): + request = self.env['employee.department.jobs'].create({ + 'employee_id': self.employee.id, + 'promotion_type': 'both', + 'new_department_id': self.dep2.id, + 'new_job_id': self.job2.id, + 'date': fields.Date.today(), + 'state': 'hr_manager', + }) + + request.store_level_group_and_degree_values() + + request.approved() + + self.assertEqual(self.employee.department_id.id, self.dep2.id, "Department not updated") + self.assertEqual(self.employee.job_id.id, self.job2.id, "Job not updated") + self.assertEqual(self.employee.joining_date, fields.Date.today(), "Joining date not updated") + + def test_service_duration_calculation(self): + today = fields.Date.today() + one_year_one_month_ago = today - relativedelta(years=1, months=1) + + self.employee.joining_date = one_year_one_month_ago + + request = self.env['employee.department.jobs'].create({ + 'employee_id': self.employee.id, + 'date': today, + }) + + request.store_level_group_and_degree_values() + request._compute_duration() + + self.assertEqual(request.service_year, 1, "Service year calculation wrong") + self.assertEqual(request.service_month, 1, "Service month calculation wrong") + self.assertEqual(request.service_day, 0, "Service day calculation wrong") \ No newline at end of file diff --git a/employee_requests/tests/test_hr_clearance_form.py b/employee_requests/tests/test_hr_clearance_form.py new file mode 100644 index 0000000..8ded97f --- /dev/null +++ b/employee_requests/tests/test_hr_clearance_form.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from unittest.mock import patch +import base64 + + +class TestHrClearanceForm(TransactionCase): + + def setUp(self): + super(TestHrClearanceForm, self).setUp() + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee Clearance', + 'first_hiring_date': '2020-01-01', + }) + self.clearance = self.env['hr.clearance.form'].create({ + 'employee_id': self.employee.id, + 'clearance_type': 'final', + }) + + def test_bank_attachment_constraint(self): + + + self.clearance.state = 'admin_manager' + + with self.assertRaises(UserError, msg="Should require attachment for Final Clearance"): + self.clearance.wait() + + + file_content = b'This is a test file content' + encoded_content = base64.b64encode(file_content) + + attachment = self.env['ir.attachment'].create({ + 'name': 'Bank Clearance.pdf', + 'datas': encoded_content, + 'res_model': 'hr.clearance.form', + 'res_id': self.clearance.id, + }) + + self.clearance.bank_attachment_id = [(6, 0, [attachment.id])] + + self.clearance.wait() + self.assertEqual(self.clearance.state, 'wait', "State should move to wait after attachment") + + def test_custody_check_blocking(self): + + with patch('odoo.models.Model.search') as mock_search: + def side_effect(domain, **kwargs): + if domain == [('state', '=', 'installed'), ('name', '=', 'exp_employee_custody')]: + return [1] + + if len(domain) > 0 and domain[0][0] == 'employee_id' and domain[0][2] == self.employee.id: + return [1] + + return [] + + mock_search.side_effect = side_effect + + if 'custom.employee.custody' in self.env: + with self.assertRaises(UserError, msg="Should block clearance if custody exists"): + self.clearance.check_custody() + else: + pass \ No newline at end of file diff --git a/employee_requests/tests/test_hr_personal_permission.py b/employee_requests/tests/test_hr_personal_permission.py new file mode 100644 index 0000000..d1c0e89 --- /dev/null +++ b/employee_requests/tests/test_hr_personal_permission.py @@ -0,0 +1,143 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo import fields +from datetime import datetime, timedelta + + +class TestHrPersonalPermission(TransactionCase): + + def setUp(self): + super(TestHrPersonalPermission, self).setUp() + + self.permission_type = self.env['hr.personal.permission.type'].create({ + 'name': 'Medical Permission', + 'daily_hours': 4.0, + 'monthly_hours': 10.0, + 'approval_by': 'direct_manager', + }) + + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Standard 8 Hours', + 'is_full_day': True, + 'hours_per_day': 8.0, + 'full_min_sign_in': 7.0, + 'full_max_sign_in': 9.0, + 'full_min_sign_out': 15.0, + 'full_max_sign_out': 17.0, + }) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Ahmed Test Employee', + 'resource_calendar_id': self.calendar.id, + 'first_hiring_date': fields.Date.today() - timedelta(days=365), + }) + + def test_01_duration_calculation(self): + start_time = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) + end_time = start_time + timedelta(hours=1.5) + + permission = self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': end_time, + 'permission_number': 10.0, + }) + + self.assertEqual(permission.duration, 1.5, "Duration calculation is wrong") + + def test_02_daily_limit_constraint(self): + + self.permission_type.daily_hours = 2.0 + + start_time = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0) + end_time = start_time + timedelta(hours=3) + + with self.assertRaises(UserError, msg="Should prevent permission > daily limit"): + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': end_time, + 'permission_number': 10.0, + }) + + def test_03_monthly_limit_constraint(self): + self.permission_type.monthly_hours = 4.0 + + base_date = datetime.now().replace(day=1, hour=9, minute=0, second=0, microsecond=0) + + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': base_date, + 'date_to': base_date + timedelta(hours=2), + 'state': 'approve', + 'permission_number': 4.0, + }) + + base_date_2 = base_date + timedelta(days=1) + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': base_date_2, + 'date_to': base_date_2 + timedelta(hours=2), + 'state': 'approve', + 'permission_number': 2.0, + }) + + base_date_3 = base_date + timedelta(days=2) + + with self.assertRaises(UserError, msg="Should prevent permission > monthly limit"): + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': base_date_3, + 'date_to': base_date_3 + timedelta(hours=1), + 'permission_number': 0.0, # الرصيد انتهى + }) + + def test_04_check_attendance_hours(self): + + start_time = datetime.now().replace(hour=18, minute=0, second=0, microsecond=0) + end_time = start_time + timedelta(hours=1) + + with self.assertRaises(ValidationError, msg="Should prevent permission outside attendance hours"): + self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': end_time, + 'permission_number': 10.0, + }) + + def test_05_overlap_with_holiday(self): + if 'hr.holidays' not in self.env: + return + + today = fields.Date.today() + holiday_status = self.env['hr.holidays.status'].search([], limit=1) + if not holiday_status: + holiday_status = self.env['hr.holidays.status'].create({'name': 'Test Leave'}) + + self.env['hr.holidays'].create({ + 'employee_id': self.employee.id, + 'holiday_status_id': holiday_status.id, + 'date_from': today, + 'date_to': today, + 'type': 'remove', + 'state': 'validate', + }) + + start_time = datetime.now().replace(hour=10, minute=0, second=0, microsecond=0) + + with self.assertRaises(UserError, msg="Should prevent permission during approved holiday"): + perm = self.env['hr.personal.permission'].create({ + 'employee_id': self.employee.id, + 'permission_type_id': self.permission_type.id, + 'date_from': start_time, + 'date_to': start_time + timedelta(hours=1), + 'permission_number': 10.0, + }) + perm.check_holiday_mission() \ No newline at end of file diff --git a/employee_requests/tests/test_overtime_process.py b/employee_requests/tests/test_overtime_process.py new file mode 100644 index 0000000..c89b8df --- /dev/null +++ b/employee_requests/tests/test_overtime_process.py @@ -0,0 +1,188 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo import fields +from datetime import date, timedelta + + +class TestOvertimeProcess(TransactionCase): + + def setUp(self): + super(TestOvertimeProcess, self).setUp() + + self.account_type_expenses = self.env['account.account'].search([('account_type', '=', 'expense')], limit=1) + if not self.account_type_expenses: + user_type_expense = self.env.ref('account.data_account_type_expenses') + self.account_type_expenses = self.env['account.account'].create({ + 'name': 'Overtime Expense', + 'code': 'X6000', + 'account_type': 'expense', + 'reconcile': True, + }) + + self.overtime_account = self.account_type_expenses + self.journal_account = self.env['account.account'].search([('account_type', '=', 'asset_cash')], limit=1) + + self.journal = self.env['account.journal'].create({ + 'name': 'Overtime Journal', + 'type': 'cash', + 'code': 'OTJ', + 'default_account_id': self.journal_account.id, + }) + + self.working_hours = self.env['resource.calendar'].create({ + 'name': 'Standard 40 Hours', + 'hours_per_day': 8.0, + # 'max_overtime_hour': 10.0, + # 'overtime_factor_daily': 1.5, + # 'overtime_factor_holiday': 2.0, + # 'work_days': 20, + # 'work_hour': 8, + # 'journal_overtime_id': self.journal.id, + # 'account_overtime_id': self.overtime_account.id, + }) + + self.working_hours.write({ + 'max_overtime_hour': 20.0, + 'overtime_factor_daily': 1.5, + 'overtime_factor_holiday': 2.0, + 'work_days': 30, + 'work_hour': 8, + 'journal_overtime_id': self.journal.id, + 'account_overtime_id': self.overtime_account.id, + }) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Ahmed Tester', + 'state': 'open', + 'first_hiring_date': date.today() - timedelta(days=365), # موظف منذ سنة + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract for Ahmed', + 'employee_id': self.employee.id, + 'state': 'open', + 'wage': 5000, # الراتب الأساسي + 'resource_calendar_id': self.working_hours.id, + # 'total_allowance': 1000, + # 'salary': 5000, + }) + self.contract.write({ + 'total_allowance': 1000, + 'salary': 5000, + }) + + def test_overtime_period_constraint_cross_month(self): + + # Arrange + date_from = date(2025, 1, 31) + date_to = date(2025, 2, 1) # شهر مختلف + + # Act & Assert + with self.assertRaises(UserError): + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'request_date': date_from, + 'date_from': date_from, + 'date_to': date_to, + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 5.0, + })] + }) + + request.line_ids_over_time.get_max_remain_hours() + + def test_max_hours_constraint_and_exception(self): + + # Arrange + self.working_hours.write({'max_overtime_hour': 10.0}) + + date_from = date(2025, 3, 1) + date_to = date(2025, 3, 5) + + with self.assertRaises(UserError): + self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date_from, + 'date_to': date_to, + 'exception': False, + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 15.0, + })] + }) + + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date_from, + 'date_to': date_to, + 'exception': True, + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 15.0, + })] + }) + + # Assert + self.assertTrue(request.line_ids_over_time.exception, "Exception flag should be propagated to lines") + + def test_calculate_daily_rate(self): + + expected_hourly_rate = 8500.0 / 240.0 + + # Act + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date(2025, 4, 1), + 'date_to': date(2025, 4, 1), + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 2.0, + })] + }) + + line = request.line_ids_over_time[0] + + # Assert + self.assertAlmostEqual(line.daily_hourly_rate, expected_hourly_rate, places=2, + msg="Daily hourly rate calculation is incorrect") + + expected_total_price = expected_hourly_rate * 2.0 + self.assertAlmostEqual(line.price_hour, expected_total_price, places=2, + msg="Total price calculation is incorrect") + + def test_accounting_transfer_validation(self): + + # Arrange + request = self.env['employee.overtime.request'].create({ + 'employee_id': self.employee.id, + 'date_from': date(2025, 5, 1), + 'date_to': date(2025, 5, 2), + 'transfer_type': 'accounting', + 'line_ids_over_time': [(0, 0, { + 'employee_id': self.employee.id, + 'over_time_workdays_hours': 10.0, + })] + }) + + request.state = 'executive_office' + + # Act + request.validated() + + # Assert + self.assertEqual(request.state, 'validated', "State should be validated") + line = request.line_ids_over_time[0] + + self.assertTrue(line.move_id, "Account Move should be created") + self.assertEqual(line.move_id.state, 'draft', "Move should be in draft state initially") + + debit_line = line.move_id.line_ids.filtered(lambda l: l.debit > 0) + credit_line = line.move_id.line_ids.filtered(lambda l: l.credit > 0) + + self.assertTrue(debit_line and credit_line, "Should have debit and credit lines") + self.assertEqual(debit_line.account_id, self.overtime_account, "Debit account mismatch") + self.assertEqual(credit_line.account_id, self.journal.default_account_id, "Credit account mismatch") + self.assertAlmostEqual(debit_line.debit, line.price_hour, places=2, msg="Debit amount mismatch") + diff --git a/exp_employee_custody/tests/__init__.py b/exp_employee_custody/tests/__init__.py new file mode 100644 index 0000000..dc1dd70 --- /dev/null +++ b/exp_employee_custody/tests/__init__.py @@ -0,0 +1 @@ +from . import test_custody_receiving \ No newline at end of file diff --git a/exp_employee_custody/tests/test_custody_receiving.py b/exp_employee_custody/tests/test_custody_receiving.py new file mode 100644 index 0000000..9b6381c --- /dev/null +++ b/exp_employee_custody/tests/test_custody_receiving.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import ValidationError +from odoo.tests import tagged +from datetime import date + + +@tagged('post_install', '-at_install') +class TestCustodyReceiving(TransactionCase): + + def setUp(self): + super(TestCustodyReceiving, self).setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee for Custody', + 'emp_no': 'EMP001', + }) + + self.deduction_category = self.env['hr.salary.rule.category'].create({ + 'name': 'Deduction', + 'code': 'DED', + 'rule_type': 'deduction', + }) + + self.salary_rule = self.env['hr.salary.rule'].create({ + 'name': 'Custody Deduction Rule', + 'sequence': 1, + 'code': 'CUST_DED', + 'category_id': self.deduction_category.id, + 'condition_select': 'none', + 'amount_select': 'fix', + 'amount_fix': 0.0, + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract Test', + 'employee_id': self.employee.id, + 'wage': 5000.0, + 'state': 'open', + }) + + self.custody_delivered = self.env['custom.employee.custody'].create({ + 'employee_id': self.employee.id, + 'state': 'approve', + }) + + self.custody_line = self.env['employee.custody.line'].create({ + 'employee_custody_line': self.custody_delivered.id, + 'name': 'Laptop Dell', + 'serial': 'SN123456', + 'quantity': 1.0, + 'receiving_quantity': 0.0, + 'amount': 0.0, + }) + + def test_01_custody_receiving_workflow_full_return(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + 'note': 'Returning Laptop', + }) + + + receiving._get_custody_line_domain() + + self.assertTrue(receiving.return_custody_line_ids, "Should fetch pending custody lines") + line = receiving.return_custody_line_ids[0] + self.assertEqual(line.custody_line_id, self.custody_line, "Should link to correct original line") + self.assertEqual(line.quantity, 1.0, "Should default to remaining quantity") + + receiving.send() + self.assertEqual(receiving.state, 'submit') + + receiving.dr_manager() + self.assertEqual(receiving.state, 'direct') + + receiving.dr_hr_manager() + self.assertEqual(receiving.state, 'admin') + + receiving.warehouse_keeper() + self.assertEqual(receiving.state, 'approve') + + receiving.done() + self.assertEqual(receiving.state, 'done') + + + self.assertEqual(self.custody_line.receiving_quantity, 1.0, "Original line receiving qty should be updated") + + self.assertEqual(self.custody_delivered.state, 'done', "Original custody should be done as all items returned") + + def test_02_custody_receiving_with_deduction(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + 'salary_rule_id': self.salary_rule.id, + }) + receiving._get_custody_line_domain() + + line = receiving.return_custody_line_ids[0] + line.deduction_amount = 500.0 + + receiving.compute_deduction_amount() + self.assertEqual(receiving.deduction_amount, 500.0) + self.assertTrue(receiving.salary_rule_flag) + + receiving.state = 'approve' + receiving.done() + + self.assertTrue(receiving.advantage_line_id, "Should create advantage line") + self.assertEqual(receiving.advantage_line_id.amount, 500.0, "Advantage amount mismatch") + self.assertEqual(receiving.advantage_line_id.contract_advantage_id, self.contract) + + def test_03_validation_over_quantity(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + }) + receiving._get_custody_line_domain() + + line = receiving.return_custody_line_ids[0] + line.quantity = 2.0 + + receiving.state = 'approve' + + with self.assertRaises(ValidationError): + receiving.done() + + def test_04_reset_to_draft(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + 'salary_rule_id': self.salary_rule.id, + }) + receiving._get_custody_line_domain() + + receiving.return_custody_line_ids[0].deduction_amount = 100.0 + receiving.compute_deduction_amount() + receiving.state = 'approve' + receiving.done() + + self.assertEqual(receiving.state, 'done') + self.assertTrue(receiving.advantage_line_id) + + receiving.set_to_draft() + + self.assertEqual(receiving.state, 'draft') + self.assertFalse(receiving.advantage_line_id.exists(), "Advantage line should be deleted") + self.assertEqual(self.custody_line.receiving_quantity, 0.0, "Original qty should be reverted") + self.assertEqual(self.custody_delivered.state, 'approve') + + def test_05_unlink_protection(self): + + receiving = self.env['hr.custody.receiving'].create({ + 'employee_id': self.employee.id, + }) + receiving.send() + + with self.assertRaises(ValidationError): + receiving.unlink() \ No newline at end of file diff --git a/exp_hr_payroll/models/hr_payslip.py b/exp_hr_payroll/models/hr_payslip.py index d845ad4..eaf60b3 100644 --- a/exp_hr_payroll/models/hr_payslip.py +++ b/exp_hr_payroll/models/hr_payslip.py @@ -182,14 +182,14 @@ class HrPayslip(models.Model): current_leave_struct['number_of_days'] += hours / work_hours # compute worked days - work_data = contract.employee_id._get_work_days_data(day_from, day_to, + work_data = contract.employee_id._get_work_days_data_batch(day_from, day_to, calendar=contract.resource_calendar_id) attendances = { 'name': _("Normal Working Days paid at 100%"), 'sequence': 1, 'code': 'WORK100', - 'number_of_days': work_data['days'], - 'number_of_hours': work_data['hours'], + 'number_of_days': work_data.get('days', 0.0), + 'number_of_hours': work_data.get('hours', 0.0), 'contract_id': contract.id, } diff --git a/exp_hr_payroll/models/hr_salary_rule.py b/exp_hr_payroll/models/hr_salary_rule.py index d19d53a..236dbe2 100644 --- a/exp_hr_payroll/models/hr_salary_rule.py +++ b/exp_hr_payroll/models/hr_salary_rule.py @@ -28,10 +28,10 @@ class HrPayrollStructure(models.Model): rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id', string='Salary Rules') - @api.constrains('parent_id') - def _check_parent_id(self): - if not self._check_recursion(): - raise ValidationError(_('You cannot create a recursive salary structure.')) + # @api.constrains('parent_id') + # def _check_parent_id(self): + # if not self._has_cycle(): + # raise ValidationError(_('You cannot create a recursive salary structure.')) def copy(self, default=None): self.ensure_one() @@ -85,7 +85,7 @@ class HrSalaryRuleCategory(models.Model): @api.constrains('parent_id') def _check_parent_id(self): - if not self._check_recursion(): + if not self._has_cycle(): raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rule Category.')) @@ -176,7 +176,7 @@ class HrSalaryRule(models.Model): @api.constrains('parent_rule_id') def _check_parent_rule_id(self): - if not self._check_recursion(parent='parent_rule_id'): + if not self._has_cycle('parent_rule_id'): raise ValidationError(_('Error! You cannot create recursive hierarchy of Salary Rules.')) def _recursive_search_of_rules(self): diff --git a/exp_hr_payroll/tests/__init__.py b/exp_hr_payroll/tests/__init__.py new file mode 100644 index 0000000..bb7aa92 --- /dev/null +++ b/exp_hr_payroll/tests/__init__.py @@ -0,0 +1 @@ +from . import test_payroll_rules \ No newline at end of file diff --git a/exp_hr_payroll/tests/test_payroll_rules.py b/exp_hr_payroll/tests/test_payroll_rules.py new file mode 100644 index 0000000..4311de5 --- /dev/null +++ b/exp_hr_payroll/tests/test_payroll_rules.py @@ -0,0 +1,224 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo import fields +from datetime import date, datetime, timedelta + + +class TestPayrollRules(TransactionCase): + + def setUp(self): + super(TestPayrollRules, self).setUp() + + self.env.user.tz = 'UTC' + self.company = self.env.ref('base.main_company') + + self.category_basic = self.env['hr.salary.rule.category'].create({ + 'name': 'Basic', + 'code': 'BASIC', + }) + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Test Structure', + 'code': 'TEST_STRUCT', + 'company_id': self.company.id, + }) + + attendances = [] + for day in range(5): # 0=Monday to 4=Friday + attendances.append((0, 0, { + 'name': 'Morning', + 'dayofweek': str(day), + 'hour_from': 8, + 'hour_to': 12, + 'day_period': 'morning', + })) + attendances.append((0, 0, { + 'name': 'Afternoon', + 'dayofweek': str(day), + 'hour_from': 13, + 'hour_to': 17, + 'day_period': 'afternoon', + })) + + self.calendar = self.env['resource.calendar'].create({ + 'name': 'Standard 40 Hours UTC', + 'tz': 'UTC', + 'hours_per_day': 8.0, + 'attendance_ids': attendances, + 'company_id': self.company.id, + }) + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee Payroll', + 'company_id': self.company.id, + 'resource_calendar_id': self.calendar.id, + }) + + if self.employee.resource_id: + self.employee.resource_id.write({ + 'calendar_id': self.calendar.id, + 'tz': 'UTC', + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract for Test', + 'employee_id': self.employee.id, + 'struct_id': self.structure.id, + 'wage': 5000.0, + 'state': 'open', + 'date_start': date.today() - timedelta(days=100), + 'resource_calendar_id': self.calendar.id, + 'schedule_pay': 'monthly', + 'company_id': self.company.id, + }) + + self.payslip = self.env['hr.payslip'].create({ + 'employee_id': self.employee.id, + 'contract_id': self.contract.id, + 'struct_id': self.structure.id, + 'date_from': date.today().replace(day=1), + 'date_to': (date.today().replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1), + 'company_id': self.company.id, + }) + + def test_satisfy_condition_python(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Python Condition Rule', + 'sequence': 10, + 'code': 'PY_COND', + 'category_id': self.category_basic.id, + 'condition_select': 'python', + 'condition_python': 'result = contract.wage > 3000', + 'amount_select': 'fix', + 'amount_fix': 100.0, + }) + localdict = {'contract': self.contract, 'employee': self.employee} + self.assertTrue(rule._satisfy_condition(localdict)) + + rule.condition_python = 'result = contract.wage > 6000' + self.assertFalse(rule._satisfy_condition(localdict)) + + def test_satisfy_condition_range(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Range Condition Rule', + 'sequence': 10, + 'code': 'RANGE_COND', + 'category_id': self.category_basic.id, + 'condition_select': 'range', + 'condition_range': 'contract.wage', + 'condition_range_min': 1000, + 'condition_range_max': 6000, + 'amount_select': 'fix', + 'amount_fix': 100.0, + }) + localdict = {'contract': self.contract} + self.assertTrue(rule._satisfy_condition(localdict)) + + self.contract.wage = 8000 + self.assertFalse(rule._satisfy_condition(localdict)) + + def test_compute_rule_percentage(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Percentage Rule', + 'sequence': 10, + 'code': 'PERCENT', + 'category_id': self.category_basic.id, + 'amount_select': 'percentage', + 'amount_percentage_base': 'contract.wage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + }) + localdict = {'contract': self.contract} + amount, qty, rate = rule._compute_rule(localdict) + self.assertEqual(amount, 5000.0) + self.assertEqual(amount * qty * rate / 100.0, 500.0) + + def test_compute_rule_python_code(self): + rule = self.env['hr.salary.rule'].create({ + 'name': 'Python Code Rule', + 'sequence': 10, + 'code': 'PY_CODE', + 'category_id': self.category_basic.id, + 'amount_select': 'code', + 'amount_python_compute': 'result = contract.wage + 500', + }) + localdict = {'contract': self.contract} + amount, qty, rate = rule._compute_rule(localdict) + self.assertEqual(amount, 5500.0) + + def test_get_contract(self): + old_contract = self.env['hr.contract'].create({ + 'name': 'Old Contract', + 'employee_id': self.employee.id, + 'wage': 4000, + 'state': 'close', + 'date_start': date.today() - timedelta(days=400), + 'date_end': date.today() - timedelta(days=200), + 'resource_calendar_id': self.calendar.id, + 'struct_id': self.structure.id, + }) + + date_from = date.today().replace(day=1) + date_to = (date.today().replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1) + contract_ids = self.env['hr.payslip'].get_contract(self.employee, date_from, date_to) + + self.assertIn(self.contract.id, contract_ids) + self.assertNotIn(old_contract.id, contract_ids) + + def test_get_worked_day_lines(self): + today = date.today() + days_ahead = 0 - today.weekday() + if days_ahead <= 0: + days_ahead += 7 + + next_monday = today + timedelta(days=days_ahead) + date_from = next_monday + date_to = next_monday + timedelta(days=4) + try: + worked_days = self.env['hr.payslip'].get_worked_day_lines(self.contract, date_from, date_to) + + work_entry = next((item for item in worked_days if item['code'] == 'WORK100'), None) + + self.assertIsNotNone(work_entry, "WORK100 entry not found in result") + + except AttributeError as e: + print(f"Skipping test_get_worked_day_lines due to missing dependency: {e}") + + def test_payslip_line_compute_total(self): + line = self.env['hr.payslip.line'].create({ + 'slip_id': self.payslip.id, + 'name': 'Test Line', + 'code': 'TEST', + 'contract_id': self.contract.id, + 'salary_rule_id': self.env['hr.salary.rule'].search([], limit=1).id, + 'employee_id': self.employee.id, + 'quantity': 2.0, + 'amount': 500.0, + 'rate': 50.0, + 'category_id': self.category_basic.id, + }) + self.assertEqual(line.total, 500.0) + + def test_get_inputs(self): + rule_with_input = self.env['hr.salary.rule'].create({ + 'name': 'Rule with Input', + 'code': 'INPUT_RULE', + 'category_id': self.category_basic.id, + 'amount_select': 'fix', + 'amount_fix': 0.0, + 'sequence': 50, + }) + self.env['hr.rule.input'].create({ + 'name': 'Commission Input', + 'code': 'COMMISSION', + 'input_id': rule_with_input.id, + }) + self.structure.write({'rule_ids': [(4, rule_with_input.id)]}) + + date_from = date.today() + date_to = date.today() + inputs = self.env['hr.payslip'].get_inputs(self.contract, date_from, date_to) + + found_input = next((i for i in inputs if i['code'] == 'COMMISSION'), None) + self.assertIsNotNone(found_input) + self.assertEqual(found_input['contract_id'], self.contract.id) \ No newline at end of file diff --git a/exp_official_mission/models/hr_official_mission.py b/exp_official_mission/models/hr_official_mission.py index 2177538..2c5b699 100644 --- a/exp_official_mission/models/hr_official_mission.py +++ b/exp_official_mission/models/hr_official_mission.py @@ -1175,7 +1175,7 @@ class HrOfficialMissionEmployee(models.Model): def check_dates(self): for rec in self: if rec.hour_from >= 24 or rec.hour_to >= 24: - raise exceptions.ValidationError(_('Wrong Time Format.!')) + raise ValidationError(_('Wrong Time Format.!')) date_from = datetime.strptime(str(rec.date_from), DEFAULT_SERVER_DATE_FORMAT).date() date_to = datetime.strptime(str(rec.date_to), DEFAULT_SERVER_DATE_FORMAT).date() delta = timedelta(days=1) @@ -1190,7 +1190,7 @@ class HrOfficialMissionEmployee(models.Model): '&', ('hour_from', '>=', rec.hour_from), ('hour_to', '<=', rec.hour_to), ]) if missions_ids: - raise exceptions.ValidationError(_('Sorry The Employee %s Actually On %s For this Time') % + raise ValidationError(_('Sorry The Employee %s Actually On %s For this Time') % (rec.employee_id.name, missions_ids.official_mission_id.mission_type.name)) date_from += delta @@ -1203,7 +1203,7 @@ class HrOfficialMissionEmployee(models.Model): ('official_mission_id.process_type', '=', 'training'), ('official_mission_id.course_name.id', '=', item.official_mission_id.course_name.id)]) if duplicated: - raise exceptions.ValidationError( + raise ValidationError( _("Employee %s has already take this course.") % (item.employee_id.name)) if item.official_mission_id and item.official_mission_id.mission_type.duration_type == 'days' \ and item.date_from and item.date_to: @@ -1230,7 +1230,7 @@ class HrOfficialMissionEmployee(models.Model): if year_last_record == year_now_record: number_days = number_days + rec.days if number_days > days_per_year: - raise exceptions.ValidationError( + raise ValidationError( _("Sorry The Employee %s, The Number of Requests Cannot Exceed %s Maximum Days Per year.") % ( rec.employee_id.name, days_per_year)) #### @@ -1427,7 +1427,7 @@ class HrOfficialMissionEmployee(models.Model): def compute_number_of_hours(self): for item in self: if item.hour_from >= 24 or item.hour_to >= 24: - raise exceptions.ValidationError(_('Wrong Time Format.!')) + raise ValidationError(_('Wrong Time Format.!')) if item.official_mission_id.hour_to and item.official_mission_id.hour_from: if item.hour_from and item.hour_to: if (item.hour_to - item.hour_from) < 0: @@ -1788,7 +1788,7 @@ class MissionTable(models.Model): date_to = rec.destination_id.date_to if date_from and date_to: if not (date_from <= rec.date <= date_to): - raise exceptions.ValidationError( + raise ValidationError( _("The mission date %(date)s must be between destination's date from %(date_from)s and date to %(date_to)s.", date=rec.date, date_from=date_from, date_to=date_to) ) diff --git a/exp_official_mission/tests/__init__.py b/exp_official_mission/tests/__init__.py new file mode 100644 index 0000000..075bb68 --- /dev/null +++ b/exp_official_mission/tests/__init__.py @@ -0,0 +1 @@ +from . import test_official_mission \ No newline at end of file diff --git a/exp_official_mission/tests/test_official_mission.py b/exp_official_mission/tests/test_official_mission.py new file mode 100644 index 0000000..14af7bd --- /dev/null +++ b/exp_official_mission/tests/test_official_mission.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged +from datetime import date, timedelta + + +@tagged('post_install', '-at_install') +class TestOfficialMission(TransactionCase): + + def setUp(self): + super(TestOfficialMission, self).setUp() + + self.company = self.env.company + + self.account = self.env['account.account'].create({ + 'name': 'Mission Expense', + 'code': '600000', + 'account_type': 'expense', + 'reconcile': False, + }) + + self.journal = self.env['account.journal'].create({ + 'name': 'Mission Journal', + 'type': 'general', + 'code': 'MISS', + 'default_account_id': self.account.id, + }) + + self.emp_type = self.env['hr.contract.type'].create({'name': 'Permanent'}) + + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + 'work_email': 'test@example.com', + 'employee_type_id': self.emp_type.id, + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Test Contract', + 'employee_id': self.employee.id, + 'wage': 5000, + 'state': 'open', + }) + self.employee.contract_id = self.contract + + self.mission_type = self.env['hr.official.mission.type'].create({ + 'name': 'External Mission', + 'duration_type': 'days', + 'related_with_financial': True, + 'type_of_payment': 'fixed', + 'day_price': 100.0, + 'journal_id': self.journal.id, + 'account_id': self.account.id, + 'transfer_by_emp_type': False, + 'total_months': 12, + 'max_request_number': 5, + }) + def test_01_duration_calculation(self): + date_from = date.today() + date_to = date.today() + timedelta(days=4) + mission = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date_from, + 'date_to': date_to, + }) + + mission._get_mission_no() + + expected_days = 5 + self.assertEqual(mission.date_duration, expected_days, "Duration in days calculated incorrectly") + + def test_02_workflow_and_financials(self): + mission = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today(), + 'date_to': date.today() + timedelta(days=2), + 'move_type': 'accounting', + }) + + + mission_line = self.env['hr.official.mission.employee'].create({ + 'official_mission_id': mission.id, + 'employee_id': self.employee.id, + 'date_from': mission.date_from, + 'date_to': mission.date_to, + 'hour_from': 8.0, + 'hour_to': 16.0, + }) + + mission_line.days = 3 + mission_line.amount = 300.0 + + + self.assertEqual(mission_line.days, 3, "Employee line days incorrect") + self.assertEqual(mission_line.amount, 300.0, "Employee amount calculation incorrect") + + mission.send() + self.assertEqual(mission.state, 'send') + + mission.accounting_manager() + mission.depart_manager() + + mission.approve() + + self.assertEqual(mission.state, 'approve', "Mission should be approved") + + self.assertTrue(mission_line.account_move_id, "Journal Entry should be created") + self.assertEqual(mission_line.account_move_id.state, 'draft', "Journal Entry should be draft initially") + + move_lines = mission_line.account_move_id.line_ids + debit_line = move_lines.filtered(lambda l: l.debit > 0) + self.assertEqual(debit_line.debit, 300.0, "Journal Entry amount mismatch") + def test_03_overlap_constraint(self): + + mission1 = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today(), + 'date_to': date.today() + timedelta(days=5), + }) + line1 = self.env['hr.official.mission.employee'].create({ + 'official_mission_id': mission1.id, + 'employee_id': self.employee.id, + 'date_from': date.today(), + 'date_to': date.today() + timedelta(days=5), + 'hour_from': 8, + 'hour_to': 16, + }) + mission1.state = 'approve' + mission2 = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today() + timedelta(days=2), + 'date_to': date.today() + timedelta(days=6), + }) + + + with self.assertRaises(ValidationError): + self.env['hr.official.mission.employee'].create({ + 'official_mission_id': mission2.id, + 'employee_id': self.employee.id, + 'date_from': date.today() + timedelta(days=2), + 'date_to': date.today() + timedelta(days=6), + 'hour_from': 8, + 'hour_to': 16, + }) + + def test_04_employees_required(self): + mission = self.env['hr.official.mission'].create({ + 'mission_type': self.mission_type.id, + 'date_from': date.today(), + 'date_to': date.today(), + }) + + with self.assertRaises(UserError): + mission.send() \ No newline at end of file diff --git a/exp_payroll_custom/__manifest__.py b/exp_payroll_custom/__manifest__.py index 2487668..bd37b35 100644 --- a/exp_payroll_custom/__manifest__.py +++ b/exp_payroll_custom/__manifest__.py @@ -66,4 +66,6 @@ 'installable': True, 'auto_install': False, 'application': True, + 'test_tags': ['standard', 'at_install'], + } diff --git a/exp_payroll_custom/models/hr_advance_payslip.py b/exp_payroll_custom/models/hr_advance_payslip.py index b499ed8..c05ac92 100644 --- a/exp_payroll_custom/models/hr_advance_payslip.py +++ b/exp_payroll_custom/models/hr_advance_payslip.py @@ -104,25 +104,26 @@ class SalaryRuleInput(models.Model): def withdraw(self): payslip = self.env['hr.payslip'].search([('number', '=', self.number)]) - loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', self.employee_id.id)]) - if self.number == payslip.number: - if self.loan_ids: - for loan in self.loan_ids: - loan.paid = False - if loans: - for i in loans: - if i.id == loan.loan_id.id: - for l in i.deduction_lines: - if loan.date == l.installment_date and loan.paid is False: - l.paid = False - #i.remaining_loan_amount += l.installment_amount - i.get_remaining_loan_amount() + if 'hr.loan.salary.advance' in self.env: + loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', self.employee_id.id)]) + if self.number == payslip.number: + if self.loan_ids: + for loan in self.loan_ids: + loan.paid = False + if loans: + for i in loans: + if i.id == loan.loan_id.id: + for l in i.deduction_lines: + if loan.date == l.installment_date and loan.paid is False: + l.paid = False + #i.remaining_loan_amount += l.installment_amount + i.get_remaining_loan_amount() - # check remaining loan and change state to pay - if i.state == 'closed' and i.remaining_loan_amount > 0.0: - i.state = 'pay' - elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: - i.state = 'closed' + # check remaining loan and change state to pay + if i.state == 'closed' and i.remaining_loan_amount > 0.0: + i.state = 'pay' + elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: + i.state = 'closed' for line in payslip.worked_days_line_ids: if line.name != 'Working days for this month': @@ -852,38 +853,38 @@ class SalaryRuleInput(models.Model): d.amount = d.amount payslip.deduction_ids = [fields.Command.set(deductions.ids)] - # Loans # - loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', payslip.employee_id.id), - ('request_type.refund_from', '=', 'salary'), - ('state', '=', 'pay')]).filtered( - lambda item: item.employee_id.state == 'open') - if loans: - for loan in loans: - for l in loan.deduction_lines: - if not l.paid and ( - str(l.installment_date) <= str(payslip.date_from) or str(l.installment_date) <= str( - payslip.date_to)): - employee_loan_id = payslip.loan_ids.filtered( - lambda item: item.name == loan.request_type.name) - if not employee_loan_id: - payslip_loans.append({ - 'name': loan.request_type.name, - 'code': loan.code, - 'amount': round((-l.installment_amount), 2), - 'date': l.installment_date, - 'account_id': loan.request_type.account_id.id, - 'loan_id': loan.id - }) - l.paid = True - l.payment_date = payslip.date_to - else: - payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] + if 'hr.loan.salary.advance' in self.env: + loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', payslip.employee_id.id), + ('request_type.refund_from', '=', 'salary'), + ('state', '=', 'pay')]).filtered( + lambda item: item.employee_id.state == 'open') + if loans: + for loan in loans: + for l in loan.deduction_lines: + if not l.paid and ( + str(l.installment_date) <= str(payslip.date_from) or str(l.installment_date) <= str( + payslip.date_to)): + employee_loan_id = payslip.loan_ids.filtered( + lambda item: item.name == loan.request_type.name) + if not employee_loan_id: + payslip_loans.append({ + 'name': loan.request_type.name, + 'code': loan.code, + 'amount': round((-l.installment_amount), 2), + 'date': l.installment_date, + 'account_id': loan.request_type.account_id.id, + 'loan_id': loan.id + }) + l.paid = True + l.payment_date = payslip.date_to + else: + payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] - # check remaining loan and change state to closed - if loan.remaining_loan_amount <= 0.0 < loan.gm_propos_amount: - loan.state = 'closed' + # check remaining loan and change state to closed + if loan.remaining_loan_amount <= 0.0 < loan.gm_propos_amount: + loan.state = 'closed' - payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] + payslip.loan_ids = [(0, 0, loan_item) for loan_item in payslip_loans] payslip.allowance_ids._compute_total() payslip.deduction_ids._compute_total() for pay in payslip: @@ -2963,29 +2964,30 @@ class HrPayslipRun(models.Model): def withdraw(self): for line in self.slip_ids: payslip = self.env['hr.payslip'].search([('number', '=', line.number)]) - loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', line.employee_id.id)]) - if line.number == payslip.number: - if line.loan_ids: - for loan in line.loan_ids: - loan.paid = False - if loans: - for i in loans: - if i.id == loan.loan_id.id: - for l in i.deduction_lines: - if loan.date == l.installment_date and loan.paid is False: - l.paid = False - l.payment_date = False - #i.remaining_loan_amount += l.installment_amount - i.get_remaining_loan_amount() + if 'hr.loan.salary.advance' in self.env: + loans = self.env['hr.loan.salary.advance'].search([('employee_id', '=', line.employee_id.id)]) + if line.number == payslip.number: + if line.loan_ids: + for loan in line.loan_ids: + loan.paid = False + if loans: + for i in loans: + if i.id == loan.loan_id.id: + for l in i.deduction_lines: + if loan.date == l.installment_date and loan.paid is False: + l.paid = False + l.payment_date = False + #i.remaining_loan_amount += l.installment_amount + i.get_remaining_loan_amount() - # check remaining loan and change state to pay - if i.state == 'closed' and i.remaining_loan_amount > 0.0: - i.state = 'pay' - elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: - i.state = 'closed' - for record in payslip: - record.write({'state': 'draft'}) - record.unlink() + # check remaining loan and change state to pay + if i.state == 'closed' and i.remaining_loan_amount > 0.0: + i.state = 'pay' + elif i.remaining_loan_amount == 0.0 and i.gm_propos_amount > 0.0: + i.state = 'closed' + for record in payslip: + record.write({'state': 'draft'}) + record.unlink() self.write({'slip_ids': [fields.Command.clear()]}) self.write({'state': 'draft'}) diff --git a/exp_payroll_custom/models/hr_contract.py b/exp_payroll_custom/models/hr_contract.py index f5338fb..2e9c778 100644 --- a/exp_payroll_custom/models/hr_contract.py +++ b/exp_payroll_custom/models/hr_contract.py @@ -5,6 +5,11 @@ from datetime import datetime from odoo import models, fields, api, _ from odoo.exceptions import UserError +# -*- coding: utf-8 -*- +from datetime import datetime +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + class HrContractSalaryScale(models.Model): _inherit = 'hr.contract' @@ -15,84 +20,91 @@ class HrContractSalaryScale(models.Model): salary_degree = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) hide = fields.Boolean(string='Hide', compute="compute_type") required_condition = fields.Boolean(string='Required Condition', compute='compute_move_type') - total_allowance = fields.Float(string='Total Allowance', compute='compute_function',store=True) - total_deduction = fields.Float(string='Total Deduction', compute='compute_function',store=True) - total_net = fields.Float(string='Total Net', compute='compute_function',store=True) + total_allowance = fields.Float(string='Total Allowance', compute='compute_function', store=True) + total_deduction = fields.Float(string='Total Deduction', compute='compute_function', store=True) + total_net = fields.Float(string='Total Net', compute='compute_function', store=True) advantages = fields.One2many('contract.advantage', 'contract_advantage_id', string='Advantages') - house_allowance_temp = fields.Float(string='House Allowance', compute='compute_function',store=True) - transport_allowance = fields.Float(string='Transport Allowance', compute='compute_function',store=True) + house_allowance_temp = fields.Float(string='House Allowance', compute='compute_function', store=True) + transport_allowance = fields.Float(string='Transport Allowance', compute='compute_function', store=True) @api.constrains('advantages', 'salary', 'salary_group') def amount_constrains(self): for rec in self: - localdict = dict(employee=rec.employee_id.id, contract=rec.env['hr.contract'].search([ - ('employee_id', '=', rec.employee_id.id)])) + localdict = dict(employee=rec.employee_id, contract=rec) + if rec.salary_group.gread_max > 0 and rec.salary_group.gread_min > 0: - if rec.salary > rec.salary_group.gread_max or rec.salary < rec.salary_group.gread_min: - raise UserError(_('The Basic Salary Is Greater Than Group Gread Max Or less than Gread Min')) - for item in self.advantages: - item.to_get_contract_id() - if item.benefits_discounts._compute_rule(localdict)[0] < item.amount and item.type == 'exception': - raise UserError(_( - 'The amount you put is greater than fact value of this Salary rule %s (%s).') % ( - item.benefits_discounts.name, item.benefits_discounts.code)) + if rec.salary > rec.salary_group.gread_max or rec.salary < rec.salary_group.gread_min: + raise UserError(_('The Basic Salary Is Greater Than Group Gread Max Or less than Gread Min')) + + for item in rec.advantages: + if item.type == 'exception': + rule_val = item.benefits_discounts._compute_rule(localdict)[0] + if rule_val < item.amount: + raise UserError(_( + 'The amount you put is greater than fact value of this Salary rule %s (%s).') % ( + item.benefits_discounts.name, item.benefits_discounts.code)) @api.depends('salary_scale.transfer_type') def compute_move_type(self): - self.compute_function() + # self.compute_function() if self.salary_scale.transfer_type == 'one_by_one': self.required_condition = True else: self.required_condition = False - @api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree','salary','advantages','house_allowance_temp','transport_allowance','total_deduction','salary_insurnce','total_allowance','state') + @api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree', 'salary', 'advantages', + 'house_allowance_temp', 'transport_allowance', 'total_deduction', 'total_allowance', 'state') def compute_function(self): for item in self: item.house_allowance_temp = 0 item.transport_allowance = 0 item.total_net = 0 - contract = self.env['hr.contract'].search([('employee_id', '=', item.employee_id.id)]) - localdict = dict(employee=item.employee_id.id, contract=contract) - current_date = datetime.now().date() - # customize type in advantages + localdict = dict(employee=item.employee_id, contract=item) + current_date = fields.Date.today() + allowance_customize_items = item.advantages.filtered( lambda key: key.type == 'customize' and key.out_rule is False and - key.benefits_discounts.category_id.rule_type == 'allowance' and - (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) - >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) + key.benefits_discounts.category_id.rule_type == 'allowance' and + (key.date_to if key.date_to else current_date) >= current_date >= key.date_from + ) allow_sum_custom = sum(x.amount for x in allowance_customize_items) for x in allowance_customize_items: if x.benefits_discounts.rules_type == 'house': item.house_allowance_temp += x.amount - if x.benefits_discounts.rules_type == 'transport': item.transport_allowance += x.amount - # allow_custom_ids = [record.benefits_discounts.id for record in allowance_customize_items] deduction_customize_items = item.advantages.filtered( lambda key: key.type == 'customize' and key.out_rule is False and key.benefits_discounts.category_id.rule_type == 'deduction' and - (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) - >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) + (key.date_to if key.date_to else current_date) >= current_date >= key.date_from + ) ded_sum_custom = sum(x.amount for x in deduction_customize_items) - ded_custom_ids = [record.benefits_discounts.id for record in deduction_customize_items] + ded_custom_ids = deduction_customize_items.mapped('benefits_discounts.id') - # exception type in advantages exception_items = item.advantages.filtered(lambda key: key.type == 'exception') + + if exception_items: + exception_items = exception_items.filtered( + lambda key: (key.date_to.month if key.date_to else current_date.month) + >= current_date.month >= key.date_from.month + ) + total_rule_result, sum_except, sum_customize_expect = 0.0, 0.0, 0.0 for x in exception_items: rule_result = x.benefits_discounts._compute_rule(localdict)[0] - if x.date_from >= str(current_date): + + if x.date_from >= current_date: total_rule_result = rule_result - elif str(current_date) > x.date_from: - if x.date_to and str(current_date) <= x.date_to: + elif current_date > x.date_from: + if x.date_to and current_date <= x.date_to: total_rule_result = rule_result - x.amount - elif x.date_to and str(current_date) >= x.date_to: - total_rule_result = 0 # rule_result + elif x.date_to and current_date >= x.date_to: + total_rule_result = 0 elif not x.date_to: total_rule_result = rule_result - x.amount else: @@ -107,85 +119,42 @@ class HrContractSalaryScale(models.Model): else: sum_except += total_rule_result - if exception_items: - exception_items = item.advantages.filtered( - lambda key: (datetime.strptime(str(key.date_to), - "%Y-%m-%d").date().month if key.date_to else current_date.month) - >= current_date.month >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date().month) - - except_ids = [record.benefits_discounts.id for record in exception_items] + except_ids = exception_items.mapped('benefits_discounts.id') rule_ids = item.salary_scale.rule_ids.filtered( lambda key: key.id not in ded_custom_ids and key.id not in except_ids) - level_rule_ids = item.salary_level.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) - # key.id not in allow_custom_ids and key.id not in ded_custom_ids and + if item.salary_level: + rule_ids += item.salary_level.rule_ids.filtered( + lambda key: key.id not in except_ids) - group_rule_ids = item.salary_group.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) - # key.id not in allow_custom_ids and key.id not in ded_custom_ids and + if item.salary_group: + rule_ids += item.salary_group.rule_ids.filtered( + lambda key: key.id not in except_ids) total_allowance = 0 total_ded = 0 + for line in rule_ids: + try: + amount = line._compute_rule(localdict)[0] + except Exception: + amount = 0.0 + if line.category_id.rule_type == 'allowance': - try: - total_allowance += line._compute_rule(localdict)[0] - except: - total_allowance += 0 - - if line.category_id.rule_type == 'deduction': - try: - total_ded += line._compute_rule(localdict)[0] - except: - total_ded += 0 - + total_allowance += amount + elif line.category_id.rule_type == 'deduction': + total_ded += amount if line.rules_type == 'house': - item.house_allowance_temp += line._compute_rule(localdict)[0] + item.house_allowance_temp += amount if line.rules_type == 'transport': - item.transport_allowance += line._compute_rule(localdict)[0] + item.transport_allowance += amount - item.total_allowance = total_allowance - item.total_deduction = -total_ded - - if item.salary_level: - total_allowance = 0 - total_deduction = 0 - for line in level_rule_ids: - if line.category_id.rule_type == 'allowance': - try: - total_allowance += line._compute_rule(localdict)[0] - except: - total_allowance += 0 - elif line.category_id.rule_type == 'deduction': - try: - total_deduction += line._compute_rule(localdict)[0] - except: - total_deduction += 0 - - item.total_allowance += total_allowance - item.total_deduction += -total_deduction - - if item.salary_group: - total_allowance = 0 - total_deduction = 0 - for line in group_rule_ids: - if line.category_id.rule_type == 'allowance': - total_allowance += line._compute_rule(localdict)[0] - elif line.category_id.rule_type == 'deduction': - total_deduction += line._compute_rule(localdict)[0] - - item.total_allowance += total_allowance - item.total_deduction += -total_deduction - - item.total_allowance += allow_sum_custom - item.total_allowance += sum_customize_expect - item.total_deduction += -ded_sum_custom - item.total_deduction += -sum_except + item.total_allowance = total_allowance + allow_sum_custom + sum_customize_expect + item.total_deduction = -(total_ded + ded_sum_custom + sum_except) item.total_net = item.total_allowance + item.total_deduction - # filter salary_level,salary_group,salary_degree - @api.onchange('salary_scale') def onchange_salary_scale(self): for item in self: @@ -207,8 +176,6 @@ class HrContractSalaryScale(models.Model): 'salary_group': [('id', 'in', [])], 'salary_degree': [('id', 'in', [])]}} - # filter depend on salary_level - @api.onchange('salary_level') def onchange_salary_level(self): for item in self: @@ -221,7 +188,6 @@ class HrContractSalaryScale(models.Model): return {'domain': {'salary_group': [('id', 'in', [])], 'salary_degree': [('id', 'in', [])]}} - # filter depend on salary_group @api.onchange('salary_group') def onchange_salary_group(self): @@ -232,29 +198,228 @@ class HrContractSalaryScale(models.Model): return {'domain': {'salary_degree': [('id', 'in', degree_ids.ids)]}} else: return {'domain': {'salary_degree': [('id', 'in', [])]}} - - @api.depends('salary_degree') - def _get_amount(self): - for record in self: - record.transport_allowance_temp = record.transport_allowance * record.wage / 100 \ - if record.transport_allowance_type == 'perc' else record.transport_allowance - record.house_allowance_temp = record.house_allowance * record.wage / 100 \ - if record.house_allowance_type == 'perc' else record.house_allowance - record.communication_allowance_temp = record.communication_allowance * record.wage / 100 \ - if record.communication_allowance_type == 'perc' else record.communication_allowance - record.field_allowance_temp = record.field_allowance * record.wage / 100 \ - if record.field_allowance_type == 'perc' else record.field_allowance - record.special_allowance_temp = record.special_allowance * record.wage / 100 \ - if record.special_allowance_type == 'perc' else record.special_allowance - record.other_allowance_temp = record.other_allowance * record.wage / 100 \ - if record.other_allowance_type == 'perc' else record.other_allowance - @api.depends('contractor_type.salary_type') def compute_type(self): - if self.contractor_type.salary_type == 'scale': - self.hide = True - else: - self.hide = False + for rec in self: + if rec.contractor_type.salary_type == 'scale': + rec.hide = True + else: + rec.hide = False +# +# class HrContractSalaryScale(models.Model): +# _inherit = 'hr.contract' +# +# salary_level = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) +# salary_scale = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])], index=True) +# salary_group = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) +# salary_degree = fields.Many2one(comodel_name='hr.payroll.structure', domain=[('id', 'in', [])]) +# hide = fields.Boolean(string='Hide', compute="compute_type") +# required_condition = fields.Boolean(string='Required Condition', compute='compute_move_type') +# total_allowance = fields.Float(string='Total Allowance', compute='compute_function',store=True) +# total_deduction = fields.Float(string='Total Deduction', compute='compute_function',store=True) +# total_net = fields.Float(string='Total Net', compute='compute_function',store=True) +# advantages = fields.One2many('contract.advantage', 'contract_advantage_id', string='Advantages') +# house_allowance_temp = fields.Float(string='House Allowance', compute='compute_function',store=True) +# transport_allowance = fields.Float(string='Transport Allowance', compute='compute_function',store=True) +# +# @api.constrains('advantages', 'salary', 'salary_group') +# def amount_constrains(self): +# for rec in self: +# localdict = dict(employee=rec.employee_id.id, contract=rec.env['hr.contract'].search([ +# ('employee_id', '=', rec.employee_id.id)])) +# if rec.salary_group.gread_max > 0 and rec.salary_group.gread_min > 0: +# if rec.salary > rec.salary_group.gread_max or rec.salary < rec.salary_group.gread_min: +# raise UserError(_('The Basic Salary Is Greater Than Group Gread Max Or less than Gread Min')) +# for item in self.advantages: +# item.to_get_contract_id() +# if item.benefits_discounts._compute_rule(localdict)[0] < item.amount and item.type == 'exception': +# raise UserError(_( +# 'The amount you put is greater than fact value of this Salary rule %s (%s).') % ( +# item.benefits_discounts.name, item.benefits_discounts.code)) +# +# @api.depends('salary_scale.transfer_type') +# def compute_move_type(self): +# self.compute_function() +# if self.salary_scale.transfer_type == 'one_by_one': +# self.required_condition = True +# else: +# self.required_condition = False +# +# @api.depends('salary_scale', 'salary_level', 'salary_group', 'salary_degree','salary','advantages','house_allowance_temp','transport_allowance','total_deduction','salary_insurnce','total_allowance','state') +# def compute_function(self): +# for item in self: +# item.house_allowance_temp = 0 +# item.transport_allowance = 0 +# item.total_net = 0 +# contract = self.env['hr.contract'].search([('employee_id', '=', item.employee_id.id)]) +# localdict = dict(employee=item.employee_id.id, contract=contract) +# current_date = datetime.now().date() +# +# # customize type in advantages +# allowance_customize_items = item.advantages.filtered( +# lambda key: key.type == 'customize' and key.out_rule is False and +# key.benefits_discounts.category_id.rule_type == 'allowance' and +# (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) +# >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) +# +# allow_sum_custom = sum(x.amount for x in allowance_customize_items) +# for x in allowance_customize_items: +# if x.benefits_discounts.rules_type == 'house': +# item.house_allowance_temp += x.amount +# +# if x.benefits_discounts.rules_type == 'transport': +# item.transport_allowance += x.amount +# # allow_custom_ids = [record.benefits_discounts.id for record in allowance_customize_items] +# +# deduction_customize_items = item.advantages.filtered( +# lambda key: key.type == 'customize' and key.out_rule is False and +# key.benefits_discounts.category_id.rule_type == 'deduction' and +# (datetime.strptime(str(key.date_to), "%Y-%m-%d").date() if key.date_to else current_date) +# >= current_date >= datetime.strptime(str(key.date_from), "%Y-%m-%d").date()) +# +# ded_sum_custom = sum(x.amount for x in deduction_customize_items) +# ded_custom_ids = [record.benefits_discounts.id for record in deduction_customize_items] +# +# # exception type in advantages +# exception_items = item.advantages.filtered(lambda key: key.type == 'exception') +# total_rule_result, sum_except, sum_customize_expect = 0.0, 0.0, 0.0 +# +# for x in exception_items: +# rule_result = x.benefits_discounts._compute_rule(localdict)[0] +# if x.date_from >= current_date: +# total_rule_result = rule_result +# elif current_date > x.date_from: +# if x.date_to and current_date <= x.date_to: +# total_rule_result = rule_result - x.amount +# elif x.date_to and current_date >= x.date_to: +# total_rule_result = 0 # rule_result +# elif not x.date_to: +# total_rule_result = rule_result - x.amount +# else: +# if rule_result > x.amount: +# total_rule_result = rule_result - x.amount +# +# if total_rule_result: +# if x.benefits_discounts.category_id.rule_type == 'allowance': +# sum_customize_expect += total_rule_result +# if x.benefits_discounts.rules_type == 'house': +# item.house_allowance_temp += total_rule_result - x.amount +# else: +# sum_except += total_rule_result +# +# if exception_items: +# exception_items = item.advantages.filtered( +# lambda key: (key.date_to.month if key.date_to else current_date.month) +# >= current_date.month >= key.date_from.month) +# # if exception_items: +# # exception_items = item.advantages.filtered( +# # lambda key: (datetime.strptime(key.date_to, +# # "%Y-%m-%d").date().month if key.date_to else current_date.month) +# # >= current_date.month >= datetime.strptime(key.date_from, "%Y-%m-%d").date().month) +# +# except_ids = [record.benefits_discounts.id for record in exception_items] +# +# rule_ids = item.salary_scale.rule_ids.filtered( +# lambda key: key.id not in ded_custom_ids and key.id not in except_ids) +# +# level_rule_ids = item.salary_level.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) +# # key.id not in allow_custom_ids and key.id not in ded_custom_ids and +# +# group_rule_ids = item.salary_group.benefits_discounts_ids.filtered(lambda key: key.id not in except_ids) +# # key.id not in allow_custom_ids and key.id not in ded_custom_ids and +# +# total_allowance = 0 +# total_ded = 0 +# for line in rule_ids: +# if line.category_id.rule_type == 'allowance': +# try: +# total_allowance += line._compute_rule(localdict)[0] +# except: +# total_allowance += 0 +# +# if line.category_id.rule_type == 'deduction': +# try: +# total_ded += line._compute_rule(localdict)[0] +# except: +# total_ded += 0 +# +# +# if line.rules_type == 'house': +# item.house_allowance_temp += line._compute_rule(localdict)[0] +# if line.rules_type == 'transport': +# item.transport_allowance += line._compute_rule(localdict)[0] +# +# item.total_allowance = total_allowance +# item.total_deduction = -total_ded +# +# if item.salary_level: +# total_allowance = 0 +# total_deduction = 0 +# for line in level_rule_ids: +# if line.category_id.rule_type == 'allowance': +# try: +# total_allowance += line._compute_rule(localdict)[0] +# except: +# total_allowance += 0 +# elif line.category_id.rule_type == 'deduction': +# try: +# total_deduction += line._compute_rule(localdict)[0] +# except: +# total_deduction += 0 +# +# item.total_allowance += total_allowance +# item.total_deduction += -total_deduction +# +# if item.salary_group: +# total_allowance = 0 +# total_deduction = 0 +# for line in group_rule_ids: +# if line.category_id.rule_type == 'allowance': +# total_allowance += line._compute_rule(localdict)[0] +# elif line.category_id.rule_type == 'deduction': +# total_deduction += line._compute_rule(localdict)[0] +# +# item.total_allowance += total_allowance +# item.total_deduction += -total_deduction +# +# item.total_allowance += allow_sum_custom +# item.total_allowance += sum_customize_expect +# item.total_deduction += -ded_sum_custom +# item.total_deduction += -sum_except +# item.total_net = item.total_allowance + item.total_deduction +# +# # filter salary_level,salary_group,salary_degree +# +# +# # filter depend on salary_level +# +# +# # filter depend on salary_group +# +# +# +# @api.depends('salary_degree') +# def _get_amount(self): +# for record in self: +# record.transport_allowance_temp = record.transport_allowance * record.wage / 100 \ +# if record.transport_allowance_type == 'perc' else record.transport_allowance +# record.house_allowance_temp = record.house_allowance * record.wage / 100 \ +# if record.house_allowance_type == 'perc' else record.house_allowance +# record.communication_allowance_temp = record.communication_allowance * record.wage / 100 \ +# if record.communication_allowance_type == 'perc' else record.communication_allowance +# record.field_allowance_temp = record.field_allowance * record.wage / 100 \ +# if record.field_allowance_type == 'perc' else record.field_allowance +# record.special_allowance_temp = record.special_allowance * record.wage / 100 \ +# if record.special_allowance_type == 'perc' else record.special_allowance +# record.other_allowance_temp = record.other_allowance * record.wage / 100 \ +# if record.other_allowance_type == 'perc' else record.other_allowance +# +# @api.depends('contractor_type.salary_type') +# def compute_type(self): +# if self.contractor_type.salary_type == 'scale': +# self.hide = True +# else: +# self.hide = False class Advantages(models.Model): diff --git a/exp_payroll_custom/models/hr_salary_rules.py b/exp_payroll_custom/models/hr_salary_rules.py index 6dbafa7..20b8d9f 100644 --- a/exp_payroll_custom/models/hr_salary_rules.py +++ b/exp_payroll_custom/models/hr_salary_rules.py @@ -79,249 +79,347 @@ class HrSalaryRules(models.Model): if rec.category_id.rule_type != 'deduction' and rec.rules_type == 'insurnce': raise UserError(_("The Salary Rule is Not Deduction")) - # Override function compute rule in hr salary rule - def _compute_rule(self, localdict): + + self.ensure_one() payslip = localdict.get('payslip') contract = localdict.get('contract') + current_date = fields.Date.today() + + fix_amount_value = self.amount_fix if hasattr(self, 'amount_fix') else getattr(self, 'fixed_amount', 0.0) + + + def get_related_amount(): + salary_type = getattr(self, 'salary_type', 'fixed') + if salary_type == 'related_levels' and contract.salary_level: + related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_level.id == contract.salary_level.id) + return related.salary if related else 0.0 + elif salary_type == 'related_groups' and contract.salary_group: + related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_group.id == contract.salary_group.id) + return related.salary if related else 0.0 + elif salary_type == 'related_degrees' and contract.salary_degree: + related = self.salary_amount_ids.filtered( + lambda r: r.salary_scale_degree.id == contract.salary_degree.id) + return related.salary if related else 0.0 + return fix_amount_value + if self.amount_select == 'percentage': - total_percent, total = 0, 0 - if self.related_benefits_discounts: - for line in self.related_benefits_discounts: + total_percent = 0.0 + related_rules = getattr(self, 'related_benefits_discounts', []) + + if related_rules: + for line in related_rules: calc_line = line._compute_rule(localdict)[0] + line_in_advantages = False + advantages = contract.advantages if contract else [] - if line.amount_select == 'fix': - if contract.advantages: - for con in contract.advantages: - if line.id == con.benefits_discounts.id: - if payslip: - if con.date_from > payslip.date_from: - total_percent = calc_line - elif con.date_to is not None and str( - con.date_to) >= payslip.date_to or con.date_to is None: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent += total + if advantages: + con = next((adv for adv in advantages if adv.benefits_discounts.id == line.id), None) + if con: + line_in_advantages = True + is_valid_date = False + + if payslip: + if con.date_from > payslip.date_from: + total_percent += calc_line + continue + elif (not con.date_to) or (con.date_to >= payslip.date_to): + is_valid_date = True + else: + if con.date_from <= current_date: + if (not con.date_to) or (con.date_to >= current_date): + is_valid_date = True + + if is_valid_date: + total_to_add = 0.0 + if con.type == 'exception': + if con.amount < calc_line: + total_to_add = calc_line - con.amount else: - if str(con.date_from) < str(datetime.now().date()): - if con.date_to: - if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - >= datetime.now().date().month or not con.date_to: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent += total - else: - total_percent = calc_line - else: - total_percent += calc_line + total_to_add = 0.0 + elif con.type == 'customize': + total_to_add = con.amount - elif line.amount_select == 'percentage': - if contract.advantages: - for con in contract.advantages: - if line.id == con.benefits_discounts.id: - if payslip: - if con.date_from > payslip.date_from: - total_percent = calc_line - elif con.date_to is not None and str( - con.date_to) >= payslip.date_to or con.date_to is None: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent -= calc_line - total_percent += total - else: - if str(con.date_from) < str(datetime.now().date()): - if con.date_to: - if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - >= datetime.now().date().month or not con.date_to: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent -= calc_line - total_percent += total + if line.amount_select == 'percentage': + total_percent -= calc_line + total_percent += total_to_add else: - if con.type != 'exception': - total_percent += calc_line - break - else: - total_percent += calc_line + total_percent += total_to_add + else: + total_percent += calc_line - else: - if contract.advantages: - for con in contract.advantages: - if line.id == con.benefits_discounts.id: - if payslip: - if con.date_from > payslip.date_from: - total_percent = calc_line - elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent = 0 - total_percent += total - else: - if con.date_from < (datetime.now().date()): - if con.date_to: - if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - >= datetime.now().date().month or not con.date_to: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount - total_percent = 0 - total_percent += total - else: - if datetime.strptime(str(con.date_from), - "%Y-%m-%d").date().month >= datetime.now().date().month: - if con.type == 'exception': - if con.amount > calc_line or con.amount == calc_line: - pass - elif con.amount < calc_line: - total = calc_line - con.amount - elif con.type == 'customize': - total = con.amount + calc_line - total_percent = 0 - total_percent += total + if not line_in_advantages: + total_percent += calc_line - else: - if not total_percent: - total_percent = calc_line - else: - total_percent += calc_line if total_percent: - if self.salary_type == 'fixed': - try: - return float(total_percent * self.amount_percentage / 100), \ - float(safe_eval(self.quantity, localdict)), self.amount_percentage - except: - raise UserError( - _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - elif self.salary_type == 'related_levels': - levels_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_level.id == contract.salary_level.id) - if levels_ids: - for l in levels_ids: - try: - return float(l.salary * total_percent / 100), float( - safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_groups': - groups_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_group.id == contract.salary_group.id) - if groups_ids: - for g in groups_ids: - try: - return float(g.salary * total_percent / 100), float( - safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_degrees': - degrees_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - if degrees_ids: - for d in degrees_ids: - try: - return float(d.salary * total_percent / 100), float( - safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % ( - self.name, self.code)) - else: - return 0, 0, 0 - else: try: - return 0, 0, 0 - except: - raise UserError(_('There is no total for rule : %s') % self.name) + qty = float(safe_eval(self.quantity, localdict)) + rate = self.amount_percentage + return float(total_percent * self.amount_percentage / 100), qty, rate + except Exception as e: + raise UserError(_('Error calculating percentage rule %s: %s') % (self.name, e)) + else: + return 0.0, 0.0, 0.0 elif self.amount_select == 'fix': - if self.salary_type == 'fixed': - try: - return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - elif self.salary_type == 'related_levels': - levels_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_level.id == contract.salary_level.id) - if levels_ids: - for l in levels_ids: - try: - return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_groups': - groups_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_group.id == contract.salary_group.id) - if groups_ids: - for g in groups_ids: - try: - return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - else: - return 0, 0, 0 - elif self.salary_type == 'related_degrees': - degrees_ids = self.salary_amount_ids.filtered( - lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - if degrees_ids: - for d in degrees_ids: - try: - return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 - except: - raise UserError( - _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - else: - return 0, 0, 0 - else: - raise UserError(_('Error, Select Salary type to calculate rule')) + try: + qty = float(safe_eval(self.quantity, localdict)) + amount = get_related_amount() + return amount, qty, 100.0 + except Exception as e: + raise UserError(_('Error computing fix rule %s: %s') % (self.name, e)) else: try: safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) - return float(localdict['result']), 'result_qty' in localdict and localdict[ - 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 - except: - raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) + return float(localdict.get('result', 0.0)), \ + localdict.get('result_qty', 1.0), \ + localdict.get('result_rate', 100.0) + except Exception as e: + raise UserError(_('Error computing python rule %s: %s') % (self.name, e)) + # Override function compute rule in hr salary rule + + # def _compute_rule(self, localdict): + # payslip = localdict.get('payslip') + # contract = localdict.get('contract') + # if self.amount_select == 'percentage': + # total_percent, total = 0, 0 + # if self.related_benefits_discounts: + # for line in self.related_benefits_discounts: + # calc_line = line._compute_rule(localdict)[0] + # + # if line.amount_select == 'fix': + # if contract.advantages: + # for con in contract.advantages: + # if line.id == con.benefits_discounts.id: + # if payslip: + # if con.date_from > payslip.date_from: + # total_percent = calc_line + # elif con.date_to is not None and str( + # con.date_to) >= payslip.date_to or con.date_to is None: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent += total + # else: + # if str(con.date_from) < str(datetime.now().date()): + # if con.date_to: + # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + # >= datetime.now().date().month or not con.date_to: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent += total + # else: + # total_percent = calc_line + # else: + # total_percent += calc_line + # + # elif line.amount_select == 'percentage': + # if contract.advantages: + # for con in contract.advantages: + # if line.id == con.benefits_discounts.id: + # if payslip: + # if con.date_from > payslip.date_from: + # total_percent = calc_line + # elif con.date_to is not None and str( + # con.date_to) >= payslip.date_to or con.date_to is None: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent -= calc_line + # total_percent += total + # else: + # if str(con.date_from) < str(datetime.now().date()): + # if con.date_to: + # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + # >= datetime.now().date().month or not con.date_to: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent -= calc_line + # total_percent += total + # else: + # if con.type != 'exception': + # total_percent += calc_line + # break + # else: + # total_percent += calc_line + # + # else: + # if contract.advantages: + # for con in contract.advantages: + # if line.id == con.benefits_discounts.id: + # if payslip: + # if con.date_from > payslip.date_from: + # total_percent = calc_line + # elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent = 0 + # total_percent += total + # else: + # if con.date_from < (datetime.now().date()): + # if con.date_to: + # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + # >= datetime.now().date().month or not con.date_to: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + # total_percent = 0 + # total_percent += total + # else: + # if datetime.strptime(str(con.date_from), + # "%Y-%m-%d").date().month >= datetime.now().date().month: + # if con.type == 'exception': + # if con.amount > calc_line or con.amount == calc_line: + # pass + # elif con.amount < calc_line: + # total = calc_line - con.amount + # elif con.type == 'customize': + # total = con.amount + calc_line + # total_percent = 0 + # total_percent += total + # + # else: + # if not total_percent: + # total_percent = calc_line + # else: + # total_percent += calc_line + # if total_percent: + # if self.salary_type == 'fixed': + # try: + # return float(total_percent * self.amount_percentage / 100), \ + # float(safe_eval(self.quantity, localdict)), self.amount_percentage + # except: + # raise UserError( + # _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # elif self.salary_type == 'related_levels': + # levels_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_level.id == contract.salary_level.id) + # if levels_ids: + # for l in levels_ids: + # try: + # return float(l.salary * total_percent / 100), float( + # safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_groups': + # groups_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_group.id == contract.salary_group.id) + # if groups_ids: + # for g in groups_ids: + # try: + # return float(g.salary * total_percent / 100), float( + # safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_degrees': + # degrees_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + # if degrees_ids: + # for d in degrees_ids: + # try: + # return float(d.salary * total_percent / 100), float( + # safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % ( + # self.name, self.code)) + # else: + # return 0, 0, 0 + # else: + # try: + # return 0, 0, 0 + # except: + # raise UserError(_('There is no total for rule : %s') % self.name) + # + # elif self.amount_select == 'fix': + # if self.salary_type == 'fixed': + # try: + # return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # elif self.salary_type == 'related_levels': + # levels_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_level.id == contract.salary_level.id) + # if levels_ids: + # for l in levels_ids: + # try: + # return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_groups': + # groups_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_group.id == contract.salary_group.id) + # if groups_ids: + # for g in groups_ids: + # try: + # return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # else: + # return 0, 0, 0 + # elif self.salary_type == 'related_degrees': + # degrees_ids = self.salary_amount_ids.filtered( + # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + # if degrees_ids: + # for d in degrees_ids: + # try: + # return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 + # except: + # raise UserError( + # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + # else: + # return 0, 0, 0 + # else: + # raise UserError(_('Error, Select Salary type to calculate rule')) + # + # else: + # try: + # safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) + # return float(localdict['result']), 'result_qty' in localdict and localdict[ + # 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 + # except: + # raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) class SalaryConfig(models.Model): diff --git a/exp_payroll_custom/tests/__init__.py b/exp_payroll_custom/tests/__init__.py new file mode 100644 index 0000000..af07c36 --- /dev/null +++ b/exp_payroll_custom/tests/__init__.py @@ -0,0 +1,5 @@ +from . import test_salary_scale +from . import test_salary_rule_computation +from . import test_employee_promotions +from . import test_payroll_flow +from . import test_employee_reward \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_employee_promotions.py b/exp_payroll_custom/tests/test_employee_promotions.py new file mode 100644 index 0000000..d6faae2 --- /dev/null +++ b/exp_payroll_custom/tests/test_employee_promotions.py @@ -0,0 +1,148 @@ +from odoo.tests.common import TransactionCase, Form +from odoo.exceptions import UserError +from odoo.tests import tagged +from datetime import date + + +@tagged('post_install', '-at_install') +class TestEmployeePromotions(TransactionCase): + + def setUp(cls): + super(TestEmployeePromotions, cls).setUp() + + + cls.structure_scale = cls.env['hr.payroll.structure'].create({ + 'name': 'General Scale', + 'type': 'scale', + 'code': 'SCL_TEST_01' + }) + + cls.level_1 = cls.env['hr.payroll.structure'].create({ + 'name': 'Level 1', + 'type': 'level', + 'salary_scale_id': cls.structure_scale.id, + 'code': 'LVL_TEST_01' + }) + cls.group_A = cls.env['hr.payroll.structure'].create({ + 'name': 'Group A', + 'type': 'group', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_level_id': cls.level_1.id, + 'code': 'GRP_TEST_A' + }) + cls.degree_1 = cls.env['hr.payroll.structure'].create({ + 'name': 'Degree 1', + 'type': 'degree', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_group_id': cls.group_A.id, + 'base_salary': 5000.0, + 'code': 'DEG_TEST_1' + }) + + cls.level_2 = cls.env['hr.payroll.structure'].create({ + 'name': 'Level 2', + 'type': 'level', + 'salary_scale_id': cls.structure_scale.id, + 'code': 'LVL_TEST_02' + }) + cls.group_B = cls.env['hr.payroll.structure'].create({ + 'name': 'Group B', + 'type': 'group', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_level_id': cls.level_2.id, + 'code': 'GRP_TEST_B' + }) + cls.degree_2 = cls.env['hr.payroll.structure'].create({ + 'name': 'Degree 2', + 'type': 'degree', + 'salary_scale_id': cls.structure_scale.id, + 'salary_scale_group_id': cls.group_B.id, + 'base_salary': 7000.0, + 'code': 'DEG_TEST_2' + }) + + cls.employee = cls.env['hr.employee'].create({ + 'name': 'Test Employee', + 'salary_scale': cls.structure_scale.id, + 'salary_level': cls.level_1.id, + 'salary_group': cls.group_A.id, + 'salary_degree': cls.degree_1.id, + }) + + cls.contract = cls.env['hr.contract'].create({ + 'name': 'Test Contract', + 'employee_id': cls.employee.id, + 'wage': 5000.0, + 'state': 'open', + 'salary_level': cls.level_1.id, + 'salary_group': cls.group_A.id, + 'salary_degree': cls.degree_1.id, + }) + + cls.employee.contract_id = cls.contract + + def test_01_promotion_workflow_full_cycle(self): + + promotion_form = Form(self.env['employee.promotions']) + promotion_form.date = date.today() + promotion_form.employee_id = self.employee + + self.assertEqual(promotion_form.old_degree, self.degree_1, "Should auto-fill old degree from employee") + + promotion_form.new_level = self.level_2 + promotion_form.new_group = self.group_B + promotion_form.new_degree = self.degree_2 + + promotion = promotion_form.save() + + promotion.confirm() + self.assertEqual(promotion.state, 'confirm') + + promotion.hr_manager() + self.assertEqual(promotion.state, 'hr_manager') + + promotion.approved() + self.assertEqual(promotion.state, 'approved') + + + self.assertEqual(self.employee.contract_id.salary_degree, self.degree_2, + "Contract degree should be updated to new degree") + self.assertEqual(self.employee.contract_id.salary, 7000.0, + "Contract salary should be updated to new base salary") + + def test_02_redraft_reverts_values(self): + + promotion = self.env['employee.promotions'].create({ + 'date': date.today(), + 'employee_id': self.employee.id, + 'old_degree': self.degree_1.id, + 'old_level_2': self.level_1.id, + 'old_group_2': self.group_A.id, + 'old_degree_2': self.degree_1.id, + 'new_degree': self.degree_2.id, + 'new_level': self.level_2.id, + 'new_group': self.group_B.id, + }) + + promotion.approved() + self.assertEqual(self.employee.contract_id.salary_degree, self.degree_2) + + promotion.re_draft() + + self.assertEqual(promotion.state, 'draft') + self.assertEqual(self.employee.contract_id.salary_degree, self.degree_1, + "Should revert to old degree on re-draft") + + def test_03_unlink_restriction(self): + promotion = self.env['employee.promotions'].create({ + 'date': date.today(), + 'employee_id': self.employee.id, + 'state': 'confirm' + }) + + with self.assertRaises(UserError): + promotion.unlink() + + promotion.state = 'draft' + promotion.unlink() + self.assertFalse(promotion.exists()) \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_employee_reward.py b/exp_payroll_custom/tests/test_employee_reward.py new file mode 100644 index 0000000..44395e8 --- /dev/null +++ b/exp_payroll_custom/tests/test_employee_reward.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo.tests import tagged +from datetime import date + + +@tagged('post_install', '-at_install') +class TestEmployeeReward(TransactionCase): + + def setUp(self): + super(TestEmployeeReward, self).setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + }) + + self.account_debit = self.env['account.account'].create({ + 'name': 'Debit Account', + 'code': '100001', + 'account_type': 'expense', + 'reconcile': True, + }) + + self.account_credit = self.env['account.account'].create({ + 'name': 'Credit Account', + 'code': '200001', + 'account_type': 'liability_payable', + 'reconcile': True, + }) + + self.journal = self.env['account.journal'].create({ + 'name': 'Reward Journal', + 'type': 'general', + 'code': 'REW', + 'default_account_id': self.account_credit.id, + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract for Test', + 'employee_id': self.employee.id, + 'wage': 5000.0, + 'state': 'open', + }) + + def test_01_reward_workflow_and_calculation(self): + + reward = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'Excellent Performance', + 'date': date.today(), + 'reward_type': 'amount', + 'amount': 1000.0, + 'transfer_type': 'accounting', + 'account_id': self.account_debit.id, + 'journal_id': self.journal.id, + }) + + reward_line = self.env['lines.ids.reward'].create({ + 'employee_reward_id': reward.id, + 'employee_id': self.employee.id, + 'percentage': 50.0, + }) + + self.assertEqual(reward_line.amount, 500.0, "Amount calculation is wrong based on percentage") + + + reward.action_submit() + self.assertEqual(reward.state, 'submitted', "State should be submitted") + self.assertEqual(reward_line.reward_state, 'submitted', "Line state should match parent") + + reward.action_hrm() + self.assertEqual(reward.state, 'hrm', "State should be hrm") + + reward.action_done() + self.assertEqual(reward.state, 'done', "State should be done") + + self.assertTrue(reward_line.move_id, "Journal Entry should be created") + self.assertEqual(reward_line.move_id.state, 'draft', "Move should be created in draft") + + move_lines = reward_line.move_id.line_ids + debit_line = move_lines.filtered(lambda l: l.debit > 0) + credit_line = move_lines.filtered(lambda l: l.credit > 0) + + self.assertEqual(debit_line.account_id, self.account_debit, "Debit account mismatch") + self.assertEqual(credit_line.account_id, self.account_credit, "Credit account mismatch") + self.assertEqual(debit_line.debit, 500.0, "Debit amount incorrect") + + def test_02_constraint_reward_once_yearly(self): + + reward_1 = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'First Reward', + 'date': date.today(), + 'reward_type': 'amount', + 'amount': 1000.0, + 'reward_once': True, + 'transfer_type': 'accounting', + 'account_id': self.account_debit.id, + 'journal_id': self.journal.id, + }) + self.env['lines.ids.reward'].create({ + 'employee_reward_id': reward_1.id, + 'employee_id': self.employee.id, + 'percentage': 100.0, + }) + + reward_1.action_submit() + reward_1.action_hrm() + reward_1.action_done() + + reward_2 = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'Second Reward', + 'date': date.today(), + 'reward_type': 'amount', + 'amount': 500.0, + 'reward_once': True, + }) + + with self.assertRaises(UserError): + self.env['lines.ids.reward'].create({ + 'employee_reward_id': reward_2.id, + 'employee_id': self.employee.id, + 'percentage': 100.0, + }) + + def test_03_positive_amount_check(self): + reward = self.env['hr.employee.reward'].create({ + 'allowance_reason': 'Negative Test', + 'date': date.today(), + 'amount': 100.0, + }) + + with self.assertRaises(UserError): + reward.amount = -50.0 + reward.chick_amount_positive() \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_payroll_flow.py b/exp_payroll_custom/tests/test_payroll_flow.py new file mode 100644 index 0000000..cdcc9a1 --- /dev/null +++ b/exp_payroll_custom/tests/test_payroll_flow.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase, Form +from odoo.exceptions import UserError +from odoo.tests import tagged +from datetime import date, datetime, timedelta +from dateutil.relativedelta import relativedelta + + +class TestPayrollAdvanceFlow(TransactionCase): + + def setUp(cls): + super(TestPayrollAdvanceFlow, cls).setUp() + + cls.company = cls.env.company + + cls.account_salary = cls.env['account.account'].create({ + 'name': 'Basic Salary Account', + 'code': '600001', + 'account_type': 'expense', + 'reconcile': True, + }) + + cls.account_payable = cls.env['account.account'].create({ + 'name': 'Salaries Payable', + 'code': '200001', + 'account_type': 'liability_payable', + 'reconcile': True, + }) + + cls.journal = cls.env['account.journal'].create({ + 'name': 'Salary Journal', + 'type': 'general', + 'code': 'SAL', + 'default_account_id': cls.account_payable.id, + }) + + cls.rule_basic = cls.env['hr.salary.rule'].create({ + 'name': 'Basic Salary', + 'sequence': 1, + 'code': 'BASIC', + 'category_id': cls.env.ref('exp_hr_payroll.ALW').id, + 'condition_select': 'none', + 'amount_select': 'code', + 'amount_python_compute': 'result = contract.wage', + 'rule_debit_account_id': cls.account_salary.id, + }) + + cls.rule_net = cls.env['hr.salary.rule'].create({ + 'name': 'Net Salary', + 'sequence': 100, + 'code': 'NET', + 'category_id': cls.env.ref('exp_hr_payroll.DED').id, + 'condition_select': 'none', + 'amount_select': 'code', + 'amount_python_compute': 'result = categories.BASIC + categories.ALW + categories.DED', + 'rule_credit_account_id': cls.account_payable.id, + }) + + cls.structure = cls.env['hr.payroll.structure'].create({ + 'name': 'Standard Structure', + 'type': 'scale', + 'code': 'STRUCT_001', + 'rule_ids': [(4, cls.rule_basic.id), (4, cls.rule_net.id)], + 'transfer_type': 'one_by_one', + }) + + cls.employee = cls.env['hr.employee'].create({ + 'name': 'Test Employee Payroll', + 'first_hiring_date': date.today() - relativedelta(years=1), + 'state': 'open', + }) + + cls.contract = cls.env['hr.contract'].create({ + 'name': 'Contract For Test', + 'employee_id': cls.employee.id, + 'state': 'program_directory', + 'wage': 5000.0, + 'salary_scale': cls.structure.id, + 'journal_id': cls.journal.id, + 'date_start': date.today() - relativedelta(years=1), + }) + def test_01_payslip_compute_and_transfer(self): + + date_from = date.today().replace(day=1) + date_to = date.today() + relativedelta(months=+1, day=1, days=-1) + + payslip = self.env['hr.payslip'].create({ + 'name': 'Test Payslip', + 'employee_id': self.employee.id, + 'date_from': date_from, + 'date_to': date_to, + 'contract_id': self.contract.id, + 'struct_id': self.structure.id + }) + + payslip.compute_sheet() + + self.assertEqual(payslip.state, 'computed', "State should be 'computed' after computing sheet") + + basic_line = payslip.line_ids.filtered(lambda l: l.code == 'BASIC') + self.assertEqual(basic_line.total, 5000.0, "Basic salary should be 5000") + + payslip.compute_totals() + self.assertEqual(payslip.total_allowances, 5000.0, "Total allowances should be calculated correctly") + + payslip.confirm() + self.assertEqual(payslip.state, 'confirmed') + + payslip.transfer() + self.assertEqual(payslip.state, 'transfered') + self.assertTrue(payslip.move_id, "Journal Entry should be created") + self.assertEqual(payslip.move_id.state, 'draft', "Move should be created in draft state initially") + + def test_02_payslip_loans_integration(self): + + payslip = self.env['hr.payslip'].create({ + 'name': 'Loan Payslip', + 'employee_id': self.employee.id, + 'date_from': date.today().replace(day=1), + 'date_to': date.today() + relativedelta(months=+1, day=1, days=-1), + }) + + self.env['payslip.loans'].create({ + 'payslip_loan': payslip.id, + 'name': 'Car Loan', + 'code': 'LOAN01', + 'amount': 500.0, + 'date': date.today(), + 'account_id': self.account_payable.id, + }) + + payslip.compute_totals() + self.assertEqual(payslip.total_loans, 500.0, "Total loans field should calculate sum of loan lines") + + + self.assertEqual(payslip.total_sum, 500.0, "Total sum logic check (depends on allowances setup)") + + def test_03_payslip_run_batch_process(self): + + date_start = date.today().replace(day=1) + date_end = date.today() + relativedelta(months=+1, day=1, days=-1) + + payslip_run = self.env['hr.payslip.run'].create({ + 'name': 'Monthly Run', + 'date_start': date_start, + 'date_end': date_end, + 'salary_scale': self.structure.id, + }) + + payslip_run.check_date_start() + self.assertEqual(payslip_run.date_end, date_end) + + payslip_run.compute_sheet() + + self.assertTrue(payslip_run.slip_ids, "Payslips should be generated for eligible employees") + generated_slip = payslip_run.slip_ids[0] + self.assertEqual(generated_slip.employee_id, self.employee) + self.assertEqual(generated_slip.state, 'computed') + + payslip_run.confirm() + self.assertEqual(generated_slip.state, 'confirmed') + + + payslip_run.transfer() + self.assertEqual(payslip_run.state, 'transfered') + self.assertTrue(payslip_run.move_id or generated_slip.move_id, "Accounting move should be generated") + + def test_04_payslip_withdraw_and_reset(self): + + payslip = self.env['hr.payslip'].create({ + 'name': 'Withdraw Test', + 'employee_id': self.employee.id, + 'date_from': date.today(), + 'date_to': date.today(), + }) + payslip.compute_sheet() + payslip.confirm() + + payslip.withdraw() + + self.assertEqual(payslip.state, 'draft', "State should return to draft after withdraw") + self.assertFalse(payslip.move_id, "Account move should be unlinked/deleted") \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_salary_rule_computation.py b/exp_payroll_custom/tests/test_salary_rule_computation.py new file mode 100644 index 0000000..9455b60 --- /dev/null +++ b/exp_payroll_custom/tests/test_salary_rule_computation.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError +from odoo import fields +from datetime import date, timedelta + + +class TestSalaryRuleComputation(TransactionCase): + + def setUp(self): + super(TestSalaryRuleComputation, self).setUp() + + self.employee = self.env['hr.employee'].create({'name': 'Test Employee'}) + + self.category_basic = self.env['hr.salary.rule.category'].create({ + 'name': 'Basic', 'code': 'BASIC', 'rule_type': 'allowance' + }) + self.category_allowance = self.env['hr.salary.rule.category'].create({ + 'name': 'Allowance', 'code': 'ALW', 'rule_type': 'allowance' + }) + + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Test Structure', 'code': 'TEST_STRUCT', 'type': 'scale', 'parent_id': False + }) + + + self.rule_basic = self.env['hr.salary.rule'].create({ + 'name': 'Basic Salary', 'code': 'BASIC', + 'category_id': self.category_basic.id, + 'amount_select': 'fix', + 'amount_fix': 1000.0, + 'sequence': 1, + 'salary_type': 'fixed', + }) + + self.rule_housing = self.env['hr.salary.rule'].create({ + 'name': 'Housing Allowance', 'code': 'HOUSING', + 'category_id': self.category_allowance.id, + 'amount_select': 'fix', + 'amount_fix': 500.0, + 'sequence': 2, + 'salary_type': 'fixed', + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract Test', + 'employee_id': self.employee.id, + 'salary_scale': self.structure.id, + 'salary': 5000.0, + 'wage': 5000.0, + 'state': 'open', + }) + + def test_01_compute_fixed_rule(self): + localdict = {'contract': self.contract, 'employee': self.employee} + + amount, qty, rate = self.rule_basic._compute_rule(localdict) + self.assertEqual(amount, 1000.0, "Basic salary fixed amount should be 1000") + + def test_02_compute_percentage_with_related_rules(self): + + rule_percent = self.env['hr.salary.rule'].create({ + 'name': 'Social Security', 'code': 'SOC', + 'category_id': self.category_allowance.id, + 'amount_select': 'percentage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + 'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])], + 'salary_type': 'fixed', + }) + + localdict = {'contract': self.contract, 'employee': self.employee} + + amount, qty, rate = rule_percent._compute_rule(localdict) + + self.assertEqual(amount, 150.0, "Percentage calculation failed. Expected 150.0") + + def test_03_percentage_with_exception_advantage(self): + + today = fields.Date.today() + yesterday = today - timedelta(days=1) + self.env['contract.advantage'].create({ + 'contract_advantage_id': self.contract.id, + 'employee_id': self.employee.id, + 'benefits_discounts': self.rule_basic.id, + 'type': 'exception', + + 'amount': 200.0, + 'date_from': yesterday, + 'date_to': today + timedelta(days=30), + }) + self.contract.invalidate_recordset(['advantages']) + rule_percent = self.env['hr.salary.rule'].create({ + 'name': 'Social Security Modified', 'code': 'SOC_MOD', + 'category_id': self.category_allowance.id, + 'amount_select': 'percentage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + 'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])], + 'salary_type': 'fixed', + }) + + localdict = {'contract': self.contract, 'employee': self.employee} + amount, qty, rate = rule_percent._compute_rule(localdict) + + self.assertEqual(amount, 130.0, f"Exception logic failed. Expected 130.0, Got {amount}") + + def test_04_date_range_validation(self): + + old_date = fields.Date.today() - timedelta(days=365) + + self.env['contract.advantage'].create({ + 'contract_advantage_id': self.contract.id, + 'employee_id': self.employee.id, + 'benefits_discounts': self.rule_basic.id, + 'type': 'exception', + 'amount': 200.0, + 'date_from': old_date, + 'date_to': old_date + timedelta(days=30), + }) + + rule_percent = self.env['hr.salary.rule'].create({ + 'name': 'Social Security Date', 'code': 'SOC_DATE', + 'category_id': self.category_allowance.id, + 'amount_select': 'percentage', + 'amount_percentage': 10.0, + 'quantity': '1.0', + 'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])], + 'salary_type': 'fixed', + }) + + localdict = {'contract': self.contract, 'employee': self.employee} + amount, qty, rate = rule_percent._compute_rule(localdict) + + self.assertEqual(amount, 150.0, "Date validation failed. Expired exception should be ignored.") + + def test_05_salary_type_related_levels(self): + + level_1 = self.env['hr.payroll.structure'].create({ + 'name': 'Level 1', 'code': 'LVL1', 'type': 'level', 'parent_id': False + }) + self.contract.salary_level = level_1.id + + + rule_level = self.env['hr.salary.rule'].create({ + 'name': 'Level Rule', 'code': 'LVL_RULE', + 'category_id': self.category_basic.id, + 'amount_select': 'fix', + 'salary_type': 'related_levels', + }) + + self.env['related.salary.amount'].create({ + 'salary_rule_id': rule_level.id, + 'salary_scale_level': level_1.id, + 'salary': 2500.0, + }) + rule_level.invalidate_recordset(['salary_amount_ids']) + self.contract.invalidate_recordset(['salary_level']) + localdict = {'contract': self.contract, 'employee': self.employee} + amount, qty, rate = rule_level._compute_rule(localdict) + + self.assertEqual(amount, 2500.0, "Related Level salary calculation failed.") \ No newline at end of file diff --git a/exp_payroll_custom/tests/test_salary_scale.py b/exp_payroll_custom/tests/test_salary_scale.py new file mode 100644 index 0000000..1bca45a --- /dev/null +++ b/exp_payroll_custom/tests/test_salary_scale.py @@ -0,0 +1,96 @@ +from odoo.tests.common import TransactionCase +from odoo import fields +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError + +class TestHrContractSalaryScale(TransactionCase): + def setUp(self): + super().setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Test Employee', + }) + + allow_cat = self.env['hr.salary.rule.category'].create({ + 'name': 'Allowance', + 'code': 'ALW', + 'rule_type': 'allowance' + }) + ded_cat = self.env['hr.salary.rule.category'].create({ + 'name': 'Deduction', + 'code': 'DED', + 'rule_type': 'deduction' + }) + + self.structure = self.env['hr.payroll.structure'].create({ + 'name': 'Scale Structure', + 'type': 'scale', + 'code': 'SCALE_TEST', + 'parent_id': False, + }) + + self.allow_rule = self.env['hr.salary.rule'].create({ + 'name': 'Allowance Rule', + 'code': 'ALLOW1', + 'category_id': allow_cat.id, + 'amount_select': 'fix', + 'amount_fix': 1000, + 'quantity': '1.0', + 'condition_select': 'none' + }) + + self.ded_rule = self.env['hr.salary.rule'].create({ + 'name': 'Deduction Rule', + 'code': 'DED1', + 'category_id': ded_cat.id, + 'amount_select': 'fix', + 'amount_fix': 200, + 'quantity': '1.0', + 'condition_select': 'none' + }) + + self.structure.rule_ids = [(6, 0, [self.allow_rule.id, self.ded_rule.id])] + + self.contract = self.env['hr.contract'].create({ + 'name': 'Test Contract', + 'employee_id': self.employee.id, + 'salary_scale': self.structure.id, + 'salary': 5000, + }) + + def test_compute_function_basic(self): + + self.contract.compute_function() + + + self.assertEqual(self.contract.total_allowance, 1000.0) + def test_salary_group_constraint(self): + group = self.env['hr.payroll.structure'].create({ + 'name': 'Group A', + 'gread_min': 3000, + 'gread_max': 6000, + 'code': 'SCALE_TEST' + + }) + + self.contract.salary_group = group.id + self.contract.salary = 5000 + + def test_compute_move_type_one_by_one(self): + self.structure.transfer_type = 'one_by_one' + self.contract.salary_scale = self.structure.id + + self.contract.compute_move_type() + + self.assertTrue(self.contract.required_condition) + + def test_exception_amount_greater_than_rule(self): + rule = self.allow_rule + + advantage = self.env['contract.advantage'].create({ + 'contract_advantage_id': self.contract.id, + 'benefits_discounts': rule.id, + 'type': 'exception', + 'amount': 2000, + 'date_from': fields.Date.today(), + }) diff --git a/hr_holidays_public/models/hr_government_exit_return_custom.py b/hr_holidays_public/models/hr_government_exit_return_custom.py index 7744253..9412608 100644 --- a/hr_holidays_public/models/hr_government_exit_return_custom.py +++ b/hr_holidays_public/models/hr_government_exit_return_custom.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- from odoo import models, fields, api, _ -from odoo.exceptions import ValidationError +from odoo.exceptions import ValidationError,UserError class HRHolidays(models.Model): diff --git a/hr_holidays_public/models/return_from_leave.py b/hr_holidays_public/models/return_from_leave.py index 3f57870..27d41ef 100644 --- a/hr_holidays_public/models/return_from_leave.py +++ b/hr_holidays_public/models/return_from_leave.py @@ -6,7 +6,7 @@ import calendar from dateutil.relativedelta import relativedelta from odoo.tools.translate import _ from odoo import models, fields, api -from odoo.exceptions import UserError +from odoo.exceptions import UserError ,ValidationError class ReturnFromLeave(models.Model): @@ -73,7 +73,7 @@ class ReturnFromLeave(models.Model): def _chick_leave_type(self): for rec in self: if rec.leave_request_id.holiday_status_id.leave_type == 'annual' and rec.decision == 'other': - raise exceptions.ValidationError(_("Sorry Cannot be Create an Annual Leave from the same annual Leave")) + raise ValidationError(_("Sorry Cannot be Create an Annual Leave from the same annual Leave")) @api.depends('leave_request_id') def _compute_dates_of_leave(self): @@ -132,7 +132,7 @@ class ReturnFromLeave(models.Model): else: request.diff_days = len(list(set([xd for xd in exceeded_dates if xd not in event_dates and xd not in wkns_dates]))) else: - raise exceptions.ValidationError(_("Sorry this leave ends by %s.\n" + raise ValidationError(_("Sorry this leave ends by %s.\n" "If you plan for an early return kindly apply for leave " "cancellation.") % request.leave_request_id.date_to) else: @@ -155,7 +155,7 @@ class ReturnFromLeave(models.Model): self.settling_leave_id.draft_state() self.settling_leave_id.unlink() else: - raise exceptions.ValidationError(_("Sorry The link leave cannot be deleted %s After approved") + raise ValidationError(_("Sorry The link leave cannot be deleted %s After approved") % self.settling_leave_id.holiday_status_id.name) self.state = 'draft' self.leave_request_id.return_from_leave = False @@ -166,14 +166,14 @@ class ReturnFromLeave(models.Model): request_id = rec.leave_request_id if rec.decision == 'law': # create unpaid leave if not request_id.holiday_status_id.unpaid_holiday_id: - raise exceptions.ValidationError(_("Sorry no unpaid leave is defined for %s leave kindly set one") + raise ValidationError(_("Sorry no unpaid leave is defined for %s leave kindly set one") % request_id.holiday_status_id.name) status_id = request_id.holiday_status_id.unpaid_holiday_id.id elif rec.decision == 'deduct': # Deduct from leave balance status_id = request_id.holiday_status_id.id elif rec.decision == 'other': # create annual leave if not request_id.holiday_status_id.annual_holiday_id: - raise exceptions.ValidationError(_("Sorry no annual leave is defined for %s leave kindly set one") + raise ValidationError(_("Sorry no annual leave is defined for %s leave kindly set one") % request_id.holiday_status_id.name) status_id = request_id.holiday_status_id.annual_holiday_id.id @@ -183,7 +183,7 @@ class ReturnFromLeave(models.Model): ('check_allocation_view', '=', 'balance') ], order='id desc', limit=1).remaining_leaves or 0.0 if balance < rec.diff_days: - raise exceptions.ValidationError( + raise ValidationError( _("Sorry your %s leave balance it is not enough to deduct from it, The balance is %s.") % (request_id.holiday_status_id.name, round(balance, 2))) @@ -225,7 +225,7 @@ class ReturnFromLeave(models.Model): if self.decision == 'deduct': self.settling_leave_id.financial_manager() elif self.leave_request_id.state != 'validate1': - raise exceptions.ValidationError( + raise ValidationError( _("Sorry %s leave is not approved yet. kindly approve it first") % ( self.leave_request_id.display_name)) self.leave_request_id.remove_delegated_access() @@ -240,7 +240,7 @@ class ReturnFromLeave(models.Model): leave.settling_leave_id.draft_state() leave.settling_leave_id.unlink() else: - raise exceptions.ValidationError(_("Sorry The link leave cannot be deleted %s After approved") + raise ValidationError(_("Sorry The link leave cannot be deleted %s After approved") % leave.settling_leave_id.holiday_status_id.name) self.state = 'refuse' diff --git a/hr_holidays_public/tests/__init__.py b/hr_holidays_public/tests/__init__.py new file mode 100644 index 0000000..f8a724b --- /dev/null +++ b/hr_holidays_public/tests/__init__.py @@ -0,0 +1 @@ +from . import test_hr_holidays_custom \ No newline at end of file diff --git a/hr_holidays_public/tests/test_hr_holidays_custom.py b/hr_holidays_public/tests/test_hr_holidays_custom.py new file mode 100644 index 0000000..ac1df70 --- /dev/null +++ b/hr_holidays_public/tests/test_hr_holidays_custom.py @@ -0,0 +1,179 @@ +# -*- coding: utf-8 -*- +from odoo.tests.common import TransactionCase +from odoo.exceptions import UserError, ValidationError +from odoo.tests import tagged +from datetime import date, timedelta, datetime + + +@tagged('post_install', '-at_install') +class TestHRHolidaysCustom(TransactionCase): + + def setUp(self): + super(TestHRHolidaysCustom, self).setUp() + + self.employee = self.env['hr.employee'].create({ + 'name': 'Employee Test', + 'gender': 'male', + 'first_hiring_date': date.today() - timedelta(days=365 * 2), + 'state': 'open', + }) + + self.replacement_employee = self.env['hr.employee'].create({ + 'name': 'Replacement Employee', + 'gender': 'male', + 'first_hiring_date': date.today() - timedelta(days=365), + 'state': 'open', + }) + + self.contract = self.env['hr.contract'].create({ + 'name': 'Contract Test', + 'employee_id': self.employee.id, + 'wage': 5000, + 'state': 'open', + 'date_start': date.today() - timedelta(days=365 * 2), + 'emp_type': 'saudi', # ضروري لفلتر السكيجوال + }) + self.employee.contract_id = self.contract + + self.replacement_contract = self.env['hr.contract'].create({ + 'name': 'Replacement Contract', + 'employee_id': self.replacement_employee.id, + 'wage': 4000, + 'state': 'open', + 'date_start': date.today() - timedelta(days=365), + 'emp_type': 'saudi', + }) + self.replacement_employee.contract_id = self.replacement_contract + + self.holiday_status = self.env['hr.holidays.status'].create({ + 'name': 'Annual Leave Test', + 'leave_type': 'annual', + 'limit': False, + 'number_of_days': 30, + 'active': True, + 'alternative_days': 2, + 'alternative_chick': False, + }) + + self.env['hr.holidays'].create({ + 'name': 'Balance Record', + 'holiday_status_id': self.holiday_status.id, + 'employee_id': self.employee.id, + 'type': 'add', + 'check_allocation_view': 'balance', + 'remaining_leaves': 30.0, + 'state': 'validate', + }) + + self.env['hr.holidays'].create({ + 'name': 'Replacement Balance', + 'holiday_status_id': self.holiday_status.id, + 'employee_id': self.replacement_employee.id, + 'type': 'add', + 'check_allocation_view': 'balance', + 'remaining_leaves': 10.0, + 'state': 'validate', + }) + + def test_01_leave_workflow_and_ticket_creation(self): + leave_request = self.env['hr.holidays'].create({ + 'name': 'Leave Request with Ticket', + 'employee_id': self.employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': datetime.now().strftime('%Y-%m-%d 08:00:00'), + 'date_to': (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d 17:00:00'), + 'number_of_days_temp': 5, + 'type': 'remove', + 'check_allocation_view': 'allocation', + 'issuing_ticket': 'yes', + 'ticket_cash_request_for': 'employee', + }) + + leave_request.confirm() + leave_request.hr_manager() + leave_request.approved() + leave_request.financial_manager() + + self.assertTrue(leave_request.request_done) + ticket = self.env['hr.ticket.request'].search([('leave_request_id', '=', leave_request.id)]) + self.assertTrue(ticket) + + def test_02_replacement_employee_constraint(self): + today = datetime.now() + self.env['hr.holidays'].create({ + 'name': 'Replacement Employee Leave', + 'employee_id': self.replacement_employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': today.strftime('%Y-%m-%d 08:00:00'), + 'date_to': (today + timedelta(days=2)).strftime('%Y-%m-%d 17:00:00'), + 'type': 'remove', + 'state': 'validate1', + }) + + with self.assertRaises(UserError): + self.env['hr.holidays'].create({ + 'name': 'Main Employee Leave', + 'employee_id': self.employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': today.strftime('%Y-%m-%d 08:00:00'), + 'date_to': (today + timedelta(days=2)).strftime('%Y-%m-%d 17:00:00'), + 'type': 'remove', + 'replace_by': self.replacement_employee.id, + }) + + def test_03_check_balance_limit(self): + with self.assertRaises(UserError): + leave = self.env['hr.holidays'].create({ + 'name': 'Exceed Balance Leave', + 'employee_id': self.employee.id, + 'holiday_status_id': self.holiday_status.id, + 'date_from': datetime.now().strftime('%Y-%m-%d 08:00:00'), + 'date_to': (datetime.now() + timedelta(days=40)).strftime('%Y-%m-%d 17:00:00'), + 'number_of_days_temp': 41, + 'type': 'remove', + }) + leave._check_number_of_days() + + def test_04_scheduler_queue_allocation(self): + + monthly_leave_type = self.env['hr.holidays.status'].create({ + 'name': 'Monthly Leave', + 'leave_type': 'annual', + 'balance_type': 'monthly', + 'leave_annual_type': 'open_balance', + 'company_id': self.env.company.id, + 'emp_type': 'all', + 'alternative_days': 2, + 'alternative_chick': False, + 'number_of_days': 0, + 'duration_ids': [(0, 0, { + 'name': 'Level 1', + 'date_from': 0, + 'date_to': 10, + 'duration': 30 + })] + }) + + + for i in range(2): + self.env['hr.holidays'].create({ + 'name': f'Dummy Allocation {i}', + 'holiday_status_id': monthly_leave_type.id, + 'employee_id': self.replacement_employee.id, + 'type': 'add', + 'check_allocation_view': 'balance', + 'number_of_days_temp': 0, + 'state': 'confirm' + }) + + self.env['hr.holidays'].process_holidays_scheduler_queue() + + allocation = self.env['hr.holidays'].search([ + ('employee_id', '=', self.employee.id), + ('holiday_status_id', '=', monthly_leave_type.id), + ('check_allocation_view', '=', 'balance'), + ('type', '=', 'add') + ]) + + self.assertTrue(allocation, "Scheduler should create allocation record") + self.assertGreater(allocation.remaining_leaves, 0.0) \ No newline at end of file From 87bab36cb2adbfbd9002f9ca42e58cd0dc2f2da9 Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Wed, 24 Dec 2025 01:32:32 +0300 Subject: [PATCH 2/3] make unit test hr base --- .../models/employee_overtime_request.py | 4 +- employee_requests/tests/__init__.py | 6 +- exp_payroll_custom/models/hr_salary_rules.py | 546 +++++++----------- .../tests/test_salary_rule_computation.py | 8 +- 4 files changed, 233 insertions(+), 331 deletions(-) diff --git a/employee_requests/models/employee_overtime_request.py b/employee_requests/models/employee_overtime_request.py index 9204fe6..d0301d0 100644 --- a/employee_requests/models/employee_overtime_request.py +++ b/employee_requests/models/employee_overtime_request.py @@ -227,7 +227,7 @@ class employee_overtime_request(models.Model): 'debit': record.price_hour, 'account_id': account_debit_id.id, 'partner_id': record.employee_id.user_id.partner_id.id, - # 'analytic_account_id': analytic_account_id.id, + 'analytic_account_id': analytic_account_id.id, } credit_line_vals = { 'name': record.employee_id.name, @@ -242,6 +242,8 @@ class employee_overtime_request(models.Model): 'date': item.request_date, 'ref': record.employee_id.name, 'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)], + 'res_model': 'employee.overtime.request', + 'res_id': self.id }) record.account_id = account_debit_id.id diff --git a/employee_requests/tests/__init__.py b/employee_requests/tests/__init__.py index 80a14d2..7430a82 100644 --- a/employee_requests/tests/__init__.py +++ b/employee_requests/tests/__init__.py @@ -1,4 +1,4 @@ -# from . import test_overtime_process -# from . import test_employee_department_jobs -# from . import test_hr_clearance_form +from . import test_overtime_process +from . import test_employee_department_jobs +from . import test_hr_clearance_form from . import test_hr_personal_permission \ No newline at end of file diff --git a/exp_payroll_custom/models/hr_salary_rules.py b/exp_payroll_custom/models/hr_salary_rules.py index 20b8d9f..e76d280 100644 --- a/exp_payroll_custom/models/hr_salary_rules.py +++ b/exp_payroll_custom/models/hr_salary_rules.py @@ -80,346 +80,246 @@ class HrSalaryRules(models.Model): raise UserError(_("The Salary Rule is Not Deduction")) def _compute_rule(self, localdict): - - self.ensure_one() payslip = localdict.get('payslip') contract = localdict.get('contract') - current_date = fields.Date.today() - - fix_amount_value = self.amount_fix if hasattr(self, 'amount_fix') else getattr(self, 'fixed_amount', 0.0) - - - def get_related_amount(): - salary_type = getattr(self, 'salary_type', 'fixed') - if salary_type == 'related_levels' and contract.salary_level: - related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_level.id == contract.salary_level.id) - return related.salary if related else 0.0 - elif salary_type == 'related_groups' and contract.salary_group: - related = self.salary_amount_ids.filtered(lambda r: r.salary_scale_group.id == contract.salary_group.id) - return related.salary if related else 0.0 - elif salary_type == 'related_degrees' and contract.salary_degree: - related = self.salary_amount_ids.filtered( - lambda r: r.salary_scale_degree.id == contract.salary_degree.id) - return related.salary if related else 0.0 - return fix_amount_value - if self.amount_select == 'percentage': - total_percent = 0.0 - related_rules = getattr(self, 'related_benefits_discounts', []) - - if related_rules: - for line in related_rules: + total_percent, total = 0, 0 + if self.related_benefits_discounts: + for line in self.related_benefits_discounts: calc_line = line._compute_rule(localdict)[0] - line_in_advantages = False - advantages = contract.advantages if contract else [] - if advantages: - con = next((adv for adv in advantages if adv.benefits_discounts.id == line.id), None) - if con: - line_in_advantages = True - is_valid_date = False - - if payslip: - if con.date_from > payslip.date_from: - total_percent += calc_line - continue - elif (not con.date_to) or (con.date_to >= payslip.date_to): - is_valid_date = True - else: - if con.date_from <= current_date: - if (not con.date_to) or (con.date_to >= current_date): - is_valid_date = True - - if is_valid_date: - total_to_add = 0.0 - if con.type == 'exception': - if con.amount < calc_line: - total_to_add = calc_line - con.amount + if line.amount_select == 'fix': + if contract.advantages: + for con in contract.advantages: + if line.id == con.benefits_discounts.id: + if payslip: + if con.date_from > payslip.date_from: + total_percent = calc_line + elif con.date_to is not None and str( + con.date_to) >= payslip.date_to or con.date_to is None: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent += total else: - total_to_add = 0.0 - elif con.type == 'customize': - total_to_add = con.amount - - if line.amount_select == 'percentage': - total_percent -= calc_line - total_percent += total_to_add + if str(con.date_from) < str(datetime.now().date()): + if con.date_to: + if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + >= datetime.now().date().month or not con.date_to: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent += total else: - total_percent += total_to_add - else: - total_percent += calc_line + total_percent = calc_line + else: + total_percent += calc_line - if not line_in_advantages: - total_percent += calc_line + elif line.amount_select == 'percentage': + if contract.advantages: + for con in contract.advantages: + if line.id == con.benefits_discounts.id: + if payslip: + if con.date_from > payslip.date_from: + total_percent = calc_line + elif con.date_to is not None and str( + con.date_to) >= payslip.date_to or con.date_to is None: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent -= calc_line + total_percent += total + else: + if str(con.date_from) < str(datetime.now().date()): + if con.date_to: + if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + >= datetime.now().date().month or not con.date_to: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent -= calc_line + total_percent += total + else: + if con.type != 'exception': + total_percent += calc_line + break + else: + total_percent += calc_line + else: + if contract.advantages: + for con in contract.advantages: + if line.id == con.benefits_discounts.id: + if payslip: + if con.date_from > payslip.date_from: + total_percent = calc_line + elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent = 0 + total_percent += total + else: + if con.date_from < (datetime.now().date()): + if con.date_to: + if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ + >= datetime.now().date().month or not con.date_to: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + total_percent = 0 + total_percent += total + else: + if datetime.strptime(str(con.date_from), + "%Y-%m-%d").date().month >= datetime.now().date().month: + if con.type == 'exception': + if con.amount > calc_line or con.amount == calc_line: + pass + elif con.amount < calc_line: + total = calc_line - con.amount + elif con.type == 'customize': + total = con.amount + calc_line + total_percent = 0 + total_percent += total + + else: + if not total_percent: + total_percent = calc_line + else: + total_percent += calc_line if total_percent: - try: - qty = float(safe_eval(self.quantity, localdict)) - rate = self.amount_percentage - return float(total_percent * self.amount_percentage / 100), qty, rate - except Exception as e: - raise UserError(_('Error calculating percentage rule %s: %s') % (self.name, e)) + if self.salary_type == 'fixed': + try: + return float(total_percent * self.amount_percentage / 100), \ + float(safe_eval(self.quantity, localdict)), self.amount_percentage + except: + raise UserError( + _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + elif self.salary_type == 'related_levels': + levels_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_level.id == contract.salary_level.id) + if levels_ids: + for l in levels_ids: + try: + return float(l.salary * total_percent / 100), float( + safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_groups': + groups_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_group.id == contract.salary_group.id) + if groups_ids: + for g in groups_ids: + try: + return float(g.salary * total_percent / 100), float( + safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_degrees': + degrees_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + if degrees_ids: + for d in degrees_ids: + try: + return float(d.salary * total_percent / 100), float( + safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % ( + self.name, self.code)) + else: + return 0, 0, 0 else: - return 0.0, 0.0, 0.0 + try: + return 0, 0, 0 + except: + raise UserError(_('There is no total for rule : %s') % self.name) elif self.amount_select == 'fix': - try: - qty = float(safe_eval(self.quantity, localdict)) - amount = get_related_amount() - return amount, qty, 100.0 - except Exception as e: - raise UserError(_('Error computing fix rule %s: %s') % (self.name, e)) + if self.salary_type == 'fixed': + try: + return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + elif self.salary_type == 'related_levels': + levels_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_level.id == contract.salary_level.id) + if levels_ids: + for l in levels_ids: + try: + return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_groups': + groups_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_group.id == contract.salary_group.id) + if groups_ids: + for g in groups_ids: + try: + return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + else: + return 0, 0, 0 + elif self.salary_type == 'related_degrees': + degrees_ids = self.salary_amount_ids.filtered( + lambda item: item.salary_scale_degree.id == contract.salary_degree.id) + if degrees_ids: + for d in degrees_ids: + try: + return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 + except: + raise UserError( + _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) + else: + return 0, 0, 0 + else: + raise UserError(_('Error, Select Salary type to calculate rule')) else: try: safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) - return float(localdict.get('result', 0.0)), \ - localdict.get('result_qty', 1.0), \ - localdict.get('result_rate', 100.0) - except Exception as e: - raise UserError(_('Error computing python rule %s: %s') % (self.name, e)) - # Override function compute rule in hr salary rule - - # def _compute_rule(self, localdict): - # payslip = localdict.get('payslip') - # contract = localdict.get('contract') - # if self.amount_select == 'percentage': - # total_percent, total = 0, 0 - # if self.related_benefits_discounts: - # for line in self.related_benefits_discounts: - # calc_line = line._compute_rule(localdict)[0] - # - # if line.amount_select == 'fix': - # if contract.advantages: - # for con in contract.advantages: - # if line.id == con.benefits_discounts.id: - # if payslip: - # if con.date_from > payslip.date_from: - # total_percent = calc_line - # elif con.date_to is not None and str( - # con.date_to) >= payslip.date_to or con.date_to is None: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent += total - # else: - # if str(con.date_from) < str(datetime.now().date()): - # if con.date_to: - # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - # >= datetime.now().date().month or not con.date_to: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent += total - # else: - # total_percent = calc_line - # else: - # total_percent += calc_line - # - # elif line.amount_select == 'percentage': - # if contract.advantages: - # for con in contract.advantages: - # if line.id == con.benefits_discounts.id: - # if payslip: - # if con.date_from > payslip.date_from: - # total_percent = calc_line - # elif con.date_to is not None and str( - # con.date_to) >= payslip.date_to or con.date_to is None: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent -= calc_line - # total_percent += total - # else: - # if str(con.date_from) < str(datetime.now().date()): - # if con.date_to: - # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - # >= datetime.now().date().month or not con.date_to: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent -= calc_line - # total_percent += total - # else: - # if con.type != 'exception': - # total_percent += calc_line - # break - # else: - # total_percent += calc_line - # - # else: - # if contract.advantages: - # for con in contract.advantages: - # if line.id == con.benefits_discounts.id: - # if payslip: - # if con.date_from > payslip.date_from: - # total_percent = calc_line - # elif con.date_to is not None and con.date_to >= payslip.date_to or con.date_to is None: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent = 0 - # total_percent += total - # else: - # if con.date_from < (datetime.now().date()): - # if con.date_to: - # if datetime.strptime(str(con.date_to), "%Y-%m-%d").date().month \ - # >= datetime.now().date().month or not con.date_to: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount - # total_percent = 0 - # total_percent += total - # else: - # if datetime.strptime(str(con.date_from), - # "%Y-%m-%d").date().month >= datetime.now().date().month: - # if con.type == 'exception': - # if con.amount > calc_line or con.amount == calc_line: - # pass - # elif con.amount < calc_line: - # total = calc_line - con.amount - # elif con.type == 'customize': - # total = con.amount + calc_line - # total_percent = 0 - # total_percent += total - # - # else: - # if not total_percent: - # total_percent = calc_line - # else: - # total_percent += calc_line - # if total_percent: - # if self.salary_type == 'fixed': - # try: - # return float(total_percent * self.amount_percentage / 100), \ - # float(safe_eval(self.quantity, localdict)), self.amount_percentage - # except: - # raise UserError( - # _('Wrong percentage base or quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # elif self.salary_type == 'related_levels': - # levels_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_level.id == contract.salary_level.id) - # if levels_ids: - # for l in levels_ids: - # try: - # return float(l.salary * total_percent / 100), float( - # safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_groups': - # groups_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_group.id == contract.salary_group.id) - # if groups_ids: - # for g in groups_ids: - # try: - # return float(g.salary * total_percent / 100), float( - # safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_degrees': - # degrees_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - # if degrees_ids: - # for d in degrees_ids: - # try: - # return float(d.salary * total_percent / 100), float( - # safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % ( - # self.name, self.code)) - # else: - # return 0, 0, 0 - # else: - # try: - # return 0, 0, 0 - # except: - # raise UserError(_('There is no total for rule : %s') % self.name) - # - # elif self.amount_select == 'fix': - # if self.salary_type == 'fixed': - # try: - # return self.fixed_amount, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError(_('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # elif self.salary_type == 'related_levels': - # levels_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_level.id == contract.salary_level.id) - # if levels_ids: - # for l in levels_ids: - # try: - # return l.salary, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_groups': - # groups_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_group.id == contract.salary_group.id) - # if groups_ids: - # for g in groups_ids: - # try: - # return g.salary, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # else: - # return 0, 0, 0 - # elif self.salary_type == 'related_degrees': - # degrees_ids = self.salary_amount_ids.filtered( - # lambda item: item.salary_scale_degree.id == contract.salary_degree.id) - # if degrees_ids: - # for d in degrees_ids: - # try: - # return d.salary, float(safe_eval(self.quantity, localdict)), 100.0 - # except: - # raise UserError( - # _('Wrong quantity defined for salary rule %s (%s).') % (self.name, self.code)) - # else: - # return 0, 0, 0 - # else: - # raise UserError(_('Error, Select Salary type to calculate rule')) - # - # else: - # try: - # safe_eval(self.amount_python_compute, localdict, mode='exec', nocopy=True) - # return float(localdict['result']), 'result_qty' in localdict and localdict[ - # 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 - # except: - # raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) + return float(localdict['result']), 'result_qty' in localdict and localdict[ + 'result_qty'] or 1.0, 'result_rate' in localdict and localdict['result_rate'] or 100.0 + except: + raise UserError(_('Wrong python code defined for salary rule %s (%s).') % (self.name, self.code)) class SalaryConfig(models.Model): diff --git a/exp_payroll_custom/tests/test_salary_rule_computation.py b/exp_payroll_custom/tests/test_salary_rule_computation.py index 9455b60..5632d52 100644 --- a/exp_payroll_custom/tests/test_salary_rule_computation.py +++ b/exp_payroll_custom/tests/test_salary_rule_computation.py @@ -28,7 +28,7 @@ class TestSalaryRuleComputation(TransactionCase): 'name': 'Basic Salary', 'code': 'BASIC', 'category_id': self.category_basic.id, 'amount_select': 'fix', - 'amount_fix': 1000.0, + 'fixed_amount': 1000, 'sequence': 1, 'salary_type': 'fixed', }) @@ -37,7 +37,7 @@ class TestSalaryRuleComputation(TransactionCase): 'name': 'Housing Allowance', 'code': 'HOUSING', 'category_id': self.category_allowance.id, 'amount_select': 'fix', - 'amount_fix': 500.0, + 'fixed_amount': 500, 'sequence': 2, 'salary_type': 'fixed', }) @@ -85,7 +85,7 @@ class TestSalaryRuleComputation(TransactionCase): 'benefits_discounts': self.rule_basic.id, 'type': 'exception', - 'amount': 200.0, + 'amount': 200, 'date_from': yesterday, 'date_to': today + timedelta(days=30), }) @@ -114,7 +114,7 @@ class TestSalaryRuleComputation(TransactionCase): 'employee_id': self.employee.id, 'benefits_discounts': self.rule_basic.id, 'type': 'exception', - 'amount': 200.0, + 'amount': 200, 'date_from': old_date, 'date_to': old_date + timedelta(days=30), }) From 389272ac58727a17867a1f1da951f493a2c65ce2 Mon Sep 17 00:00:00 2001 From: mohammed-alkhazrji Date: Fri, 26 Dec 2025 02:41:38 +0300 Subject: [PATCH 3/3] add moduls --- exp_hr_appraisal_kpi/__init__.py | 2 + exp_hr_appraisal_kpi/__manifest__.py | 39 + exp_hr_appraisal_kpi/i18n/ar_001.po | 1097 +++++++++++++++++ exp_hr_appraisal_kpi/models/__init__.py | 9 + .../models/appraisal_percentage.py | 23 + .../models/employee_apprisal.py | 162 +++ .../models/employee_performance_evaluation.py | 149 +++ exp_hr_appraisal_kpi/models/kpi_item.py | 150 +++ exp_hr_appraisal_kpi/models/kpi_period.py | 62 + exp_hr_appraisal_kpi/models/kpi_skill.py | 61 + exp_hr_appraisal_kpi/models/skill_apprisal.py | 141 +++ .../models/years_employee_goals.py | 220 ++++ exp_hr_appraisal_kpi/security/group.xml | 95 ++ .../security/ir.model.access.csv | 71 ++ .../static/description/icon.png | Bin 0 -> 32929 bytes .../static/src/css/website_rtl.css | 22 + .../views/appraisal_percentage.xml | 69 ++ .../views/employee_apprisal.xml | 223 ++++ .../views/employee_performance_evaluation.xml | 86 ++ exp_hr_appraisal_kpi/views/kpi_category.xml | 56 + exp_hr_appraisal_kpi/views/kpi_item.xml | 51 + exp_hr_appraisal_kpi/views/kpi_period.xml | 44 + exp_hr_appraisal_kpi/views/kpi_skills.xml | 109 ++ .../views/skill_appraisal.xml | 87 ++ .../views/years_employee_goals.xml | 86 ++ hr_holidays_community/models/hr.py | 4 +- hr_linkedin_recruitment/README.rst | 56 + hr_linkedin_recruitment/__init__.py | 23 + hr_linkedin_recruitment/__manifest__.py | 52 + .../controller/__init__.py | 22 + .../controller/hr_linkedin_recruitment.py | 176 +++ .../data/auth_linkedin_data.xml | 20 + hr_linkedin_recruitment/doc/RELEASE_NOTES.md | 6 + hr_linkedin_recruitment/doc/requirment.txt | 8 + hr_linkedin_recruitment/models/__init__.py | 25 + .../models/auth_outh_provider.py | 31 + hr_linkedin_recruitment/models/hr_job.py | 161 +++ .../models/linkedin_comments.py | 34 + .../models/mechanize_op.py | 77 ++ .../models/recruitment_config.py | 49 + .../security/ir.model.access.csv | 2 + .../description/assets/icons/capture (1).png | Bin 0 -> 36623 bytes .../static/description/assets/icons/check.png | Bin 0 -> 3676 bytes .../description/assets/icons/chevron.png | Bin 0 -> 310 bytes .../static/description/assets/icons/cogs.png | Bin 0 -> 1377 bytes .../description/assets/icons/consultation.png | Bin 0 -> 1458 bytes .../description/assets/icons/ecom-black.png | Bin 0 -> 576 bytes .../assets/icons/education-black.png | Bin 0 -> 733 bytes .../description/assets/icons/hotel-black.png | Bin 0 -> 911 bytes .../static/description/assets/icons/img.png | Bin 0 -> 1173 bytes .../description/assets/icons/license.png | Bin 0 -> 1095 bytes .../description/assets/icons/lifebuoy.png | Bin 0 -> 1199 bytes .../assets/icons/manufacturing-black.png | Bin 0 -> 673 bytes .../assets/icons/photo-capture.png | Bin 0 -> 10898 bytes .../description/assets/icons/pos-black.png | Bin 0 -> 878 bytes .../description/assets/icons/puzzle.png | Bin 0 -> 653 bytes .../assets/icons/restaurant-black.png | Bin 0 -> 905 bytes .../assets/icons/service-black.png | Bin 0 -> 839 bytes .../assets/icons/trading-black.png | Bin 0 -> 427 bytes .../description/assets/icons/training.png | Bin 0 -> 627 bytes .../description/assets/icons/update.png | Bin 0 -> 1225 bytes .../static/description/assets/icons/user.png | Bin 0 -> 988 bytes .../description/assets/icons/wrench.png | Bin 0 -> 1205 bytes .../description/assets/misc/Cybrosys R.png | Bin 0 -> 82191 bytes .../description/assets/misc/categories.png | Bin 0 -> 1532 bytes .../description/assets/misc/check-box.png | Bin 0 -> 1118 bytes .../description/assets/misc/compass.png | Bin 0 -> 1931 bytes .../description/assets/misc/corporate.png | Bin 0 -> 1177 bytes .../assets/misc/customer-support.png | Bin 0 -> 2136 bytes .../description/assets/misc/cybrosys-logo.png | Bin 0 -> 4496 bytes .../static/description/assets/misc/email.svg | 33 + .../description/assets/misc/features.png | Bin 0 -> 589 bytes .../static/description/assets/misc/logo.png | Bin 0 -> 3452 bytes .../static/description/assets/misc/phone.svg | 3 + .../description/assets/misc/pictures.png | Bin 0 -> 1692 bytes .../description/assets/misc/pie-chart.png | Bin 0 -> 2338 bytes .../description/assets/misc/right-arrow.png | Bin 0 -> 967 bytes .../description/assets/misc/star (1) 2.svg | 9 + .../static/description/assets/misc/star.png | Bin 0 -> 1642 bytes .../description/assets/misc/support (1) 1.svg | 9 + .../description/assets/misc/support-email.svg | 6 + .../description/assets/misc/support.png | Bin 0 -> 3892 bytes .../description/assets/misc/tick-mark.svg | 17 + .../description/assets/misc/whatsapp 1.svg | 9 + .../description/assets/misc/whatsapp.png | Bin 0 -> 5097 bytes .../description/assets/misc/whatsapp.svg | 33 + .../static/description/assets/modules/1.gif | Bin 0 -> 1336986 bytes .../static/description/assets/modules/2.jpg | Bin 0 -> 88546 bytes .../static/description/assets/modules/3.jpg | Bin 0 -> 91575 bytes .../static/description/assets/modules/4.jpg | Bin 0 -> 87433 bytes .../static/description/assets/modules/5.jpg | Bin 0 -> 86680 bytes .../static/description/assets/modules/6.jpg | Bin 0 -> 81960 bytes .../description/assets/screenshots/1.png | Bin 0 -> 128178 bytes .../description/assets/screenshots/10.png | Bin 0 -> 120152 bytes .../description/assets/screenshots/11.png | Bin 0 -> 29798 bytes .../description/assets/screenshots/2.png | Bin 0 -> 130746 bytes .../description/assets/screenshots/3.png | Bin 0 -> 141131 bytes .../description/assets/screenshots/4.png | Bin 0 -> 102763 bytes .../description/assets/screenshots/5.png | Bin 0 -> 68270 bytes .../description/assets/screenshots/6.png | Bin 0 -> 89159 bytes .../description/assets/screenshots/7.png | Bin 0 -> 117080 bytes .../description/assets/screenshots/8.png | Bin 0 -> 220716 bytes .../description/assets/screenshots/9.png | Bin 0 -> 116951 bytes .../description/assets/screenshots/hero.gif | Bin 0 -> 337797 bytes .../static/description/banner.jpg | Bin 0 -> 94529 bytes .../static/description/icon.png | Bin 0 -> 9686 bytes .../static/description/index.html | 874 +++++++++++++ .../hr_job_linkedin_likes_comments_views.xml | 64 + .../views/linkedin_comments_views.xml | 21 + hr_linkedin_recruitment/views/oauth_views.xml | 32 + .../views/recruitment_config_settings.xml | 30 + hr_multicompany_employee_number/__init__.py | 2 + .../__manifest__.py | 20 + .../data/ir_sequence_data.xml | 16 + .../models/__init__.py | 2 + .../models/hr_employee.py | 83 ++ .../security/ir.model.access.csv | 2 + hr_training_payment/__init__.py | 2 + hr_training_payment/__manifest__.py | 27 + hr_training_payment/i18n/ar_001.po | 62 + hr_training_payment/models/__init__.py | 4 + .../models/hr_official_mission.py | 135 ++ hr_training_payment/models/mission_type.py | 9 + .../views/hr_official_mission.xml | 23 + hr_training_payment/views/mission_type.xml | 19 + to_attendance_system/__init__.py | 5 + to_attendance_system/__manifest__.py | 29 + to_attendance_system/controllers/__init__.py | 3 + .../controllers/controllers.py | 20 + to_attendance_system/data/scheduler_data.xml | 25 + to_attendance_system/demo/demo.xml | 30 + to_attendance_system/i18n/ar_001.po | 567 +++++++++ to_attendance_system/models/__init__.py | 3 + to_attendance_system/models/helper.py | 106 ++ to_attendance_system/models/models.py | 587 +++++++++ .../security/ir.model.access.csv | 8 + to_attendance_system/views/views.xml | 248 ++++ to_attendance_system/wizard/__init__.py | 1 + .../wizard/attendaces_wizard.py | 53 + .../wizard/attendaces_wizard.xml | 42 + 140 files changed, 7130 insertions(+), 3 deletions(-) create mode 100644 exp_hr_appraisal_kpi/__init__.py create mode 100644 exp_hr_appraisal_kpi/__manifest__.py create mode 100644 exp_hr_appraisal_kpi/i18n/ar_001.po create mode 100644 exp_hr_appraisal_kpi/models/__init__.py create mode 100644 exp_hr_appraisal_kpi/models/appraisal_percentage.py create mode 100644 exp_hr_appraisal_kpi/models/employee_apprisal.py create mode 100644 exp_hr_appraisal_kpi/models/employee_performance_evaluation.py create mode 100644 exp_hr_appraisal_kpi/models/kpi_item.py create mode 100644 exp_hr_appraisal_kpi/models/kpi_period.py create mode 100644 exp_hr_appraisal_kpi/models/kpi_skill.py create mode 100644 exp_hr_appraisal_kpi/models/skill_apprisal.py create mode 100644 exp_hr_appraisal_kpi/models/years_employee_goals.py create mode 100644 exp_hr_appraisal_kpi/security/group.xml create mode 100644 exp_hr_appraisal_kpi/security/ir.model.access.csv create mode 100644 exp_hr_appraisal_kpi/static/description/icon.png create mode 100644 exp_hr_appraisal_kpi/static/src/css/website_rtl.css create mode 100644 exp_hr_appraisal_kpi/views/appraisal_percentage.xml create mode 100644 exp_hr_appraisal_kpi/views/employee_apprisal.xml create mode 100644 exp_hr_appraisal_kpi/views/employee_performance_evaluation.xml create mode 100644 exp_hr_appraisal_kpi/views/kpi_category.xml create mode 100644 exp_hr_appraisal_kpi/views/kpi_item.xml create mode 100644 exp_hr_appraisal_kpi/views/kpi_period.xml create mode 100644 exp_hr_appraisal_kpi/views/kpi_skills.xml create mode 100644 exp_hr_appraisal_kpi/views/skill_appraisal.xml create mode 100644 exp_hr_appraisal_kpi/views/years_employee_goals.xml create mode 100644 hr_linkedin_recruitment/README.rst create mode 100644 hr_linkedin_recruitment/__init__.py create mode 100644 hr_linkedin_recruitment/__manifest__.py create mode 100644 hr_linkedin_recruitment/controller/__init__.py create mode 100644 hr_linkedin_recruitment/controller/hr_linkedin_recruitment.py create mode 100644 hr_linkedin_recruitment/data/auth_linkedin_data.xml create mode 100644 hr_linkedin_recruitment/doc/RELEASE_NOTES.md create mode 100644 hr_linkedin_recruitment/doc/requirment.txt create mode 100644 hr_linkedin_recruitment/models/__init__.py create mode 100644 hr_linkedin_recruitment/models/auth_outh_provider.py create mode 100644 hr_linkedin_recruitment/models/hr_job.py create mode 100644 hr_linkedin_recruitment/models/linkedin_comments.py create mode 100644 hr_linkedin_recruitment/models/mechanize_op.py create mode 100644 hr_linkedin_recruitment/models/recruitment_config.py create mode 100644 hr_linkedin_recruitment/security/ir.model.access.csv create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/capture (1).png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/check.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/chevron.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/cogs.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/consultation.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/ecom-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/education-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/hotel-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/img.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/license.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/lifebuoy.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/manufacturing-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/photo-capture.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/pos-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/puzzle.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/restaurant-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/service-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/trading-black.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/training.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/update.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/user.png create mode 100644 hr_linkedin_recruitment/static/description/assets/icons/wrench.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/Cybrosys R.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/categories.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/check-box.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/compass.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/corporate.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/customer-support.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/cybrosys-logo.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/email.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/features.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/logo.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/phone.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/pictures.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/pie-chart.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/right-arrow.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/star (1) 2.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/star.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/support (1) 1.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/support-email.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/support.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/tick-mark.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/whatsapp 1.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/whatsapp.png create mode 100644 hr_linkedin_recruitment/static/description/assets/misc/whatsapp.svg create mode 100644 hr_linkedin_recruitment/static/description/assets/modules/1.gif create mode 100644 hr_linkedin_recruitment/static/description/assets/modules/2.jpg create mode 100644 hr_linkedin_recruitment/static/description/assets/modules/3.jpg create mode 100644 hr_linkedin_recruitment/static/description/assets/modules/4.jpg create mode 100644 hr_linkedin_recruitment/static/description/assets/modules/5.jpg create mode 100644 hr_linkedin_recruitment/static/description/assets/modules/6.jpg create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/1.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/10.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/11.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/2.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/3.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/4.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/5.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/6.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/7.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/8.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/9.png create mode 100644 hr_linkedin_recruitment/static/description/assets/screenshots/hero.gif create mode 100644 hr_linkedin_recruitment/static/description/banner.jpg create mode 100644 hr_linkedin_recruitment/static/description/icon.png create mode 100644 hr_linkedin_recruitment/static/description/index.html create mode 100644 hr_linkedin_recruitment/views/hr_job_linkedin_likes_comments_views.xml create mode 100644 hr_linkedin_recruitment/views/linkedin_comments_views.xml create mode 100644 hr_linkedin_recruitment/views/oauth_views.xml create mode 100644 hr_linkedin_recruitment/views/recruitment_config_settings.xml create mode 100644 hr_multicompany_employee_number/__init__.py create mode 100644 hr_multicompany_employee_number/__manifest__.py create mode 100644 hr_multicompany_employee_number/data/ir_sequence_data.xml create mode 100644 hr_multicompany_employee_number/models/__init__.py create mode 100644 hr_multicompany_employee_number/models/hr_employee.py create mode 100644 hr_multicompany_employee_number/security/ir.model.access.csv create mode 100644 hr_training_payment/__init__.py create mode 100644 hr_training_payment/__manifest__.py create mode 100644 hr_training_payment/i18n/ar_001.po create mode 100644 hr_training_payment/models/__init__.py create mode 100644 hr_training_payment/models/hr_official_mission.py create mode 100644 hr_training_payment/models/mission_type.py create mode 100644 hr_training_payment/views/hr_official_mission.xml create mode 100644 hr_training_payment/views/mission_type.xml create mode 100644 to_attendance_system/__init__.py create mode 100644 to_attendance_system/__manifest__.py create mode 100644 to_attendance_system/controllers/__init__.py create mode 100644 to_attendance_system/controllers/controllers.py create mode 100644 to_attendance_system/data/scheduler_data.xml create mode 100644 to_attendance_system/demo/demo.xml create mode 100644 to_attendance_system/i18n/ar_001.po create mode 100644 to_attendance_system/models/__init__.py create mode 100644 to_attendance_system/models/helper.py create mode 100644 to_attendance_system/models/models.py create mode 100644 to_attendance_system/security/ir.model.access.csv create mode 100644 to_attendance_system/views/views.xml create mode 100644 to_attendance_system/wizard/__init__.py create mode 100644 to_attendance_system/wizard/attendaces_wizard.py create mode 100644 to_attendance_system/wizard/attendaces_wizard.xml diff --git a/exp_hr_appraisal_kpi/__init__.py b/exp_hr_appraisal_kpi/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/exp_hr_appraisal_kpi/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/exp_hr_appraisal_kpi/__manifest__.py b/exp_hr_appraisal_kpi/__manifest__.py new file mode 100644 index 0000000..f275bfd --- /dev/null +++ b/exp_hr_appraisal_kpi/__manifest__.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +################################################################################### + +{ + 'name': 'Appraisal KPI', + 'version': '18.0.1.0.0', + 'category': 'HR-Odex', + 'summary': 'Manage Appraisal KPI', + 'description': """ + Helps you to manage Appraisal of your company's staff. + """, + 'author': 'Expert Co. Ltd.', + 'company': 'Exp-co-ltd', + 'maintainer': 'Cybrosys Techno Solutions', + 'website': 'http://exp-sa.com', + 'depends': [ + + 'exp_hr_appraisal', 'base','kpi_scorecard', 'hr','kpi_scorecard', 'account', 'exp_hr_payroll', 'mail', 'hr_base', 'hr_contract', 'hr_contract_custom' + + ], + 'data': [ + 'security/group.xml', + 'security/ir.model.access.csv', + 'views/kpi_category.xml', + 'views/kpi_item.xml', + 'views/kpi_period.xml', + 'views/kpi_skills.xml', + 'views/skill_appraisal.xml', + 'views/years_employee_goals.xml', + 'views/employee_performance_evaluation.xml', + 'views/appraisal_percentage.xml', + 'views/employee_apprisal.xml', + + + + ], + 'installable': True, + 'auto_install': False, +} diff --git a/exp_hr_appraisal_kpi/i18n/ar_001.po b/exp_hr_appraisal_kpi/i18n/ar_001.po new file mode 100644 index 0000000..989e30f --- /dev/null +++ b/exp_hr_appraisal_kpi/i18n/ar_001.po @@ -0,0 +1,1097 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * exp_hr_appraisal_kpi +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2024-06-12 10:54+0000\n" +"PO-Revision-Date: 2024-06-12 10:54+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__employee_performance_evaluation__state__approve +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__skill_appraisal__state__approve +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_form +msgid "Accept" +msgstr "موافقة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__kpi_item__method_of_calculate__accumulative +msgid "Accumulative" +msgstr "تراكمي" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_needaction +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_needaction +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_needaction +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_needaction +msgid "Action Needed" +msgstr "تحتاج إلى تدخل" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_form +msgid "Add a Item" +msgstr "اضافة عنصر" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_form +msgid "Add a note" +msgstr "اضافة ملاحظة" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_form +msgid "Add a section" +msgstr "اضافة قسم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_hr_employee_appraisal +#: model:ir.model,name:exp_hr_appraisal_kpi.model_hr_group_employee_appraisal +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_group_employee_appraisal__appraisal_ids +msgid "Appraisal" +msgstr "تقـييم الموظف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_job_class_apprisal +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_job__appraisal_percentages_id +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_kpi_percentage +msgid "Appraisal Percentage" +msgstr "نسبة التقييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__years_employee_goals__state__apprisal +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.group_employee_apprisal_extend +msgid "Apprisal" +msgstr "التقييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__date_apprisal +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__date_apprisal +msgid "Apprisal Date" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.actions.act_window,name:exp_hr_appraisal_kpi.apprisal_percentag_act_window1 +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.apprisal_percentag_form_view +msgid "Apprisal Percentag" +msgstr "نسبة التقييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__apprisal_result +msgid "Apprisal Result" +msgstr "نتيجة التقييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_attachment_count +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_attachment_count +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_attachment_count +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_attachment_count +msgid "Attachment Count" +msgstr "عدد المرفقات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__kpi_item__method_of_calculate__avrerage +msgid "Average" +msgstr "متوسط" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__category_id +msgid "Category" +msgstr "الهدف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__choiec +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__choiec +msgid "Choiec" +msgstr "الدرجة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__years_employee_goals__state__close +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_goals_form +msgid "Close" +msgstr "مغلق" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.employee_apprisal_extend +msgid "Compute Apprisal" +msgstr "حساب التقييم" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.kpi_period_form_extend +msgid "Create Apprisal" +msgstr "انشاء تقيييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__create_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__create_uid +msgid "Created by" +msgstr "تم الإنشاء بواسطة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__create_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__create_date +msgid "Created on" +msgstr "تم الإنشاء في" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__department_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__department_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_item__department_item_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__department_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__department_id +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.group_employee_apprisal_extend +msgid "Department" +msgstr "القسم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_employee_table__name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__description +msgid "Description" +msgstr "الوصف" + + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_group_employee_appraisal__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_job__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_category__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_item__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__display_name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__done +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__done +msgid "Done" +msgstr "المحقق" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__employee_performance_evaluation__state__draft +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__skill_appraisal__state__draft +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__years_employee_goals__state__draft +msgid "Draft" +msgstr "مسودة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__employee_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__employee_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__employee_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__employee_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__employee_id +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.group_employee_apprisal_extend +msgid "Employee" +msgstr "الموظف" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/employee_apprisal.py:0 +#, python-format +msgid "Employee Apprisal must be unique per Employee, Year, and Period!" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__employee_appraisal2 +msgid "Employee Appraisal2" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__employee_apprisal_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__employee_apprisal_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__employee_apprisal_id +msgid "Employee Apprisal" +msgstr "تقييم الموظفين" + +#. module: exp_hr_appraisal_kpi +#: model:ir.actions.act_window,name:exp_hr_appraisal_kpi.action_year_goal_emp +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__emp_goal_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__employee_goals_id +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_year_employee_goals_list +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +msgid "Employee Goals" +msgstr "أهداف الموظف" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/years_employee_goals.py:0 +#, python-format +msgid "Employee Goals must be unique per Employee, Year, and kpi!" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_evalution_employee_goals_ +msgid "Employee Goals Appraisal" +msgstr "تقييم أهداف الموظف" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/employee_performance_evaluation.py:0 +#, python-format +msgid "Employee Goals Apprisal must be unique per Employee, Year, and Period!" +msgstr "" +"تقييم اهداف الموظف يجب ان يكون فريدا حيث لايتكرر الموظف مع السنة مع الفترة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_skill_appraisal_list +msgid "Employee Skill Appraisal" +msgstr "تقييم جدارات الموظف" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/skill_apprisal.py:0 +#, python-format +msgid "Employee Skill Apprisal must be unique per Employee, Year, and Period!" +msgstr "" +"تقييم جدارات الموظف يجب ان يكون فريدا بحيث لايتكرر السنة مع الموظف مع الفترة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__manager_id +msgid "Employee m" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.group_employee_apprisal_extend +msgid "Employee name" +msgstr "اسم الموظف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_employee_performance_evaluation +msgid "Employee performance evaluation" +msgstr "تقييم أداء الموظف" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.group_employee_apprisal_extend +msgid "Employees" +msgstr "الموظفين" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__date_end_k +msgid "End Date" +msgstr "تاريخ الانتهاء" + +#. module: exp_hr_appraisal_kpi +#: model:ir.actions.act_window,name:exp_hr_appraisal_kpi.action_evalution_goal_emp +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +msgid "Evaluation Employee Goals" +msgstr "تقييم أهداف الموظف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__mark_evaluation +msgid "Evaluation Mark" +msgstr "درجة التقييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_follower_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_follower_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_follower_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_follower_ids +msgid "Followers" +msgstr "متابعون" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_channel_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_channel_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_channel_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_channel_ids +msgid "Followers (Channels)" +msgstr "المتابعون (القنوات)" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_partner_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_partner_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_partner_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_partner_ids +msgid "Followers (Partners)" +msgstr "المتابعون (الشركاء)" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__target +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.hpi_item_extend +msgid "From(Done)" +msgstr "من (المنجز)" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__goal_ids +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_kpi_categories +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.employee_apprisal_extend +msgid "Goals" +msgstr "الأهداف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__goals_mark +msgid "Goals Apprisal Mark" +msgstr "درجة تقييم الاهداف" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.kpi_period_form_extend +msgid "Goals Period" +msgstr "فترة الأهداف" + +#. module: exp_hr_appraisal_kpi +#: model:res.groups,name:exp_hr_appraisal_kpi.group_appraisal_responsabil +msgid "Goals Responsible" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_kpi_goal_skill +msgid "Goals and Skills" +msgstr "الأهداف والجدارات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_group_employee_appraisal__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_job__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_category__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_item__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__id +msgid "ID" +msgstr "المُعرف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_needaction +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_unread +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__message_needaction +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__message_unread +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__message_needaction +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__message_unread +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__message_needaction +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__message_unread +msgid "If checked, new messages require your attention." +msgstr "إذا تم التحقق، فإن الرسائل الجديدة تحتاج إلى اهتمامك." + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_has_error +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_has_sms_error +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__message_has_error +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__message_has_sms_error +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__message_has_error +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__message_has_sms_error +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__message_has_error +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__message_has_sms_error +msgid "If checked, some messages have a delivery error." +msgstr "إذا تم التحقق، فإن بعض الرسائل بها خطأ في التسليم." + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_is_follower +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_is_follower +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_is_follower +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_is_follower +msgid "Is Follower" +msgstr "متابع" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__item_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_employee_table__item_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__item_id +msgid "Item" +msgstr "العنصر" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__items_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__items_ids +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_form +msgid "Items" +msgstr "العناصر" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_hr_job +msgid "Job Position" +msgstr "المنصب الوظيفي" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__job_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__job_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__job_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__job_id +msgid "Job Title" +msgstr "الوظيفة" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.group_employee_apprisal_extend +msgid "Job title" +msgstr "الوظيفة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__job_ids +msgid "Jobs" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_kpi_item +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__employee_eval_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__kpi_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__kpi_id +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_kpi_kpi +msgid "KPI" +msgstr "المؤشر" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_kpi_category +msgid "KPI Category" +msgstr "فئة مؤشر الأداء الرئيسي" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_kpi_period +msgid "KPI Period" +msgstr "فترة مؤشر الأدا" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__kip_id +msgid "Kip_id" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__kpi_goal_period_id +msgid "Kpi Goal Period" +msgstr "فترة أهداف مؤشر الأداء " + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period__kpi_goals_periods_ids +msgid "Kpi Goals Periods" +msgstr "فترات أهداف مؤشر الأداء" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__kpi_period_id +msgid "Kpi Period" +msgstr "فترة مؤشر الأداء" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period__kpi_periods_ids +msgid "Kpi Periods" +msgstr "فترات مؤشر الأداء " + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_group_employee_appraisal____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_job____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_category____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_item____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill____last_update +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__write_uid +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__write_uid +msgid "Last Updated by" +msgstr "آخر تحديث بواسطة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__write_date +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__write_date +msgid "Last Updated on" +msgstr "آخر تحديث في" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__level +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__level +msgid "Level" +msgstr "المستوى" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_main_attachment_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_main_attachment_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_main_attachment_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_main_attachment_id +msgid "Main Attachment" +msgstr "المرفق الرئيسي" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__manager_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__manager_id +msgid "Manager" +msgstr "المدير" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_item__mark_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__mark +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__mark_avg +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_employee_table__mark +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_employee_table__mark_avg +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__mark +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__mark_avg +msgid "Mark" +msgstr "الدرجة" + + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__mark_apprisal +msgid "Mark Apprisal" +msgstr "درجة التقييم" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.hpi_item_extend +msgid "Marks" +msgstr "الدرجات" + +#. module: exp_hr_appraisal_kpi +#: model:res.groups,name:exp_hr_appraisal_kpi.apprisal_kpi_group +msgid "Menu apprisal hide/show" +msgstr "إخفاء/إظهار قائمة التقييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_has_error +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_has_error +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_has_error +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_has_error +msgid "Message Delivery error" +msgstr "خطأ في تسليم الرسالة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_ids +msgid "Messages" +msgstr "الرسائل" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_item__method_of_calculate +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__method_of_calculate +msgid "Method Of Calculate" +msgstr "طريقة الحساب" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.apprisal_percentag_tree_view +msgid "ModelTitle" +msgstr "عنوان النموذج" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_item_item__name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__name +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__name +msgid "Name" +msgstr "الاسم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__notes +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.employee_apprisal_extend +msgid "Notes" +msgstr "ملاحظات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_needaction_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_needaction_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_needaction_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_needaction_counter +msgid "Number of Actions" +msgstr "عدد الإجراءات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_has_error_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_has_error_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_has_error_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_has_error_counter +msgid "Number of errors" +msgstr "عدد الأخطاء" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_needaction_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__message_needaction_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__message_needaction_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__message_needaction_counter +msgid "Number of messages which requires an action" +msgstr "عدد الرسائل التي تتطلب إجراء" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_has_error_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__message_has_error_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__message_has_error_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__message_has_error_counter +msgid "Number of messages with delivery error" +msgstr "عدد الرسائل ذات الأخطاء في التسليم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_unread_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__message_unread_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__message_unread_counter +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__message_unread_counter +msgid "Number of unread messages" +msgstr "عدد الرسائل غير المقروءة" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/kpi_period.py:0 +#: code:addons/exp_hr_appraisal_kpi/models/kpi_period.py:0 +#, python-format +msgid "Overlap detected between periods!" +msgstr "تم اكتشاف تداخل بين الفترات!" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__percentage_skills +msgid "Percentage of Skills Appraisal%" +msgstr "نسبة تقييم المهارات%" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_job_class_apprisal__percentage_kpi +msgid "Percentage of indicator Appraisal%" +msgstr "نسبة تقييم الأهداف%" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__period_goals_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__period_goals_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__period +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__goals_period_ids +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_goals_form +msgid "Period" +msgstr "الفترة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__period_goals_id +msgid "Period Of Goals" +msgstr "فترة الأهداف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.menu_kpi_period +msgid "Periods" +msgstr "الفترات" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/employee_apprisal.py:0 +#, python-format +msgid "" +"Please check appraisal result configuration , there is more than result for " +"percentage %s are %s " +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/employee_apprisal.py:0 +#, python-format +msgid "Please select at least one employee to make appraisal." +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__reason +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__reason +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__reason +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__reason +msgid "Reason/Justification" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__recommendations +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__recommendations +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_form +msgid "Recommendations" +msgstr "التوصيات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__employee_performance_evaluation__state__refuse +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__skill_appraisal__state__refuse +msgid "Refused" +msgstr "مرفوض" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__user_id +msgid "Related user name for the resource to manage its access." +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_form +msgid "Reset To Draft" +msgstr "إعادة تعيين إلى مسودة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_item__responsible_item_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__responsible_item_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__user_id +msgid "Responsible" +msgstr "المسؤول" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__avarage +msgid "Result" +msgstr "النتيجة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_has_sms_error +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_has_sms_error +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_has_sms_error +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_has_sms_error +msgid "SMS Delivery error" +msgstr "خطأ في تسليم الرسائل النصية" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +msgid "Select Goals" +msgstr "جلب الاهداف" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_form +msgid "Send" +msgstr "إرسال" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__sequence +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__sequence +msgid "Sequence" +msgstr "متسلسل" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__skill_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__skill_id +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.skill_search_view +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_form +msgid "Skill" +msgstr "الجدارات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.actions.act_window,name:exp_hr_appraisal_kpi.action_skill_appraisal +#: model:ir.model,name:exp_hr_appraisal_kpi.model_skill_appraisal +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item__skill_appraisal_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_item_table__skill_appraisal_id +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_tree +msgid "Skill Appraisal" +msgstr "تقييم الجدارات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.actions.act_window,name:exp_hr_appraisal_kpi.skill_act_window +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__skill_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_job__item_job_ids +#: model:ir.ui.menu,name:exp_hr_appraisal_kpi.skill_menu +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.employee_apprisal_extend +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.hr_job_from_extend +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_tree +msgid "Skills" +msgstr "الجدارات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__skill_mark +msgid "Skills Apprisal Mark" +msgstr "درجة تقييم الجدارات" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.kpi_period_form_extend +msgid "Skills Period" +msgstr "فترة الجدارات" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_kpi_period_notes__date_start_k +msgid "Star Date" +msgstr "تاريخ البدء" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_goals_form +msgid "Start Apprisal" +msgstr "بدء التقييم" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__state +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__state +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__state +msgid "State" +msgstr "الحالة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__target +msgid "Target" +msgstr "المستهدف" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/kpi_item.py:0 +#, python-format +msgid "The To value must be greater than the From value." +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.actions.act_window,help:exp_hr_appraisal_kpi.apprisal_percentag_act_window1 +msgid "There is no examples click here to add new ModelTitle." +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.actions.act_window,help:exp_hr_appraisal_kpi.skill_act_window +msgid "There is no examples click here to add new Skill." +msgstr "لا يوجد أمثلة انقر هنا لإضافة جدارات جديدة." + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_mark_mark__to +msgid "To(Target)" +msgstr "الي (المستهدف)" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_evalution_goals_employee_tree1 +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_year_goals_employee_tree1 +msgid "Total" +msgstr "الإجمالي" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__total +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__total_score +msgid "Total Mark" +msgstr "درجة التقييم" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.employee_apprisal_extend +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +msgid "Total Target" +msgstr "الهدف الإجمالي" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.employee_apprisal_extend +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +msgid "Total Weight" +msgstr "الوزن الإجمالي" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/appraisal_percentage.py:0 +#, python-format +msgid "Total percentage should be 100." +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_goals_form +msgid "Totat Traget" +msgstr "الهدف الإجمالي" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__kpi_item__method_of_calculate__undefined +msgid "Undefined" +msgstr "غير محدد" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_unread +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_unread +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_unread +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_unread +msgid "Unread Messages" +msgstr "الرسائل غير المقروءة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__message_unread_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__message_unread_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__message_unread_counter +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__message_unread_counter +msgid "Unread Messages Counter" +msgstr "عداد الرسائل غير المقروءة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__employee_performance_evaluation__state__dir_manager +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__skill_appraisal__state__dir_manager +msgid "Wait Employee Accept" +msgstr "انتظار قبول الموظف" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__employee_performance_evaluation__state__wait_hr_manager +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__skill_appraisal__state__wait_hr_manager +msgid "Wait HR Manager Accept" +msgstr "انتظار قبول مدير الموارد البشرية" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__employee_performance_evaluation__state__wait_dir_manager +#: model:ir.model.fields.selection,name:exp_hr_appraisal_kpi.selection__skill_appraisal__state__wait_dir_manager +msgid "Wait Manager Accept" +msgstr "انتظار قبول المدير" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__website_message_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__website_message_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_skill__website_message_ids +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__website_message_ids +msgid "Website Messages" +msgstr "رسائل الموقع الإلكتروني" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_employee_performance_evaluation__website_message_ids +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_appraisal__website_message_ids +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_skill_skill__website_message_ids +#: model:ir.model.fields,help:exp_hr_appraisal_kpi.field_years_employee_goals__website_message_ids +msgid "Website communication history" +msgstr "سجلات التواصل عبر الموقع الإلكتروني" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__weight +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__weight +msgid "Weight" +msgstr "الوزن" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_employee_performance_evaluation__year_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_employee_appraisal__year_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_hr_group_employee_appraisal__year_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_period_goals__year_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_skill_appraisal__year_id +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__year_id +msgid "Year" +msgstr "السنة" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_goals_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_evalution_goals_employee_tree1 +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_year_goals_employee_tree1 +msgid "Year Employee Goals" +msgstr "أهداف موظف العام" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__year_target +msgid "Year Target" +msgstr "المستهدف" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/employee_performance_evaluation.py:0 +#, python-format +msgid "" +"You can't delete a Goal apprisal not in Draft State , archive it instead." +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: code:addons/exp_hr_appraisal_kpi/models/skill_apprisal.py:0 +#, python-format +msgid "" +"You can't delete a Skill apprisal not in Draft State , archive it instead." +msgstr "لايمكن المسح حيث تم الارسال يمكن الارشفة بدلا من هذا" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_item_item +msgid "item.item" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.item_tree_view_tree +msgid "item_tree_tree" +msgstr "عنصر_شجرة_عرض" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_kpi_period_notes +msgid "kpi.period.notes" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_mark_mark +msgid "mark.mark" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_period_goals +msgid "period.goals" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_evalu_form +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_skill_appraisal_form +msgid "refuse" +msgstr "رفض" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_skill_item +msgid "skill.item" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_skill_item_table +msgid "skill.item.table" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_skill_skill +msgid "skill.skill" +msgstr "" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model,name:exp_hr_appraisal_kpi.model_years_employee_goals +msgid "years employee goals" +msgstr "أهداف الموظفين السنوية" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__first_period_traget +msgid "First Period Traget" +msgstr "مستهدف الفترة الاولي" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__second_period_traget +msgid "Second Period Traget" +msgstr "مستهدف الفترة الثانية" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__third_period_traget +msgid "Third Period Traget" +msgstr "مستهدف الفترة الثالثة" + +#. module: exp_hr_appraisal_kpi +#: model:ir.model.fields,field_description:exp_hr_appraisal_kpi.field_years_employee_goals__fourth_period_traget +msgid "Fourth Period Traget" +msgstr "مستهدف الفترة الرابعة" + +#. module: exp_hr_appraisal_kpi +#: model_terms:ir.ui.view,arch_db:exp_hr_appraisal_kpi.view_emplo_goals_form +msgid "Set To Dratt" +msgstr "اعادة التعيين لمسودة" diff --git a/exp_hr_appraisal_kpi/models/__init__.py b/exp_hr_appraisal_kpi/models/__init__.py new file mode 100644 index 0000000..99fdec4 --- /dev/null +++ b/exp_hr_appraisal_kpi/models/__init__.py @@ -0,0 +1,9 @@ +from . import kpi_item +from . import kpi_period +from . import kpi_skill +from . import skill_apprisal +from . import years_employee_goals +from . import employee_performance_evaluation +from . import appraisal_percentage +from . import employee_apprisal + diff --git a/exp_hr_appraisal_kpi/models/appraisal_percentage.py b/exp_hr_appraisal_kpi/models/appraisal_percentage.py new file mode 100644 index 0000000..b0f98d3 --- /dev/null +++ b/exp_hr_appraisal_kpi/models/appraisal_percentage.py @@ -0,0 +1,23 @@ +from odoo import fields, models, api,_ +from odoo.exceptions import ValidationError + +class AppraisalPercentage(models.Model): + _name = 'job.class.apprisal' + _description = 'Appraisal Percentage' + name = fields.Char(string='Name') + percentage_kpi = fields.Float(string="Percentage of indicator Appraisal%",) + percentage_skills = fields.Float(string="Percentage of Skills Appraisal%",) + job_ids = fields.Many2many( + comodel_name='hr.job', + string='Jobs') + + # Constraint to ensure total percentage is 100 + @api.constrains('percentage_kpi', 'percentage_skills') + def _check_percentage_total(self): + for record in self: + total_percentage = record.percentage_kpi + record.percentage_skills + if total_percentage != 1: + raise ValidationError(_("Total percentage should be 100.")) + if self.job_ids: + for rec in self.job_ids: + rec.appraisal_percentages_id = self.id \ No newline at end of file diff --git a/exp_hr_appraisal_kpi/models/employee_apprisal.py b/exp_hr_appraisal_kpi/models/employee_apprisal.py new file mode 100644 index 0000000..7193c46 --- /dev/null +++ b/exp_hr_appraisal_kpi/models/employee_apprisal.py @@ -0,0 +1,162 @@ +from odoo import models, fields,_,api,exceptions + +class EmployeeApprisal(models.Model): + _inherit = 'hr.group.employee.appraisal' + year_id = fields.Many2one(comodel_name='kpi.period',string='Year',required=True) + appraisal_ids = fields.One2many('hr.employee.appraisal', 'employee_appraisal2') + + def gen_appraisal(self): + for item in self: + if item.employee_ids: + appraisal_lines_list = [] + # Fill employee appraisal + for element in item.employee_ids: + standard_appraisal_list, manager_appraisal_list = [], [] + year_goal_obj = self.env['years.employee.goals'].search([('employee_id','=',element.id),('year_id','=',self.year_id.id)]) + print('year = ',year_goal_obj) + goal_ids = year_goal_obj.ids if year_goal_obj else [] + appraisal_line = { + 'employee_id': element.id, + 'manager_id': item.manager_id.id, + 'year_id': item.year_id.id, + 'department_id': item.department_id.id, + 'job_id': element.job_id.id, + 'appraisal_date': item.date, + 'goal_ids': [(6, 0, goal_ids)], + } + line_id = self.env['hr.employee.appraisal'].create(appraisal_line) + line_id.compute_apprisal() + appraisal_lines_list.append(line_id.id) + + item.appraisal_ids = self.env['hr.employee.appraisal'].browse(appraisal_lines_list) + + else: + raise exceptions.Warning(_('Please select at least one employee to make appraisal.')) + item.state = 'gen_appraisal' + def draft(self): + print('draft ..............') + # Delete all appraisals when re-draft + if self.appraisal_ids: + print('if appr line.............') + for line in self.appraisal_ids: + print('for..................') + if line.state == 'draft': + print('state...........') + line.unlink() + self.state = 'draft' + + elif line.state == 'closed': + line.state = 'state_done' + self.state = 'start_appraisal' + + elif line.state == 'state_done': + self.state = 'start_appraisal' + # Call the original draft method using super() + +class EmployeeApprisal(models.Model): + _inherit = 'hr.employee.appraisal' + + employee_appraisal2 = fields.Many2one('hr.group.employee.appraisal') # Inverse field + + employee_id = fields.Many2one('hr.employee', string='Employee',tracking=True,required=True) + manager_id = fields.Many2one('hr.employee', string='Manager',readonly=False,tracking=True,required=True,default=lambda item: item.get_user_id()) + year_id = fields.Many2one(comodel_name='kpi.period',string='Year',required=True) + period_goals_id = fields.Many2one('kpi.period.notes',force_save=1,string='Period',tracking=True,) + department_id = fields.Many2one('hr.department',required=True,readonly=False,store=True,compute='compute_depart_job', tracking=True,string='Department') + job_id = fields.Many2one('hr.job',force_save=1,readonly=True,store=True, string='Job Title',related='employee_id.job_id',tracking=True,) + + goals_mark = fields.Float(store=True,string='Goals Apprisal Mark',readonly=True,tracking=True) + skill_mark = fields.Float(store=True,string='Skills Apprisal Mark',readonly=True,tracking=True) + total_score = fields.Float(string='Total Mark',store=True,readonly=True,compute='compute_total_score',tracking=True) + apprisal_result = fields.Many2one('appraisal.result',string='Apprisal Result',store=True,tracking=True) + + notes= fields.Text(string='Notes',required=False) + goal_ids = fields.One2many('years.employee.goals', 'employee_apprisal_id', string='Goals') + skill_ids = fields.One2many('skill.item.employee.table', 'employee_apprisal_id', string='Skills') + + @api.constrains('employee_id', 'year_id') + def check_unique_employee_year_period_goals(self): + for record in self: + if self.search_count([ + ('employee_id', '=', record.employee_id.id), + ('year_id', '=', record.year_id.id), + ('id', '!=', record.id), + ]) > 0: + raise exceptions.ValidationError(_("Employee Apprisal must be unique per Employee, Year, and Period!")) + @api.depends('skill_mark','goals_mark',) + def compute_total_score(self): + appraisal_result_list = [] + for rec in self: + if rec.skill_mark and rec.goals_mark and rec.job_id.appraisal_percentages_id.percentage_kpi>0.0 and rec.job_id.appraisal_percentages_id.percentage_skills>0.0: + skill_mark_precentage = rec.skill_mark*rec.job_id.appraisal_percentages_id.percentage_skills + goal_mark_precentage = rec.goals_mark*rec.job_id.appraisal_percentages_id.percentage_kpi + + rec.total_score = (skill_mark_precentage+goal_mark_precentage) + appraisal_result = self.env['appraisal.result'].search([ + ('result_from', '<', rec.total_score), + ('result_to', '>=', rec.total_score)]) + if rec.total_score and len(appraisal_result) > 1: + for line in appraisal_result: + appraisal_result_list.append(line.name) + raise exceptions.Warning( + _('Please check appraisal result configuration , there is more than result for ' + 'percentage %s are %s ') % ( + round(rec.total_score, 2), appraisal_result_list)) + else: + rec.appraisal_result = appraisal_result.id + def get_user_id(self): + employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + if employee_id: + return employee_id.id + else: + return False + + @api.depends('employee_id') + def compute_depart_job(self): + for rec in self: + if rec.employee_id: + rec.department_id = rec.employee_id.department_id.id + def compute_apprisal(self): + year_goal_obj = self.env['years.employee.goals'].search([('employee_id','=',self.employee_id.id),('year_id','=',self.year_id.id)]) + if year_goal_obj: + print('if goal...........') + self.goal_ids = year_goal_obj.ids + # + sum2 = 0 + for rec in self.goal_ids: + sum2 = sum2+ ((rec.weight*int(rec.choiec))/100) + self.goals_mark = sum2 + # + item_lines=[(5,0,0)] + skill_apprisal = self.env['skill.appraisal'].search([('employee_id','=',self.employee_id.id),('year_id','=',self.year_id.id),('job_id','=',self.job_id.id)]) + dic_item = {} + print('s a = ',skill_apprisal) + for obj in skill_apprisal: + for rec in obj.items_ids: + if rec.mark and rec.item_id: + if rec.item_id.name in dic_item: + dic_item[rec.item_id.name].append(rec.mark) + else: + dic_item.update({rec.item_id.name:[rec.mark]}) + print('dic_item = ',dic_item) + averages = {} + for key, values in dic_item.items(): + # Convert values to integers and calculate sum + total = sum(int(value) for value in values) + # Calculate average + avg = total / len(values) + # Store the average in the dictionary + averages[key] = avg + + if self.job_id: + for line in self.job_id.item_job_ids: + + line_item = {'item_id':line.item_id.id,'name':line.name,'level':line.level,} + if line.item_id.name in averages: + line_item.update({'mark_avg':averages[line.item_id.name]}) + item_lines.append((0,0,line_item)) + self.skill_ids = item_lines + # Calculate the average of averages + if len(averages)!=0: + average_of_averages = sum(averages.values()) / len(averages) + self.skill_mark = average_of_averages diff --git a/exp_hr_appraisal_kpi/models/employee_performance_evaluation.py b/exp_hr_appraisal_kpi/models/employee_performance_evaluation.py new file mode 100644 index 0000000..56a4722 --- /dev/null +++ b/exp_hr_appraisal_kpi/models/employee_performance_evaluation.py @@ -0,0 +1,149 @@ +from odoo import fields, models, exceptions, api, _ +from odoo.exceptions import UserError, ValidationError +from lxml import etree +import json + + +class EmployeePerformanceEvaluation(models.Model): + _name = 'employee.performance.evaluation' + _rec_name = 'employee_id' + _inherit = ['mail.thread'] + _description = "Employee performance evaluation" + recommendations = fields.Text(string='Recommendations', tracking=True, required=False) + total = fields.Float(string='Total Mark', readonly=True, store=True, tracking=True, ) + mark_apprisal = fields.Float(string='Mark Apprisal', readonly=False, store=True, tracking=True, + compute='total_mark') + date_apprisal = fields.Date(default=lambda self: fields.Date.today(), string='Apprisal Date', tracking=True, ) + employee_id = fields.Many2one('hr.employee', string='Employee', tracking=True, required=True) + manager_id = fields.Many2one('hr.employee', string='Employee m', readonly=False, tracking=True, required=False, + default=lambda item: item.get_user_id()) + year_id = fields.Many2one(comodel_name='kpi.period', string='Year') + period_goals_id = fields.Many2one('kpi.period.notes', force_save=1, string='Period', tracking=True, ) + department_id = fields.Many2one('hr.department', readonly=False, store=True, compute='compute_depart_job', + tracking=True, string='Department') + job_id = fields.Many2one('hr.job', force_save=1, readonly=True, store=True, string='Job Title', + related='employee_id.job_id', tracking=True, ) + state = fields.Selection([ + ('draft', 'Draft'), ('dir_manager', 'Wait Employee Accept'), + ('wait_dir_manager', 'Wait Manager Accept'), + ('wait_hr_manager', 'Wait HR Manager Accept'), + ('approve', 'Accept'), + ('refuse', 'Refused') + ], string='State', tracking=True, default='draft') + emp_goal_ids = fields.One2many(comodel_name='period.goals', inverse_name='employee_eval_id', + string='Employee Goals', copy=True) + + @api.constrains('employee_id', 'year_id', 'period_goals_id') + def check_unique_employee_year_period_goals(self): + for record in self: + if self.search_count([ + ('employee_id', '=', record.employee_id.id), + ('year_id', '=', record.year_id.id), + ('period_goals_id', '=', record.period_goals_id.id), + ('id', '!=', record.id), + ]) > 0: + raise exceptions.ValidationError( + _("Employee Goals Apprisal must be unique per Employee, Year, and Period!")) + + def get_user_id(self): + employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + if employee_id: + return employee_id.id + else: + return False + + @api.depends('employee_id') + def compute_depart_job(self): + for rec in self: + if rec.employee_id: + rec.department_id = rec.employee_id.department_id.id + + @api.model + def fields_view_get(self, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): + res = super(EmployeePerformanceEvaluation, self).fields_view_get(view_id=view_id, view_type=view_type, + toolbar=toolbar, + submenu=submenu) + doc = etree.XML(res['arch']) + emp_group = self.env.ref('exp_hr_appraisal.group_appraisal_employee').id + user_group = self.env.ref('exp_hr_appraisal.group_appraisal_user').id + manager_group = self.env.ref('exp_hr_appraisal.group_appraisal_manager').id + current_user_gids = self.env.user.groups_id.mapped('id') + if ((emp_group in current_user_gids) and (user_group not in current_user_gids) and ( + manager_group not in current_user_gids)): + if view_type == 'tree' or view_type == 'form': + print('if node1.....') + + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('if node.....') + + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + for node in doc.xpath("//form"): + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + + res['arch'] = etree.tostring(doc) + elif ((user_group in current_user_gids or manager_group in current_user_gids)): + if view_type == 'tree' or view_type == 'form': + print('if node2.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + res['arch'] = etree.tostring(doc) + elif ( + user_group in current_user_gids and manager_group in current_user_gids and emp_group in current_user_gids): + if view_type == 'tree' or view_type == 'form': + print('if node3.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + + res['arch'] = etree.tostring(doc) + return res + + def send(self): + self.state = 'wait_dir_manager' + + def reset_draft(self): + self.state = 'draft' + + def action_approval(self): + if self.state == 'dir_manager': + self.state = 'wait_dir_manager' + elif self.state == 'wait_dir_manager': + self.state = 'wait_hr_manager' + else: + self.state = 'approve' + + def action_refuse(self): + self.state = 'refuse' + + def onchange_emp_goal_ids(self): + goals_lines = [(5, 0, 0)] + sum = 0 + period_goal_obj = self.env['period.goals'].search( + [('period_goals_id', '=', self.period_goals_id.id), ('employee_id', '=', self.employee_id.id), + ('year_id', '=', self.year_id.id)]) + self.emp_goal_ids = period_goal_obj.ids + for rec in self.emp_goal_ids: + sum = sum + ((rec.weight * rec.mark_evaluation) / 100) + self.mark_apprisal = sum + + def unlink(self): + for rec in self: + if rec.state != 'draft': + raise ValidationError(_("You can't delete a Goal apprisal not in Draft State , archive it instead.")) + return super().unlink() diff --git a/exp_hr_appraisal_kpi/models/kpi_item.py b/exp_hr_appraisal_kpi/models/kpi_item.py new file mode 100644 index 0000000..58fdfee --- /dev/null +++ b/exp_hr_appraisal_kpi/models/kpi_item.py @@ -0,0 +1,150 @@ +from odoo import fields, models, api,_ +from lxml import etree +import json +from odoo.exceptions import MissingError, UserError, ValidationError, AccessError + +class KPICategory(models.Model): + _inherit = 'kpi.category' + @api.model + def fields_view_get(self, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): + res = super(KPICategory, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, + submenu=submenu) + doc = etree.XML(res['arch']) + emp_group = self.env.ref('exp_hr_appraisal.group_appraisal_employee').id + user_group = self.env.ref('exp_hr_appraisal.group_appraisal_user').id + manager_group = self.env.ref('exp_hr_appraisal.group_appraisal_manager').id + current_user_gids = self.env.user.groups_id.mapped('id') + if ((emp_group in current_user_gids) and (user_group not in current_user_gids )and(manager_group not in current_user_gids)): + if view_type=='tree' or view_type=='form': + print('if node1.....') + + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('if node.....') + + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + for node in doc.xpath("//form"): + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + + res['arch'] = etree.tostring(doc) + elif ((user_group in current_user_gids or manager_group in current_user_gids)): + if view_type=='tree' or view_type=='form': + print('if node2.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + res['arch'] = etree.tostring(doc) + elif (user_group in current_user_gids and manager_group in current_user_gids and emp_group in current_user_gids): + if view_type=='tree' or view_type=='form': + print('if node3.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + + res['arch'] = etree.tostring(doc) + return res +class KPIitem(models.Model): + _inherit = 'kpi.item' + department_item_id = fields.Many2one(comodel_name='hr.department',string='Department') + responsible_item_id = fields.Many2one(comodel_name='hr.employee',string='Responsible') + mark_ids = fields.One2many(comodel_name='mark.mark',inverse_name='kip_id') + method_of_calculate = fields.Selection( + string='Method Of Calculate', + selection=[('accumulative', 'Accumulative'), + ('avrerage', 'Average'),('undefined', 'Undefined'),], + required=False,default='accumulative') + + @api.model + def fields_view_get(self, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): + res = super(KPIitem, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, + submenu=submenu) + doc = etree.XML(res['arch']) + emp_group = self.env.ref('exp_hr_appraisal.group_appraisal_employee').id + user_group = self.env.ref('exp_hr_appraisal.group_appraisal_user').id + manager_group = self.env.ref('exp_hr_appraisal.group_appraisal_manager').id + current_user_gids = self.env.user.groups_id.mapped('id') + if ((emp_group in current_user_gids) and (user_group not in current_user_gids )and(manager_group not in current_user_gids)): + if view_type=='tree' or view_type=='form': + print('if node1.....') + + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('if node.....') + + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + for node in doc.xpath("//form"): + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + + res['arch'] = etree.tostring(doc) + elif ((user_group in current_user_gids or manager_group in current_user_gids)): + if view_type=='tree' or view_type=='form': + print('if node2.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + res['arch'] = etree.tostring(doc) + elif (user_group in current_user_gids and manager_group in current_user_gids and emp_group in current_user_gids): + if view_type=='tree' or view_type=='form': + print('if node3.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + + res['arch'] = etree.tostring(doc) + return res + + @api.onchange('department_item_id') + def onchange_responsible(self): + domain = [] + if self.department_item_id: + # Define your dynamic domain based on field1's value + domain = [('department_id', '=', self.department_item_id.id)] + return {'domain': {'responsible_item_id': domain}} + + +class Marks(models.Model): + _name = 'mark.mark' + choiec = fields.Selection(string='Choiec',selection=[('1', '1'), ('2', '2'), ('3', '3'), ('4', '4'),('5','5'),]) + target = fields.Float(string='From(Done)',) + to = fields.Float(string='To(Target)',) + kip_id = fields.Many2one(comodel_name='kpi.item',string='Kip_id') + + @api.constrains('target', 'to', 'kip_id') + def _check_target_to_values(self): + for record in self: + if record.to <= record.target: + raise ValidationError(_('The To value must be greater than the From value.')) + + # Get previous marks for the same KPI sorted by target + # previous_marks = self.env['mark.mark'].search([('kip_id', '=', record.kip_id.id), ('id', '!=', record.id)], order='target') + # for prev_mark in previous_marks: + # if record.target <= prev_mark.to: + # raise ValidationError(_('The From value must be greater than the previous To value.')) diff --git a/exp_hr_appraisal_kpi/models/kpi_period.py b/exp_hr_appraisal_kpi/models/kpi_period.py new file mode 100644 index 0000000..74fe018 --- /dev/null +++ b/exp_hr_appraisal_kpi/models/kpi_period.py @@ -0,0 +1,62 @@ +from odoo import fields, models, api,_ +from odoo.exceptions import ValidationError +from odoo import models, api, exceptions +from datetime import timedelta +class KPIPeriod(models.Model): + _inherit = 'kpi.period' + kpi_periods_ids = fields.One2many( + comodel_name='kpi.period.notes', + inverse_name='kpi_period_id', + ondelete='cascade') # Add this line to enable cascade deletion + kpi_goals_periods_ids = fields.One2many( + comodel_name='kpi.period.notes', + inverse_name='kpi_goal_period_id', + ondelete='cascade' ) # Add this line to enable cascade deletion + + + +class KIPSkills (models.Model): + _name = 'kpi.period.notes' + + name = fields.Char(string='Name',) + sequence = fields.Char(string='Sequence',) + date_start_k = fields.Date(string='Star Date',) + date_end_k = fields.Date(string='End Date',) + kpi_period_id = fields.Many2one(comodel_name='kpi.period',ondelete='cascade') + kpi_goal_period_id = fields.Many2one(comodel_name='kpi.period',ondelete='cascade') + + def create_apprisal_goals_employee(self): + employee_objs = self.env['hr.employee'].search([('state','=','open')]) + employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + + for item in self: + # Fill employee appraisal + for element in employee_objs: + appraisal_line = { + 'employee_id': element.id, + 'year_id': item.kpi_goal_period_id.id, + 'department_id': element.department_id.id, + 'job_id': element.job_id.id, + 'manager_id': employee_id.id, + 'date_apprisal': fields.Date.today(), + 'period_goals_id': item.id, + } + line_id = self.env['employee.performance.evaluation'].create(appraisal_line) + line_id.onchange_emp_goal_ids() + @api.constrains('date_start_k','kpi_goal_period_id','date_end_k') + def _check_period_overlap(self): + for record in self: + if record.kpi_goal_period_id: + periods = record.kpi_goal_period_id.kpi_goals_periods_ids.sorted(key=lambda r: r.date_start_k) + for i in range(1, len(periods)): + if periods[i-1].date_end_k >= periods[i].date_start_k: + raise ValidationError(_("Overlap detected between periods!")) + + @api.constrains('date_start_k','kpi_period_id','date_end_k') + def _check_period_overlap2(self): + for record in self: + if record.kpi_period_id: + periods = record.kpi_period_id.kpi_periods_ids.sorted(key=lambda r: r.date_start_k) + for i in range(1, len(periods)): + if periods[i-1].date_end_k >= periods[i].date_start_k: + raise ValidationError(_("Overlap detected between periods!")) diff --git a/exp_hr_appraisal_kpi/models/kpi_skill.py b/exp_hr_appraisal_kpi/models/kpi_skill.py new file mode 100644 index 0000000..cfc9950 --- /dev/null +++ b/exp_hr_appraisal_kpi/models/kpi_skill.py @@ -0,0 +1,61 @@ +from odoo import fields, models, api +class Skill(models.Model): + _name = 'skill.skill' + _inherit = ['mail.thread'] + + name = fields.Char(string='Name', required=True,tracking=True,) + description = fields.Text(string='Description',tracking=True,) + items_ids = fields.One2many('skill.item', 'skill_id', string='Items',tracking=True,) +class SkillItems(models.Model): + _name = 'skill.item' + + skill_id = fields.Many2one('skill.skill', string='Skill',ondelete='cascade') + skill_appraisal_id = fields.Many2one(comodel_name='skill.appraisal') + name = fields.Char(string='Description') + level = fields.Selection([('beginner', '1'),('intermediate', '2'),('advanced', '3')],string='Level', default='beginner') + mark = fields.Selection([('1', '1'),('2', '2'),('3', '3'),('4', '4'),('5', '5')],string='Mark',Ccopy=False) + mark_avg = fields.Float(string='Mark',Ccopy=False) + item_id = fields.Many2one(comodel_name='item.item',string='Item') + display_type = fields.Selection([ + ('line_section', "Section"), + ('line_note', "Note")],default=False, help="Technical field for UX purpose.") + employee_apprisal_id = fields.Many2one( + comodel_name='hr.employee.appraisal') + sequence = fields.Integer(string='Sequence', default=10) + +class SkillItems(models.Model): + _name = 'skill.item.table' + + skill_id = fields.Many2one('skill.skill', string='Skill') + skill_appraisal_id = fields.Many2one(comodel_name='skill.appraisal',ondelete='cascade') + name = fields.Char(string='Description') + level = fields.Selection([('beginner', '1'),('intermediate', '2'),('advanced', '3')],string='Level', default='beginner') + mark = fields.Selection([('1', '1'),('2', '2'),('3', '3'),('4', '4'),('5', '5')],string='Mark',Ccopy=False) + mark_avg = fields.Float(string='Mark',Ccopy=False) + item_id = fields.Many2one(comodel_name='item.item',string='Item') + employee_apprisal_id = fields.Many2one( + + comodel_name='hr.employee.appraisal') +class SkillItems(models.Model): + _name = 'skill.item.employee.table' + + skill_id = fields.Many2one('skill.skill', string='Skill') + skill_appraisal_id = fields.Many2one(comodel_name='skill.appraisal',ondelete='cascade') + name = fields.Char(string='Description') + level = fields.Selection([('beginner', '1'),('intermediate', '2'),('advanced', '3')],string='Level', default='beginner') + mark = fields.Selection([('1', '1'),('2', '2'),('3', '3'),('4', '4'),('5', '5')],string='Mark',Ccopy=False) + mark_avg = fields.Float(string='Mark',Ccopy=False) + item_id = fields.Many2one(comodel_name='item.item',string='Item') + employee_apprisal_id = fields.Many2one( + comodel_name='hr.employee.appraisal') + + +class SkillItem(models.Model): + _name = 'item.item' + name = fields.Char(string='Name') + +class SkillJob(models.Model): + _inherit = 'hr.job' + item_job_ids = fields.Many2many('skill.item', 'merge_item_skill1_rel', 'merge1_id', 'item1_id', string='Skills') + # appraisal_percentage_id = fields.Many2one(comodel_name='job.class.apprisal',string='Appraisal Percentage') + appraisal_percentages_id = fields.Many2one(comodel_name='job.class.apprisal',string='Appraisal Percentage') diff --git a/exp_hr_appraisal_kpi/models/skill_apprisal.py b/exp_hr_appraisal_kpi/models/skill_apprisal.py new file mode 100644 index 0000000..6e91cae --- /dev/null +++ b/exp_hr_appraisal_kpi/models/skill_apprisal.py @@ -0,0 +1,141 @@ +from odoo import fields, models,exceptions, api,_ +from odoo.exceptions import UserError,ValidationError +from lxml import etree +import json +class SkillAppraisal(models.Model): + _name = 'skill.appraisal' + _inherit = ['mail.thread'] + _rec_name = 'employee_id' + _description = 'Skill Appraisal' + name= fields.Char(string='Name',tracking=True,) + recommendations= fields.Text(string='Recommendations',tracking=True,required=False) + date_apprisal = fields.Date(default=lambda self: fields.Date.today(),string='Apprisal Date',tracking=True,) + employee_id = fields.Many2one('hr.employee', string='Employee',tracking=True,required=True) + manager_id = fields.Many2one('hr.employee', string='Manager',readonly=False,tracking=True,required=True,default=lambda item: item.get_user_id()) + period = fields.Many2one('kpi.period.notes',string='Period',tracking=True,) + department_id = fields.Many2one('hr.department',readonly=True,store=True,compute='compute_depart_job', tracking=True,string='Department') + job_id = fields.Many2one('hr.job',readonly=False,store=True, string='Job Title',tracking=True,) + year_id = fields.Many2one(comodel_name='kpi.period',string='Year') + + @api.constrains('employee_id', 'year_id', 'period') + def check_unique_employee_year_period_skills(self): + for record in self: + if self.search_count([ + ('employee_id', '=', record.employee_id.id), + ('year_id', '=', record.year_id.id), + ('period', '=', record.period.id), + ('id', '!=', record.id), + ]) > 0: + raise exceptions.ValidationError(_("Employee Skill Apprisal must be unique per Employee, Year, and Period!")) + + + state = fields.Selection([ + ('draft', 'Draft'),('dir_manager', 'Wait Employee Accept'), + ('wait_dir_manager', 'Wait Manager Accept'), + ('wait_hr_manager', 'Wait HR Manager Accept'), + ('approve', 'Accept'), + ('refuse', 'Refused') + ], string='State',tracking=True,default='draft') + avarage = fields.Float(string='Result',readonly=True,store=True,tracking=True,compute='calc_avg') + items_ids = fields.One2many(comodel_name='skill.item.table',inverse_name='skill_appraisal_id',string='Items',copy=True) + @api.model + def fields_view_get(self, view_id=None, view_type='form', context=None, toolbar=False, submenu=False): + res = super(SkillAppraisal, self).fields_view_get(view_id=view_id, view_type=view_type, toolbar=toolbar, + submenu=submenu) + doc = etree.XML(res['arch']) + emp_group = self.env.ref('exp_hr_appraisal.group_appraisal_employee').id + user_group = self.env.ref('exp_hr_appraisal.group_appraisal_user').id + manager_group = self.env.ref('exp_hr_appraisal.group_appraisal_manager').id + current_user_gids = self.env.user.groups_id.mapped('id') + if ((emp_group in current_user_gids) and (user_group not in current_user_gids )and(manager_group not in current_user_gids)): + if view_type=='tree' or view_type=='form': + print('if node1.....') + + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('if node.....') + + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + for node in doc.xpath("//form"): + node.set('create', 'false') + node.set('delete', 'false') + node.set('edit', 'false') + + res['arch'] = etree.tostring(doc) + elif ((user_group in current_user_gids or manager_group in current_user_gids)): + if view_type=='tree' or view_type=='form': + print('if node2.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + res['arch'] = etree.tostring(doc) + elif (user_group in current_user_gids and manager_group in current_user_gids and emp_group in current_user_gids): + if view_type=='tree' or view_type=='form': + print('if node3.....') + # if view_type == 'tree': + for node in doc.xpath("//tree"): + print('for..node') + node.set('create', 'true') + node.set('edit', 'true') + for node in doc.xpath("//form"): + node.set('create', 'true') + node.set('edit', 'true') + + res['arch'] = etree.tostring(doc) + return res + def get_user_id(self): + employee_id = self.env['hr.employee'].search([('user_id', '=', self.env.uid)], limit=1) + if employee_id: + return employee_id.id + else: + return False + + @api.depends('employee_id') + def compute_depart_job(self): + for rec in self: + if rec.employee_id: + rec.department_id = rec.employee_id.department_id.id + rec.job_id = rec.employee_id.job_id.id + + @api.depends('items_ids.mark') + def calc_avg(self): + sum = 0 + for rec in self.items_ids: + if rec.mark and len(self.items_ids)!=0: + sum = sum+int(rec.mark) + self.avarage = sum/len(self.items_ids) + def send(self): + self.state = 'dir_manager' + def reset_draft(self): + self.state = 'draft' + def action_approval(self): + if self.state=='dir_manager': + self.state='wait_dir_manager' + elif self.state=='wait_dir_manager': + self.state='wait_hr_manager' + else: + self.state='approve' + + def action_refuse(self): + self.state = 'refuse' + + @api.onchange('job_id','employee_id') + def onchange_emp(self): + item_lines=[(5,0,0)] + for line in self.job_id.item_job_ids: + line_item = {'item_id':line.item_id.id,'name':line.name,'level':line.level} + item_lines.append((0,0,line_item)) + self.items_ids = item_lines + + def unlink(self): + for rec in self: + if rec.state != 'draft': + raise ValidationError(_("You can't delete a Skill apprisal not in Draft State , archive it instead.")) + return super().unlink() diff --git a/exp_hr_appraisal_kpi/models/years_employee_goals.py b/exp_hr_appraisal_kpi/models/years_employee_goals.py new file mode 100644 index 0000000..c6ac9b1 --- /dev/null +++ b/exp_hr_appraisal_kpi/models/years_employee_goals.py @@ -0,0 +1,220 @@ +# -*- coding: utf-8 -*- +# Part of Odoo. See LICENSE file for full copyright and licensing details. + +from odoo import fields, models, api,exceptions,_ + + +class Period(models.Model): + _name = 'period.goals' + + period_goals_id = fields.Many2one('kpi.period.notes', domain=[('kpi_period_id','=',False)], string='Period Of Goals', tracking=True) + employee_goals_id = fields.Many2one('years.employee.goals') + target = fields.Float(string='Target', store=True) + done = fields.Float(string='Done') + kpi_id = fields.Many2one(comodel_name='kpi.item', string='KPI', related='employee_goals_id.kpi_id') + employee_eval_id = fields.Many2one(comodel_name='employee.performance.evaluation', string='KPI') + weight = fields.Float(string='Weight', related='employee_goals_id.weight') + mark_evaluation = fields.Integer(string='Evaluation Mark', store=True, compute='_compute_mark_evaluation') + year_id = fields.Many2one(comodel_name='kpi.period', related='employee_goals_id.year_id') + employee_id = fields.Many2one(comodel_name='hr.employee', related='employee_goals_id.employee_id') + + @api.depends('done', 'target', 'kpi_id') + def _compute_mark_evaluation(self): + sum = 0 + for record in self: + if record.done!=0.0 and record.target!=0.0 and record.kpi_id: + done_percentage = (record.done / record.target) * 100 + marks = self.env['mark.mark'].search([('kip_id', '=', record.kpi_id.id)]) + if marks: + # Finding the closest mark where the done_percentage fits into the target-to range + closest_mark = min( + marks, + key=lambda x: abs(done_percentage - ((x.target + x.to) / 2)) + ) + if closest_mark.target <= done_percentage <= closest_mark.to: + record.mark_evaluation = int(closest_mark.choiec) + closest_mark = None + for mark in marks: + if mark.target <= done_percentage <= mark.to: + record.mark_evaluation = mark.choiec + break + else: + record.mark_evaluation = 0 # Or any other default value if fields are empty + sum = sum+ ((record.weight*record.mark_evaluation)/100) + record.employee_eval_id.mark_apprisal = sum + +class YearEmployeeGoals(models.Model): + _name = 'years.employee.goals' + _inherit = ['mail.thread'] + _description = 'years employee goals' + _rec_name = 'employee_id' + + employee_id = fields.Many2one('hr.employee', string='Employee',tracking=True,required=True) + year_id = fields.Many2one(comodel_name='kpi.period',string='Year') + category_id = fields.Many2one(comodel_name='kpi.category',string='Category') + kpi_id = fields.Many2one(comodel_name='kpi.item',string='KPI',) + method_of_calculate = fields.Selection(related='kpi_id.method_of_calculate') + responsible_item_id = fields.Many2one(comodel_name='hr.employee',related='kpi_id.responsible_item_id',store=True,string='Responsible') + user_id = fields.Many2one(comodel_name='res.users',related='responsible_item_id.user_id',store=True,string='Responsible') + department_id = fields.Many2one('hr.department',readonly=True,store=True,compute='compute_depart_job', tracking=True,string='Department') + job_id = fields.Many2one('hr.job',readonly=True,store=True,compute='compute_depart_job', string='Job Title',tracking=True,) + year_target = fields.Float(string='Year Target') + weight = fields.Float(string='Weight') + goals_period_ids = fields.One2many(comodel_name='period.goals',inverse_name='employee_goals_id',string='Period',copy=False) + done = fields.Float(string='Done',store=True,compute='total_done') + state = fields.Selection([('draft', 'Draft'),('apprisal', 'Apprisal'),('close', 'Close')], string='State',tracking=True,default='draft') + choiec = fields.Integer(string='Choiec',store=True,compute='compute_choice') + employee_apprisal_id = fields.Many2one(comodel_name='hr.employee.appraisal') + first_period_traget = fields.Float(compute='_compute_first_period_traget', string='First Period Traget', + inverse='_inverse_first_period_traget') + second_period_traget = fields.Float(compute='_compute_second_period_traget', string='Second Period Traget', + inverse='_inverse_second_period_traget') + third_period_traget = fields.Float(compute='_compute_third_period_traget', string='Third Period Traget', + inverse='_inverse_third_period_traget') + fourth_period_traget = fields.Float(compute='_compute_fourth_period_traget', string='Fourth Period Traget', + inverse='_inverse_fourth_period_traget') + + def _compute_first_period_traget(self): + for rec in self: + rec.first_period_traget = 0.0 + first_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '1') + if first_period: + rec.first_period_traget = first_period.target + + def _inverse_first_period_traget(self): + for rec in self: + first_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '1') + if first_period: + first_period.sudo().target = rec.first_period_traget + else: + if rec.year_id: + first_period = rec.year_id.kpi_goals_periods_ids.filtered(lambda period: period.sequence == '1') + if first_period: + rec.goals_period_ids = [(0, 0, {'period_goals_id':first_period.id,'target':rec.first_period_traget})] + + + def _compute_second_period_traget(self): + for rec in self: + rec.second_period_traget = 0.0 + second_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '2') + if second_period: + rec.second_period_traget = second_period.target + + def _inverse_second_period_traget(self): + for rec in self: + second_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '2') + if second_period: + second_period.sudo().target = rec.second_period_traget + else: + if rec.year_id: + second_period = rec.year_id.kpi_goals_periods_ids.filtered(lambda period: period.sequence == '2') + if second_period: + rec.goals_period_ids = [(0, 0, {'period_goals_id':second_period.id,'target':rec.second_period_traget})] + + def _compute_third_period_traget(self): + for rec in self: + rec.third_period_traget = 0.0 + third_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '3') + if third_period: + rec.third_period_traget = third_period.target + + def _inverse_third_period_traget(self): + for rec in self: + third_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '3') + if third_period: + third_period.sudo().target = rec.third_period_traget + else: + if rec.year_id: + third_period = rec.year_id.kpi_goals_periods_ids.filtered(lambda period: period.sequence == '3') + if third_period: + rec.goals_period_ids = [(0, 0, {'period_goals_id':third_period.id,'target':rec.third_period_traget})] + + def _compute_fourth_period_traget(self): + for rec in self: + rec.fourth_period_traget = 0.0 + fourth_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '4') + if fourth_period: + rec.fourth_period_traget = fourth_period.target + + def _inverse_fourth_period_traget(self): + for rec in self: + fourth_period = rec.goals_period_ids.filtered(lambda period: period.period_goals_id.sequence == '4') + if fourth_period: + fourth_period.sudo().target = rec.fourth_period_traget + else: + if rec.year_id: + fourth_period = rec.year_id.kpi_goals_periods_ids.filtered(lambda period: period.sequence == '4') + if fourth_period: + rec.goals_period_ids = [(0, 0, {'period_goals_id':fourth_period.id,'target':rec.fourth_period_traget})] + + @api.model + def search(self, args, offset=0, limit=None, order=None, count=False): + # add domain filter to only show records related to login responsible_item_id employee + if self.env.user.has_group("exp_hr_appraisal_kpi.group_appraisal_responsabil") and not self.env.user.has_group("exp_hr_appraisal.group_appraisal_manager") and not self.env.user.has_group("exp_hr_appraisal.group_appraisal_user") : + args += [('user_id','=',self.env.user.id)] + return super (YearEmployeeGoals,self).search(args,offset,limit,order,count) + + @api.depends('goals_period_ids.done','goals_period_ids.target','method_of_calculate') + def total_done(self): + for rec in self: + if rec.method_of_calculate=='accumulative': + sum=0 + for record in rec.goals_period_ids: + sum = sum+record.done + + rec.done = sum + elif rec.method_of_calculate=='avrerage': + sum=0 + for record in rec.goals_period_ids: + sum = (sum+record.done) + rec.done = sum/len(rec.goals_period_ids) + else: + rec.done=0.0 + + + @api.depends('goals_period_ids.done','done','goals_period_ids.target','method_of_calculate') + def compute_choice(self): + for rec in self: + choice = 0 + if rec.done!=0.0 and rec.year_target!=0.0 and rec.kpi_id: + done_percentage = (rec.done / rec.year_target) * 100 + marks = self.env['mark.mark'].search([('kip_id', '=', rec.kpi_id.id),('target','<=',done_percentage),('to','>=',done_percentage)],limit=1) + if marks: + choice = marks.choiec + rec.choiec = int(choice) + + def apprisal(self): + self.state='apprisal' + + def action_close(self): + self.state='close' + + def action_set_to_dratt(self): + self.state='draft' + + @api.constrains('employee_id', 'year_id', 'kpi_id') + def check_unique_employee_year_period_goals(self): + for record in self: + if self.search_count([ + ('employee_id', '=', record.employee_id.id), + ('year_id', '=', record.year_id.id), + ('kpi_id', '=', record.kpi_id.id), + ('id', '!=', record.id), + ]) > 0: + raise exceptions.ValidationError(_("Employee Goals must be unique per Employee, Year, and kpi!")) + + @api.depends('employee_id') + def compute_depart_job(self): + for rec in self: + if rec.employee_id: + rec.department_id = rec.employee_id.department_id.id + rec.job_id = rec.employee_id.job_id.id + + @api.onchange('year_id') + def onchange_emp(self): + goals_lines=[(5,0,0)] + if self.year_id: + for line in self.year_id.kpi_goals_periods_ids: + line_item = {'period_goals_id':line.id} + goals_lines.append((0,0,line_item)) + self.goals_period_ids = goals_lines diff --git a/exp_hr_appraisal_kpi/security/group.xml b/exp_hr_appraisal_kpi/security/group.xml new file mode 100644 index 0000000..e47eec5 --- /dev/null +++ b/exp_hr_appraisal_kpi/security/group.xml @@ -0,0 +1,95 @@ + + + + + Menu apprisal hide/show + + + + Goals Responsible + + + + + + Extended KPI Category Rule + + [ + '|', + ('company_id','=', False), + ('company_id', 'in', company_ids), + ] + + + + + Extended KPI Category Rule + + [ + '|', + ('company_id','=', False), + ('company_id', 'in', company_ids), + ] + + + + + + Employee: views its Skill appraisals only + + [('employee_id.user_id','=',user.id)] + + + + + Employee: views its Goal appraisals only + + [('employee_id.user_id','=',user.id)] + + + + + Manager: views Skill appraisals of its subordinates + + ['|','|',('employee_id.department_id.manager_id','=',False), + ('employee_id.department_id.manager_id.user_id','in', [user.id]), + ('employee_id.department_id.parent_id.manager_id.user_id','in', [user.id])] + + + + + Manager: views Goals appraisals of its subordinates + + ['|','|',('employee_id.department_id.manager_id','=',False), + ('employee_id.department_id.manager_id.user_id','in', [user.id]), + ('employee_id.department_id.parent_id.manager_id.user_id','in', [user.id])] + + + + + Manager: views Skills appraisals of all subordinates + + [(1 ,'=', 1)] + + + + + Manager: views Goals appraisals of all subordinates + + [(1 ,'=', 1)] + + + + + + \ No newline at end of file diff --git a/exp_hr_appraisal_kpi/security/ir.model.access.csv b/exp_hr_appraisal_kpi/security/ir.model.access.csv new file mode 100644 index 0000000..bf1cef1 --- /dev/null +++ b/exp_hr_appraisal_kpi/security/ir.model.access.csv @@ -0,0 +1,71 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_mark2,access_marks2,model_mark_mark,base.group_user,1,1,1,1 + +access_mark3,access_marks3,model_mark_mark,exp_hr_appraisal.group_appraisal_employee,1,1,1,1 +access_mark4,access_marks4,model_mark_mark,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_mark5,access_marks5,model_mark_mark,exp_hr_appraisal.group_appraisal_user,1,1,1,1 + +access_kpi_p7,access_kpi_ps8,model_kpi_period_notes,base.group_user,1,1,1,1 +access_kpi_p7,access_kpi_ps8,model_kpi_period_notes,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_p7,access_kpi_ps8,model_kpi_period_notes,exp_hr_appraisal.group_appraisal_user,1,1,1,1 +access_kpi_p7,access_kpi_ps8,model_kpi_period_notes,exp_hr_appraisal.group_appraisal_employee,1,1,1,1 + +access_kpi_p535,access_kpi_ps185,model_skill_skill,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_p535,access_kpi_ps185,model_skill_skill,exp_hr_appraisal.group_appraisal_user,1,1,1,1 + + +access_kpi_p54435,access_kpi_ps18555,model_skill_item,base.group_user,1,1,1,1 +access_kpi_p544355,access_kpi_ps189555,model_skill_item_table,base.group_user,1,1,1,1 +access_kpi_p544385,access_kpi_ps179555,model_skill_item_employee_table,base.group_user,1,1,1,1 + + + +access_kpi_p53577,access_kpi_ps17785,model_item_item,base.group_user,1,1,1,1 +access_kpi_p53577,access_kpi_ps17785,model_item_item,exp_hr_appraisal.group_appraisal_user,1,1,1,1 +access_kpi_p53577,access_kpi_ps17785,model_item_item,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_p53577,access_kpi_ps17785,model_item_item,exp_hr_appraisal.group_appraisal_employee,1,1,1,1 + +access_kpi_p5357b7p,access_kpbi_ps17785p,model_skill_appraisal,base.group_user,1,1,1,1 +access_kpi_p5357b7,access_kpbi_ps17785,model_skill_appraisal,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_p5357b7,access_kpbi_ps17785,model_skill_appraisal,exp_hr_appraisal.group_appraisal_user,1,1,1,1 +access_kpi_p5357b7,access_kpbi_ps17785,model_skill_appraisal,exp_hr_appraisal.group_appraisal_employee,1,0,0,1 + +access_kpi_p5357b7d,access_kpbi_ps1778d5,model_skill_appraisal,hr_base.group_division_manager,1,1,1,1 +access_kpi_p535d7b7,access_kpbi_ps17d785,model_skill_appraisal,hr_base.group_department_manager,1,1,1,1 +access_kpi_p5357bd7,access_kpbi_ps1778d5,model_skill_appraisal,hr.group_hr_user,1,1,1,0 + +access_kpi_emp_performansel1,access_kpbi_emp_perfomance_evalution39,model_employee_performance_evaluation,base.group_user,1,1,1,0 +access_kpi_emp_performanse1,access_kpbi_emp_perfomance_evalution3,model_employee_performance_evaluation,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_emp_performanse1,access_kpbi_emp_perfomance_evalution3,model_employee_performance_evaluation,exp_hr_appraisal.group_appraisal_user,1,1,1,1 +access_kpi_emp_performanse1,access_kpbi_emp_perfomance_evalution3,model_employee_performance_evaluation,exp_hr_appraisal.group_appraisal_employee,1,0,0,1 + +access_kpi_emp_performanse11,access_kpbi_emp_perfomance_evalution33,model_employee_performance_evaluation,hr_base.group_division_manager,1,1,1,1 +access_kpi_emp_performanse12,access_kpbi_emp_perfomance_evalutionr3,model_employee_performance_evaluation,hr_base.group_department_manager,1,1,1,1 +access_kpi_emp_performanse13,access_kpbi_emp_perfomance_evalution53,model_employee_performance_evaluation,hr.group_hr_user,1,1,1,0 + +access_kpi_emp_goals_res,access_kpbi_emp_goals1_res,model_years_employee_goals,exp_hr_appraisal_kpi.group_appraisal_responsabil,1,1,1,0 +access_kpi_emp_goals11,access_kpbi_emp_goals1,model_years_employee_goals,base.group_user,1,1,1,0 +access_kpi_emp_goals1,access_kpbi_emp_goals11,model_years_employee_goals,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_emp_goals5,access_kpbi_emp_goals151,model_years_employee_goals,exp_hr_appraisal.group_appraisal_user,1,1,1,0 +access_kpi_emp_goals,access_kpbi_emp_goals1,model_years_employee_goals,hr_base.group_department_manager,1,1,0,0 + +access_kpi_emp_goals_period,access_kpbi_emp_period1,model_period_goals,base.group_user,1,1,1,1 +access_kpi_perecentage,access_kpbi_perecentage1,model_job_class_apprisal,base.group_user,1,1,1,1 + +access_kpi_category,access_kpi_category,kpi_scorecard.model_kpi_category,base.group_user,1,1,1,1 +access_kpi_category,access_kpbi_category1,kpi_scorecard.model_kpi_category,kpi_scorecard.group_kpi_admin,1,1,1,1 +access_kpi_category,access_kpbi_category1,kpi_scorecard.model_kpi_category,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_category1,access_kpbi_category2,kpi_scorecard.model_kpi_category,exp_hr_appraisal.group_appraisal_user,1,1,1,0 +access_kpi_category21,access_kpbi_category25,kpi_scorecard.model_kpi_category,exp_hr_appraisal.group_appraisal_employee,1,0,0,0 + +access_kpi_period,access_kpbi_period1,kpi_scorecard.model_kpi_period,base.group_user,1,0,0,0 +access_kpi_period,access_kpbi_period1,kpi_scorecard.model_kpi_period,kpi_scorecard.group_kpi_admin,1,1,1,1 +access_kpi_period,access_kpbi_period1,kpi_scorecard.model_kpi_period,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_category1,access_kpbi_period2,kpi_scorecard.model_kpi_period,exp_hr_appraisal.group_appraisal_user,1,1,1,0 +access_kpi_category21,access_kpbi_period25,kpi_scorecard.model_kpi_period,exp_hr_appraisal.group_appraisal_employee,1,0,0,0 + +access_kpi_item,access_kpbi_item1,kpi_scorecard.model_kpi_item,base.group_user,1,0,0,0 +access_kpi_item,access_kpbi_item1,kpi_scorecard.model_kpi_item,kpi_scorecard.group_kpi_admin,1,1,1,1 +access_kpi_item,access_kpbi_item1,kpi_scorecard.model_kpi_item,exp_hr_appraisal.group_appraisal_manager,1,1,1,1 +access_kpi_item1,access_item2,kpi_scorecard.model_kpi_item,exp_hr_appraisal.group_appraisal_user,1,1,1,0 +access_kpi_item21,access_kpbi_item25,kpi_scorecard.model_kpi_item,exp_hr_appraisal.group_appraisal_employee,1,0,0,0 diff --git a/exp_hr_appraisal_kpi/static/description/icon.png b/exp_hr_appraisal_kpi/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..4141f52daab6a780510b5f4e3dd762511add78fa GIT binary patch literal 32929 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 OgtWK + + + + apprisal.apprisal_percentag.form + job.class.apprisal + +
+ + + + + + + + + + + +
+
+
+ + + apprisal.apprisal_percentag.tree + job.class.apprisal + + + + + + + + + + + + + + + + + + + + + + + + Apprisal Percentag + ir.actions.act_window + job.class.apprisal + list,form + +

+ There is no examples click here to add new ModelTitle. +

+
+
+ + +
+
\ No newline at end of file diff --git a/exp_hr_appraisal_kpi/views/employee_apprisal.xml b/exp_hr_appraisal_kpi/views/employee_apprisal.xml new file mode 100644 index 0000000..e0297bb --- /dev/null +++ b/exp_hr_appraisal_kpi/views/employee_apprisal.xml @@ -0,0 +1,223 @@ + + + + + + employee.apprisal.form.extend + hr.employee.appraisal + + + + + + + + + + + + + + 1 + + + + 1 + + + 1 + + + + + + + + 1 + + + 1 + + + + + + 1 + + + 1 + + + 1 + + + 1 + + + 1 + + + + + + + + + + + + + +
+ +
+
+ +
+
+
+
+
diff --git a/hr_linkedin_recruitment/views/linkedin_comments_views.xml b/hr_linkedin_recruitment/views/linkedin_comments_views.xml new file mode 100644 index 0000000..c0082a6 --- /dev/null +++ b/hr_linkedin_recruitment/views/linkedin_comments_views.xml @@ -0,0 +1,21 @@ + + + + + linkedin.comments.view.tree + linkedin.comments + + + + + + + + + + Post Comments + linkedin.comments + list + + diff --git a/hr_linkedin_recruitment/views/oauth_views.xml b/hr_linkedin_recruitment/views/oauth_views.xml new file mode 100644 index 0000000..b126b3c --- /dev/null +++ b/hr_linkedin_recruitment/views/oauth_views.xml @@ -0,0 +1,32 @@ + + + + + + + + + + auth.oauth.provider.view.form.inherit.hr.linkedin.recruitment + + auth.oauth.provider + + + + + + + + diff --git a/hr_linkedin_recruitment/views/recruitment_config_settings.xml b/hr_linkedin_recruitment/views/recruitment_config_settings.xml new file mode 100644 index 0000000..a3de3e3 --- /dev/null +++ b/hr_linkedin_recruitment/views/recruitment_config_settings.xml @@ -0,0 +1,30 @@ + + + + + + + res.config.settings.view.form.inherit.hr.linkedin.recruitment + + res.config.settings + + + +

LinkedIn Credentials

+
+
+ + + + +
+
+
+
+
+
+
diff --git a/hr_multicompany_employee_number/__init__.py b/hr_multicompany_employee_number/__init__.py new file mode 100644 index 0000000..a0fdc10 --- /dev/null +++ b/hr_multicompany_employee_number/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models diff --git a/hr_multicompany_employee_number/__manifest__.py b/hr_multicompany_employee_number/__manifest__.py new file mode 100644 index 0000000..ec7d398 --- /dev/null +++ b/hr_multicompany_employee_number/__manifest__.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +{ + 'name': 'HR Multi-Company Employee Number', + 'version': '18.0.1.0.0', + 'category': 'Human Resources', + 'summary': 'Fix employee number generation for multi-company setup', + 'description': """ + This module fixes the employee number generation issue when creating employees + from res.users in a multi-company environment. + """, + 'author': 'Custom Development', + 'depends': ['hr_base', 'hr'], + 'data': [ + 'security/ir.model.access.csv', + 'data/ir_sequence_data.xml', + ], + 'installable': True, + 'application': False, + 'auto_install': False, +} diff --git a/hr_multicompany_employee_number/data/ir_sequence_data.xml b/hr_multicompany_employee_number/data/ir_sequence_data.xml new file mode 100644 index 0000000..a49c6c1 --- /dev/null +++ b/hr_multicompany_employee_number/data/ir_sequence_data.xml @@ -0,0 +1,16 @@ + + + + + + Global Employee Number + hr.employee.global + EMP- + 4 + 1 + 1 + + + + + diff --git a/hr_multicompany_employee_number/models/__init__.py b/hr_multicompany_employee_number/models/__init__.py new file mode 100644 index 0000000..62e0430 --- /dev/null +++ b/hr_multicompany_employee_number/models/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import hr_employee diff --git a/hr_multicompany_employee_number/models/hr_employee.py b/hr_multicompany_employee_number/models/hr_employee.py new file mode 100644 index 0000000..8a34f51 --- /dev/null +++ b/hr_multicompany_employee_number/models/hr_employee.py @@ -0,0 +1,83 @@ +# -*- coding: utf-8 -*- +from datetime import date +from odoo import api, fields, models, _ +from odoo.exceptions import ValidationError + + +class HrEmployee(models.Model): + _inherit = "hr.employee" + + @api.model + def _default_emp_code(self): + seq = self.env['ir.sequence'].next_by_code('hr.employee.global') + if not seq: + # If sequence doesn't exist, create it + self._create_global_sequence() + seq = self.env['ir.sequence'].next_by_code('hr.employee.global') + return seq or '/' + + @api.model + def _create_global_sequence(self): + existing_sequence = self.env['ir.sequence'].search([ + ('code', '=', 'hr.employee.global') + ], limit=1) + + if not existing_sequence: + # Find the maximum employee number in the system + all_employees = self.env['hr.employee'].search([ + ('active', 'in', [False, True]) + ]) + + max_number = 0 + for emp in all_employees: + if emp.emp_no and emp.emp_no.startswith('EMP-'): + try: + number_part = emp.emp_no.replace('EMP-', '') + if number_part.isdigit(): + max_number = max(max_number, int(number_part)) + except (ValueError, AttributeError): + continue + + self.env['ir.sequence'].sudo().create({ + 'name': 'Global Employee Number', + 'code': 'hr.employee.global', + 'implementation': 'standard', + 'prefix': 'EMP-', + 'padding': 4, + 'number_increment': 1, + 'number_next_actual': max_number + 1, + 'company_id': False, + }) + + @api.model + def create(self, vals): + + if not vals.get('emp_no'): + vals['emp_no'] = self._default_emp_code() + + if 'company_id' not in vals: + vals['company_id'] = self.env.context.get('default_company_id') or self.env.company.id + + return super(HrEmployee, self).create(vals) + + @api.constrains("emp_no", "birthday", "attachment_ids") + def e_unique_field_name_constrains(self): + for rec in self: + # Check employee number uniqueness globally + if rec.emp_no and rec.emp_no != '/': + duplicate = self.search([ + ("emp_no", "=", rec.emp_no), + ("id", "!=", rec.id) + ], limit=1) + if duplicate: + raise ValidationError( + _("You cannot create Employee with the same employee number") + ) + + if rec.birthday and isinstance(rec.birthday, date) and rec.birthday >= date.today(): + raise ValidationError(_("Sorry, The Birthday Must Be Less than Date Today")) + + if rec.attachment_ids: + for att in rec.attachment_ids: + if not att.doc_name: + raise ValidationError(_('Attach the attachment to the Document %s') % att.name) diff --git a/hr_multicompany_employee_number/security/ir.model.access.csv b/hr_multicompany_employee_number/security/ir.model.access.csv new file mode 100644 index 0000000..f0dd6ce --- /dev/null +++ b/hr_multicompany_employee_number/security/ir.model.access.csv @@ -0,0 +1,2 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_hr_employee_multicompany,hr.employee.multicompany,hr.model_hr_employee,base.group_user,1,1,1,1 diff --git a/hr_training_payment/__init__.py b/hr_training_payment/__init__.py new file mode 100644 index 0000000..f5ba686 --- /dev/null +++ b/hr_training_payment/__init__.py @@ -0,0 +1,2 @@ +# -*- coding: utf-8 -*- +from . import models \ No newline at end of file diff --git a/hr_training_payment/__manifest__.py b/hr_training_payment/__manifest__.py new file mode 100644 index 0000000..d56e8bc --- /dev/null +++ b/hr_training_payment/__manifest__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +{ + 'name': "HR Training Payment", + + 'summary': """ + HR Training Payment + """, + + 'description': """ + HR Training Payment + """, + 'category': 'Odex25-HR/Odex25-HR', + 'version': '18.0.1.0.0', + 'sequence': 6, + 'website': 'http://exp-sa.com', + 'license': 'GPL-3', + 'author': 'Expert Co. Ltd.', + # 'depends': ['exp_official_mission','purchase_requisition_custom'], + 'depends': ['exp_official_mission'], + + 'data': [ + "views/hr_official_mission.xml", + "views/mission_type.xml", + + ], + +} diff --git a/hr_training_payment/i18n/ar_001.po b/hr_training_payment/i18n/ar_001.po new file mode 100644 index 0000000..94757e2 --- /dev/null +++ b/hr_training_payment/i18n/ar_001.po @@ -0,0 +1,62 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * hr_training_payment +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2025-02-06 18:03+0000\n" +"PO-Revision-Date: 2025-02-06 18:03+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: hr_training_payment +#: code:addons/hr_training_payment/models/hr_official_mission.py:0 +#, python-format +msgid "" +"Employee \"%s\" has no contract Please create contract to add line to " +"advantages" +msgstr "" + +#. module: hr_training_payment +#: model:ir.model,name:hr_training_payment.model_hr_official_mission +msgid "Official mission" +msgstr "" + +#. module: hr_training_payment +#: model:ir.model.fields,field_description:hr_training_payment.field_hr_official_mission_type__pr_product_id +msgid "PR Product" +msgstr "منتج طلب الشراء" + +#. module: hr_training_payment +#: model:ir.model.fields,field_description:hr_training_payment.field_hr_official_mission__purchase_request_id +msgid "Purchase Request" +msgstr "طلب الشراء" + +#. module: hr_training_payment +#: code:addons/hr_training_payment/models/hr_official_mission.py:0 +#, python-format +msgid "You do not have account or journal in mission type \"%s\" " +msgstr "" + +#. module: hr_training_payment +#: model:ir.model,name:hr_training_payment.model_hr_official_mission_type +msgid "hr.official.mission.type" +msgstr "" + +#. module: hr_training_payment +#: code:addons/hr_training_payment/models/hr_official_mission.py:0 +#, python-format +msgid "You must Enter Purchase Product in Training Type Configuration" +msgstr "يجب ادخال منتج طلب الشراء في إعداد نوع المهام" + +#. module: hr_training_payment +#: code:addons/hr_training_payment/models/hr_official_mission.py:0 +#, python-format +msgid "Training Cost Must be Bigger than Zero" +msgstr "تكلفة مركز التدريب يجب ان تكون اكبر من صفر" diff --git a/hr_training_payment/models/__init__.py b/hr_training_payment/models/__init__.py new file mode 100644 index 0000000..59af57a --- /dev/null +++ b/hr_training_payment/models/__init__.py @@ -0,0 +1,4 @@ +# -*- coding: utf-8 -*- + +from . import hr_official_mission +from . import mission_type \ No newline at end of file diff --git a/hr_training_payment/models/hr_official_mission.py b/hr_training_payment/models/hr_official_mission.py new file mode 100644 index 0000000..c9aca82 --- /dev/null +++ b/hr_training_payment/models/hr_official_mission.py @@ -0,0 +1,135 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api, exceptions,_ +from datetime import datetime, date, timedelta +from odoo.exceptions import ValidationError +import calendar + + +class HrOfficialMission(models.Model): + _inherit = 'hr.official.mission' + + purchase_request_id = fields.Many2one(comodel_name='purchase.request', string="Purchase Request") + + def approve(self): + # check if there is dealing with financial + self.employee_ids.chick_not_overtime() + if self.employee_ids and self.mission_type.related_with_financial: + # move amounts to journal entries + if self.move_type == 'accounting': + if self.mission_type.account_id and self.mission_type.journal_id: + for item in self.employee_ids: + if item.amount > 0.0: + debit_line_vals = { + 'name': item.employee_id.name + ' in official mission "%s" ' % self.mission_type.name, + 'debit': item.amount, + 'account_id': self.mission_type.account_id.id, + 'partner_id': item.employee_id.user_id.partner_id.id + } + credit_line_vals = { + 'name': item.employee_id.name + ' in official mission "%s" ' % self.mission_type.name, + 'credit': item.amount, + 'account_id': self.mission_type.journal_id.default_account_id.id, + 'partner_id': item.employee_id.user_id.partner_id.id + } + if not item.account_move_id: + move = self.env['account.move'].create({ + 'state': 'draft', + 'journal_id': self.mission_type.journal_id.id, + 'date': date.today(), + 'ref': 'Official mission for employee "%s" ' % item.employee_id.name, + 'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)], + 'res_model': 'hr.official.mission', + 'res_id': self.id + }) + # fill account move for each employee + item.write({'account_move_id': move.id}) + else: + raise exceptions.Warning( + _('You do not have account or journal in mission type "%s" ') % self.mission_type.name) + + # move amounts to advantages of employee in contract + elif self.move_type == 'payroll': + # get start and end date of the current month + current_date = date.today() + month_start = date(current_date.year, current_date.month, 1) + month_end = date(current_date.year, current_date.month, calendar.mdays[current_date.month]) + for line in self.employee_ids: + if line.sudo().employee_id.contract_id: + + advantage_arc = line.env['contract.advantage'].create({ + 'benefits_discounts': self.official_mission.id, + 'date_from': month_start, + 'date_to': month_end, + 'amount': line.amount, + 'official_mission_id': True, + 'employee_id': line.employee_id.id, + 'contract_advantage_id': line.sudo().employee_id.contract_id.id, + 'out_rule': True, + 'state': 'confirm', + 'comments': self.mission_purpose}) + line.advantage_id = advantage_arc.id + else: + raise exceptions.Warning(_( + 'Employee "%s" has no contract Please create contract to add line to advantages') + % line.employee_id.name) + + for item in self: + # create ticket request from all employee + if item.issuing_ticket == 'yes': + for emp in item.employee_ids: + ticket = self.env['hr.ticket.request'].create({ + 'employee_id': emp.employee_id.id, + 'mission_request_id': item.id, + 'mission_check': True, + 'request_for': item.ticket_cash_request_for, + 'request_type': item.ticket_cash_request_type.id, + 'cost_of_tickets': item.get_ticket_cost(emp.employee_id), + 'destination': item.destination.id, + }) + item.write({'ticket_request_id': ticket.id}) + + # move invoice training cost our trining center + if item.Training_cost > 0: + if not self.mission_type.pr_product_id.id: + raise ValidationError(_("You must Enter Purchase Product in Training Type Configuration")) + + product_line = { + 'product_id': self.mission_type.pr_product_id.id, + 'qty': 1, + 'expected_price': self.Training_cost, + } + + purchase_request = self.env['purchase.request'].create({ + 'state': 'draft', + 'department_id': self.department_id2.id, + 'date': date.today(), + 'employee_id': self.employee_id.id, + 'partner_id': self.partner_id.id, + 'product_category_ids': [(4, self.mission_type.pr_product_id.categ_id.id)], + 'purchase_purpose': self.training_details, + 'line_ids': [(0, 0, product_line)] + }) + + self.purchase_request_id = purchase_request.id + + self.state = "approve" + if self.mission_type.work_state and self.mission_type.duration_type == 'days': + for emp in self.employee_ids: + if emp.date_to >= fields.Date.today() >= emp.date_from: + emp.employee_id.write({'work_state': self.mission_type.work_state, 'active_mission_id': emp.id}) + self.call_cron_function() + + + + + + def draft_state(self): + res = super(HrOfficialMission, self).draft_state() + if self.purchase_request_id: + self.purchase_request_id.sudo().unlink() + + return res + + + diff --git a/hr_training_payment/models/mission_type.py b/hr_training_payment/models/mission_type.py new file mode 100644 index 0000000..ee23287 --- /dev/null +++ b/hr_training_payment/models/mission_type.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- + +from odoo import models, fields, api + + +class HrOfficialMissionType(models.Model): + _inherit = 'hr.official.mission.type' + pr_product_id = fields.Many2one(comodel_name='product.product', string="PR Product") + diff --git a/hr_training_payment/views/hr_official_mission.xml b/hr_training_payment/views/hr_official_mission.xml new file mode 100644 index 0000000..b2b3a1d --- /dev/null +++ b/hr_training_payment/views/hr_official_mission.xml @@ -0,0 +1,23 @@ + + + + + + + hr.official.mission.view.form + hr.official.mission + + + + + + + + True + + + + + + + diff --git a/hr_training_payment/views/mission_type.xml b/hr_training_payment/views/mission_type.xml new file mode 100644 index 0000000..036f4e2 --- /dev/null +++ b/hr_training_payment/views/mission_type.xml @@ -0,0 +1,19 @@ + + + + + + + hr.official.mission.type.view.form + hr.official.mission.type + + + + + + + + + + + diff --git a/to_attendance_system/__init__.py b/to_attendance_system/__init__.py new file mode 100644 index 0000000..bbc5580 --- /dev/null +++ b/to_attendance_system/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- + +from . import controllers +from . import models +from . import wizard diff --git a/to_attendance_system/__manifest__.py b/to_attendance_system/__manifest__.py new file mode 100644 index 0000000..661ac88 --- /dev/null +++ b/to_attendance_system/__manifest__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +{ + 'name': "Attendance system", + + 'summary': """ + Short (1 phrase/line) summary of the module's purpose, used as + subtitle on modules listing or apps.openerp.com""", + + 'description': """ + Long description of module's purpose + """, + + 'author': "Expert", + 'website': "http://www.exp-sa.com", + + 'category':'Odex25-HR/Odex25-HR', + 'version': '18.0.0.1', + + # any module necessary for this one to work correctly + 'depends': ['base'], + + # always loaded + 'data': [ + 'security/ir.model.access.csv', + 'views/views.xml', + # 'wizard/attendaces_wizard.xml', + 'data/scheduler_data.xml', + ] +} diff --git a/to_attendance_system/controllers/__init__.py b/to_attendance_system/controllers/__init__.py new file mode 100644 index 0000000..b0f26a9 --- /dev/null +++ b/to_attendance_system/controllers/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import controllers diff --git a/to_attendance_system/controllers/controllers.py b/to_attendance_system/controllers/controllers.py new file mode 100644 index 0000000..098fa26 --- /dev/null +++ b/to_attendance_system/controllers/controllers.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +from odoo import http + +# class ToAttendanceSystem(http.Controller): +# @http.route('/to_attendance_system/to_attendance_system/', auth='public') +# def index(self, **kw): +# return "Hello, world" + +# @http.route('/to_attendance_system/to_attendance_system/objects/', auth='public') +# def list(self, **kw): +# return http.request.render('to_attendance_system.listing', { +# 'root': '/to_attendance_system/to_attendance_system', +# 'objects': http.request.env['to_attendance_system.to_attendance_system'].search([]), +# }) + +# @http.route('/to_attendance_system/to_attendance_system/objects//', auth='public') +# def object(self, obj, **kw): +# return http.request.render('to_attendance_system.object', { +# 'object': obj +# }) diff --git a/to_attendance_system/data/scheduler_data.xml b/to_attendance_system/data/scheduler_data.xml new file mode 100644 index 0000000..23ac98a --- /dev/null +++ b/to_attendance_system/data/scheduler_data.xml @@ -0,0 +1,25 @@ + + + + + Synchronize attendances scheduler + + + code + 30 + minutes + model.cron_sync_attendance() + + + + Download attendances scheduler + + + code + 30 + minutes + model.download_system_attendance() + + + + \ No newline at end of file diff --git a/to_attendance_system/demo/demo.xml b/to_attendance_system/demo/demo.xml new file mode 100644 index 0000000..abe08c3 --- /dev/null +++ b/to_attendance_system/demo/demo.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/to_attendance_system/i18n/ar_001.po b/to_attendance_system/i18n/ar_001.po new file mode 100644 index 0000000..b34b780 --- /dev/null +++ b/to_attendance_system/i18n/ar_001.po @@ -0,0 +1,567 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * to_attendance_system +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2022-09-19 10:31+0000\n" +"PO-Revision-Date: 2022-09-19 10:31+0000\n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__alias +msgid "Alias" +msgstr "الاسم المستعار" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__area_alias +msgid "Area" +msgstr "المنطقة" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__area_id +msgid "Area id" +msgstr "معرف المنطقة" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__area_name +msgid "Area name" +msgstr "اسم المنطقة" + +#. module: to_attendance_system +#: model:ir.ui.menu,name:to_attendance_system.attendance_systems +msgid "Attendance Systems" +msgstr "أنظمة الحضور والانصراف" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_attendance_wizard +msgid "Attendance Wizard" +msgstr "معالج الحضور" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__attendance_id +msgid "Attendance id" +msgstr "معرف الحضور" + +#. module: to_attendance_system +#: model:ir.ui.menu,name:to_attendance_system.attendace_system_menu +msgid "Attendance systems" +msgstr "أنظمة الحضور" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__attendance_ids +msgid "Attendances" +msgstr "سجلات الحضور" + +#. module: to_attendance_system +#: code:addons/to_attendance_system/models/models.py:0 +#: model:ir.model.fields.selection,name:to_attendance_system.selection__finger_biotime_api__state__authentic +#, python-format +msgid "Authentic" +msgstr "مصادق عليه" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__crc +msgid "CRC" +msgstr "كود التحقق" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__code +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__code +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__code +msgid "Code" +msgstr "الكود" + +#. module: to_attendance_system +#: code:addons/to_attendance_system/models/models.py:0 +#, python-format +msgid "Confirmed" +msgstr "مؤكد" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__create_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard__create_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__create_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__create_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__create_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__create_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__create_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__create_uid +msgid "Created by" +msgstr "أنشئ بواسطة" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__create_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard__create_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__create_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__create_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__create_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__create_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__create_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__create_date +msgid "Created on" +msgstr "تاريخ الإنشاء" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__department_id +msgid "Department id" +msgstr "معرف القسم" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__description +msgid "Description" +msgstr "الوصف" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__display_name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard__display_name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__display_name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__display_name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__display_name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__display_name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__display_name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__display_name +msgid "Display Name" +msgstr "الاسم المعروض" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Download areas" +msgstr "تنزيل المناطق" + +#. module: to_attendance_system +#: model:ir.actions.server,name:to_attendance_system.ir_cron_scheduler_download_system_attendance_ir_actions_server +#: model:ir.cron,cron_name:to_attendance_system.ir_cron_scheduler_download_system_attendance +#: model:ir.cron,name:to_attendance_system.ir_cron_scheduler_download_system_attendance +msgid "Download attendances scheduler" +msgstr "جدولة تنزيل سجلات الحضور" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Download attenence" +msgstr "تنزيل الحضور" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Download departments" +msgstr "تنزيل الأقسام" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Download employees" +msgstr "تنزيل الموظفين" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Download postions" +msgstr "تنزيل الوظائف" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Download terminals" +msgstr "تنزيل أجهزة البصمة" + +#. module: to_attendance_system +#: code:addons/to_attendance_system/models/models.py:0 +#, python-format +msgid "Draft" +msgstr "مسودة" + +#. module: to_attendance_system +#: model:ir.actions.act_window,name:to_attendance_system.biotime_system_employee_position_action +#: model:ir.ui.menu,name:to_attendance_system.attendance_systems_employee_position +msgid "Employee Positions" +msgstr "وظائف الموظفين" + +#. module: to_attendance_system +#: model:ir.actions.act_window,name:to_attendance_system.biotime_system_employee_areas_action +msgid "Employee areas" +msgstr "مناطق الموظفين" + +#. module: to_attendance_system +#: model:ir.actions.act_window,name:to_attendance_system.biotime_system_employee_department_action +msgid "Employee departments" +msgstr "أقسام الموظفين" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__emp_system_id +msgid "Employee id" +msgstr "معرف الموظف" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__employee_ids +#: model:ir.ui.menu,name:to_attendance_system.attendance_systems_employee +msgid "Employees" +msgstr "الموظفون" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__hr_employee +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__hr_comployee_id +msgid "Empolyee Name" +msgstr "اسم الموظف" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__emp +msgid "Empolyee code" +msgstr "كود الموظف" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__server_id +msgid "Hr empolyee" +msgstr "موظف الموارد البشرية" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard__id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__id +msgid "ID" +msgstr "المعرف" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__serverUrl +msgid "IP / Domain Name" +msgstr "عنوان IP / اسم النطاق" + +#. module: to_attendance_system +#: code:addons/to_attendance_system/models/models.py:0 +#, python-format +msgid "Invalid port number" +msgstr "رقم المنفذ غير صحيح" + +#. module: to_attendance_system +#: code:addons/to_attendance_system/models/models.py:0 +#, python-format +msgid "Invalid server url" +msgstr "عنوان الخادم غير صحيح" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__is_attendance +msgid "Is attendance" +msgstr "حضور" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area____last_update +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard____last_update +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api____last_update +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department____last_update +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee____last_update +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position____last_update +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance____last_update +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal____last_update +msgid "Last Modified on" +msgstr "آخر تعديل في" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__write_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard__write_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__write_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__write_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__write_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__write_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__write_uid +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__write_uid +msgid "Last Updated by" +msgstr "آخر تحديث بواسطة" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__write_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard__write_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__write_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__write_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__write_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__write_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__write_date +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__write_date +msgid "Last Updated on" +msgstr "آخر تحديث في" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__last_activity +msgid "Last activity" +msgstr "آخر نشاط" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__loaded_employees +msgid "Loaded Employees" +msgstr "الموظفون المحملون" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Login" +msgstr "تسجيل الدخول" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Logout" +msgstr "تسجيل الخروج" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__name +msgid "Name" +msgstr "الاسم" + +#. module: to_attendance_system +#: code:addons/to_attendance_system/models/models.py:0 +#: model:ir.model.fields.selection,name:to_attendance_system.selection__finger_biotime_api__state__unauthentic +#, python-format +msgid "Not authentic" +msgstr "غير مصادق عليه" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__password +msgid "Password" +msgstr "كلمة المرور" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__port +msgid "Port" +msgstr "المنفذ" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__position_id +msgid "Position id" +msgstr "معرف الوظيفة" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__punch_state +msgid "Punch state" +msgstr "حالة البصمة" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__punch_state_display +msgid "Punch state display" +msgstr "عرض حالة البصمة" + + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__punch_time +msgid "Punch time" +msgstr "وقت البصمة" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "Refresh" +msgstr "تحديث" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__sn +msgid "SN" +msgstr "الرقم التسلسلي" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__state +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__state +msgid "State" +msgstr "الحالة" + +#. module: to_attendance_system +#: model:ir.actions.server,name:to_attendance_system.ir_cron_scheduler_sync_system_attendance_ir_actions_server +#: model:ir.cron,cron_name:to_attendance_system.ir_cron_scheduler_sync_system_attendance +#: model:ir.cron,name:to_attendance_system.ir_cron_scheduler_sync_system_attendance +msgid "Synchronize attendances scheduler" +msgstr "جدولة مزامنة سجلات الحضور" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_attendance_wizard__system_ids +msgid "System" +msgstr "النظام" + +#. module: to_attendance_system +#: model:ir.ui.menu,name:to_attendance_system.attendance_systems_employee_areas +msgid "System Areas" +msgstr "مناطق النظام" + +#. module: to_attendance_system +#: model:ir.ui.menu,name:to_attendance_system.attendance_systems_employee_attendance +msgid "System Attendance" +msgstr "حضور النظام" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__department_id +msgid "System Deparment" +msgstr "قسم النظام" + +#. module: to_attendance_system +#: model:ir.ui.menu,name:to_attendance_system.attendance_systems_employee_department +msgid "System Departments" +msgstr "أقسام النظام" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__name +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__emp_code +msgid "System Employee Name" +msgstr "اسم موظف النظام" + +#. module: to_attendance_system +#: model:ir.actions.act_window,name:to_attendance_system.biotime_system_employee_action +msgid "System Employees" +msgstr "موظفو النظام" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__position_id +msgid "System Position" +msgstr "وظيفة النظام" + +#. module: to_attendance_system +#: model:ir.actions.act_window,name:to_attendance_system.biotime_system_terminal_action +#: model:ir.ui.menu,name:to_attendance_system.attendance_systems_terminal +msgid "System Terminals" +msgstr "أجهزة النظام" + +#. module: to_attendance_system +#: model:ir.actions.act_window,name:to_attendance_system.biotime_system_attendance_action +msgid "System attendances" +msgstr "سجلات حضور النظام" + +#. module: to_attendance_system +#: model:ir.actions.act_window,name:to_attendance_system.biotime_action +msgid "System connector" +msgstr "موصل النظام" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__code +msgid "System employee Code" +msgstr "كود موظف النظام" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__terminal_id +msgid "Termainl id" +msgstr "معرف الجهاز" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__terminal_id +msgid "Terminal" +msgstr "الجهاز" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__name +msgid "Terminal name" +msgstr "اسم الجهاز" + +#. module: to_attendance_system +#: model_terms:ir.ui.view,arch_db:to_attendance_system.biotime_action_form +msgid "To system attendance" +msgstr "إلى حضور النظام" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__token +msgid "Token" +msgstr "الرمز المميز" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__upload_time +msgid "Upload time" +msgstr "وقت الرفع" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__username +msgid "Username" +msgstr "اسم المستخدم" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__verify_type +msgid "Verify type" +msgstr "نوع التحقق" + +#. module: to_attendance_system +#: code:addons/to_attendance_system/wizard/attendaces_wizard.py:0 +#, python-format +msgid "You must select at least one device to continue!" +msgstr "يجب اختيار جهاز واحد على الأقل للمتابعة!" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_employee__email +msgid "email" +msgstr "البريد الإلكتروني" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_area +msgid "finger.area" +msgstr "finger.area" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_biotime_api +msgid "finger.biotime_api" +msgstr "finger.biotime_api" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_employee_department +msgid "finger.employee.department" +msgstr "finger.employee.department" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_employee_employee +msgid "finger.employee.employee" +msgstr "finger.employee.employee" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_employee_position +msgid "finger.employee.position" +msgstr "finger.employee.position" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_system_attendance +msgid "finger.system_attendance" +msgstr "finger.system_attendance" + +#. module: to_attendance_system +#: model:ir.model,name:to_attendance_system.model_finger_terminal +msgid "finger.terminal" +msgstr "finger.terminal" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__ip_address +msgid "ip address" +msgstr "عنوان IP" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_area__server_id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_department__server_id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_employee_position__server_id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_system_attendance__server_id +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_terminal__server_id +msgid "server" +msgstr "الخادم" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__terminals_ids +msgid "terminals" +msgstr "الأجهزة" + +#. module: to_attendance_system +#: model:ir.model.fields.selection,name:to_attendance_system.selection__finger_system_attendance__state__confirmed +msgid "مؤكد" +msgstr "مؤكد" + +#. module: to_attendance_system +#: model:ir.model.fields.selection,name:to_attendance_system.selection__finger_system_attendance__state__draft +msgid "مسوده" +msgstr "مسودة" + +#. module: to_attendance_system +#: model:ir.model.fields,field_description:to_attendance_system.field_finger_biotime_api__start_sync_date +msgid "Start Sync Date" +msgstr "تاريخ بدء المزامنة" + + +#. module: to_attendance_system +#: code:addons/odoo/STANDARD_MODULES/test/odex25_hr/odex25_hr/to_attendance_system/models/models.py:0 +#: code:addons/to_attendance_system/models/models.py:0 +#, python-format +msgid "Start Sync Date cannot be in the future. Please select now or a past date." +msgstr "لا يمكن أن يكون تاريخ بدء المزامنة في المستقبل. الرجاء اختيار الوقت الحالي أو تاريخ سابق." diff --git a/to_attendance_system/models/__init__.py b/to_attendance_system/models/__init__.py new file mode 100644 index 0000000..cde864b --- /dev/null +++ b/to_attendance_system/models/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from . import models diff --git a/to_attendance_system/models/helper.py b/to_attendance_system/models/helper.py new file mode 100644 index 0000000..ad764ab --- /dev/null +++ b/to_attendance_system/models/helper.py @@ -0,0 +1,106 @@ +import requests +import json +from urllib.parse import urlparse + + +def is_valid_port(port): + try: + port = int(port) + if 1 <= port <= 65535: + return True + else: + return False + except Exception: + return False + + +def is_valid_ip(url): + try: + result = urlparse(url) + return all([result.scheme, result.netloc]) + except ValueError: + return False + + +headers = {'Content-Type': 'application/json'} + + +class ParsedRequest(object): + def __init__(self, data): + self.__dict__ = json.loads(data) + + +# serverIP = "http://160.153.0.122:8008" +# loginUrl = serverIP + "/jwt-api-token-auth/" +# refreshUrl = serverIP + "/jwt-api-token-refresh/" +# employeeUrl = serverIP + "/personnel/api/employee/" +# departmentsUrl = serverIP + "/personnel/api/departments/" +# terminalsUrl = serverIP + "/iclock/api/terminals/" +# areasUrl = serverIP + "/personnel/api/areas/" +# positionsUrl = serverIP + "/personnel/api/positions/" +# transctionsUrl = serverIP + "/iclock/api/transactions/" +# defaultHeaders = {'Content-Type': 'application/json'} +defaultHeaders = {'Content-Type': 'application/json'} + + +class HttpHelper(object): + + def login(self, username, password, url, headers=defaultHeaders): + data = {'username': username, 'password': password} + return requests.post(url, data=json.dumps(data), headers=headers) + + def refresh(self, token, url, headers=defaultHeaders): + data = {'token': token} + return requests.post(url, data=json.dumps(data), headers=headers) + + def fetch_employees(self, data, token, url): + headers = { + 'Authorization': 'JWT ' + token, + 'Content-Type': 'application/json' + } + return requests.get(url, data=json.dumps(data), headers=headers) + + def fetch_departments(self, data, token, url): + headers = { + 'Authorization': 'JWT ' + token, + 'Content-Type': 'application/json' + } + return requests.get(url, data=json.dumps(data), headers=headers) + + def fetch_terminals(self, data, token, url): + headers = { + 'Authorization': 'JWT ' + token, + 'Content-Type': 'application/json' + } + return requests.get(url, data=json.dumps(data), headers=headers) + + def fetch_areas(self, data, token, url): + headers = { + 'Authorization': 'JWT ' + token, + 'Content-Type': 'application/json' + } + return requests.get(url, data=json.dumps(data), headers=headers) + + def fetch_positions(self, data, token, url): + headers = { + 'Authorization': 'JWT ' + token, + 'Content-Type': 'application/json' + } + return requests.get(url, data=json.dumps(data), headers=headers) + + def fetch_transctions(self, token, url): + headers = { + 'Authorization': 'JWT ' + token, + 'Content-Type': 'application/json' + } + return requests.get(url, headers=headers) + + def fetch_empl_transctions(self, data, token, url): + headers = { + 'Authorization': 'JWT ' + token, + 'Content-Type': 'application/json' + } + return requests.get(url, data=json.dumps(data), headers=headers) + + +httpHelper = HttpHelper() diff --git a/to_attendance_system/models/models.py b/to_attendance_system/models/models.py new file mode 100644 index 0000000..eba1d6a --- /dev/null +++ b/to_attendance_system/models/models.py @@ -0,0 +1,587 @@ +# -*- coding: utf-8 -*- + +from datetime import datetime, timedelta + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError +from .helper import httpHelper, is_valid_port, is_valid_ip, ParsedRequest + + +class SystemAttendance(models.Model): + _name = 'finger.system_attendance' + _order = 'punch_time DESC' + + punch_state = fields.Char("Punch state", readonly=True) + punch_state_display = fields.Char("Punch state display", readonly=True) + area_alias = fields.Char("Area", readonly=True) + crc = fields.Char("CRC", readonly=True) + verify_type = fields.Integer("Verify type", readonly=True) + emp = fields.Integer("Empolyee code", readonly=True) + is_attendance = fields.Integer("Is attendance", readonly=True) + attendance_id = fields.Integer("Attendance id", readonly=True) + upload_time = fields.Datetime(string='Upload time', readonly=True) + punch_time = fields.Datetime(string="Punch time", readonly=True) + emp_code = fields.Many2one('finger.employee.employee', string='System Employee Name', readonly=True) + terminal_id = fields.Many2one('finger.terminal', string='Terminal', readonly=True) + server_id = fields.Many2one('finger.biotime_api', string='server', readonly=True) + hr_comployee_id = fields.Many2one('hr.employee', string='Empolyee Name', readonly=True) + state = fields.Selection( + [('draft', _('Draft')), + ('confirmed', _('Confirmed'))], default="draft", readonly=True) + + +class SystemTerminal(models.Model): + _name = 'finger.terminal' + + name = fields.Char("Terminal name") + terminal_id = fields.Integer("Termainl id") + sn = fields.Char("SN") + area_name = fields.Char("Area name") + last_activity = fields.Char("Last activity") + ip_address = fields.Char("ip address") + alias = fields.Char("Alias") + server_id = fields.Many2one('finger.biotime_api', string='server') + + +class SystemEmployee(models.Model): + _name = 'finger.employee.employee' + + name = fields.Char("System Employee Name") + code = fields.Char("System employee Code") + emp_system_id = fields.Integer("Employee id") + email = fields.Char("email") + department_id = fields.Many2one('finger.employee.department', string='System Deparment') + position_id = fields.Many2one('finger.employee.position', string='System Position') + hr_employee = fields.Many2one('hr.employee', string='Empolyee Name') + server_id = fields.Many2one('finger.biotime_api', string='Hr empolyee') + + +class SystemEmployeePosition(models.Model): + _name = 'finger.employee.position' + + name = fields.Char("Name") + code = fields.Char("Code") + position_id = fields.Integer("Position id") + server_id = fields.Many2one('finger.biotime_api', string='server') + + +class SystemArea(models.Model): + _name = 'finger.area' + + name = fields.Char("Name") + code = fields.Char("Code") + area_id = fields.Integer("Area id") + server_id = fields.Many2one('finger.biotime_api', string='server') + + +class SystemDepartments(models.Model): + _name = 'finger.employee.department' + + name = fields.Char("Name") + code = fields.Char("Code") + department_id = fields.Integer("Department id") + server_id = fields.Many2one('finger.biotime_api', string='server') + + +class BiotimeAPI(models.Model): + _name = 'finger.biotime_api' + + name = fields.Char() + serverUrl = fields.Char(string="IP / Domain Name") + port = fields.Char(default="80") + username = fields.Char() + password = fields.Char() + token = fields.Char() + description = fields.Text() + loaded_employees = fields.Boolean(default=False) + + terminals_ids = fields.One2many( + string='terminals', + comodel_name='finger.terminal', + inverse_name='server_id', + ) + + attendance_ids = fields.One2many( + string='Attendances', + comodel_name='finger.system_attendance', + inverse_name='server_id', + ) + employee_ids = fields.One2many( + string='Employees', + comodel_name='finger.employee.employee', + inverse_name='server_id', + ) + + state = fields.Selection( + [('authentic', _('Authentic')), + ('unauthentic', _('Not authentic'))], default="unauthentic") + start_sync_date = fields.Datetime(string="Start Sync Date", default=lambda self: fields.Datetime.now()) + + @api.constrains('start_sync_date') + def _check_start_sync_date(self): + for record in self: + if record.start_sync_date and record.start_sync_date > fields.Datetime.now(): + raise ValidationError(_("Start Sync Date cannot be in the future. Please select now or a past date.")) + @api.model + def _calc_urls(self): + serverIP = self.serverUrl + ":" + self.port + loginUrl = serverIP + "/jwt-api-token-auth/" + refreshUrl = serverIP + "/jwt-api-token-refresh/" + employeeUrl = serverIP + "/personnel/api/employee/" + departmentsUrl = serverIP + "/personnel/api/departments/" + terminalsUrl = serverIP + "/iclock/api/terminals/" + areasUrl = serverIP + "/personnel/api/areas/" + positionsUrl = serverIP + "/personnel/api/positions/" + transctionsUrl = serverIP + "/iclock/api/transactions/" + return loginUrl, refreshUrl, employeeUrl, departmentsUrl, positionsUrl, is_valid_ip, areasUrl, terminalsUrl, ParsedRequest, transctionsUrl + + @api.depends('token') + def depends_token(self): + for srv in self: + if srv.token: + srv.state = 'authentic' + + @api.onchange('token') + def onchange_token(self): + for srv in self: + if srv.token: + srv.state = 'authentic' + + @api.constrains('serverUrl') + def check_is_valid_ip(self): + for srv in self: + if not is_valid_ip(srv.serverUrl): + raise ValidationError(_("Invalid server url")) + + @api.constrains('port') + def check_is_valid_ip(self): + for srv in self: + if not is_valid_port(srv.port): + raise ValidationError(_("Invalid port number")) + + def process_attendance_scheduler(self): + for tx in self.attendance_ids: + print(tx) + + def login(self): + loginUrl, _, _, _, _, _, _, _, _, _ = self._calc_urls() + res = httpHelper.login(self.username, self.password, loginUrl) + + if res.status_code == 200: + data = res.json() + token = data.get('token', False) + if token: + self.token = token + self.state = 'authentic' + else: + data = res.json() + err = "" + for key in data: + err += ' '.join(data[key]) + raise ValidationError(err) + + def refresh(self): + _, refreshUrl, _, _, _, _, _, _, _, _ = self._calc_urls() + res = httpHelper.refresh(self.token, refreshUrl) + if res.status_code == 200: + data = res.json() + token = data.get('token', False) + if token: + self.token = token + else: + data = res.json() + err = "" + for key in data: + err += ' '.join(data[key]) + raise ValidationError(err) + + def logout(self): + self.token = False + self.state = 'unauthentic' + + def to_attendace(self): + Attend = self.env['finger.system_attendance'] + attendance = self.env['attendance.attendance'] + HR = self.env['hr.employee'] + attens = Attend.search([('server_id', '=', self.id), ('state', '=', 'draft')]) + for tx in attens: + if tx.emp_code and tx.emp_code.hr_employee: + attendance.create({ + 'employee_id': tx.emp_code.hr_employee.id, + 'name': tx.punch_time, + 'action': 'sign_in' if tx.punch_state in ["0", "2", "4"] else 'sign_out', + 'action_date': tx.punch_time, + }) + tx.write({ + 'state': 'confirmed' + }) + + def sync_employees(self): + if not self.token: + self.refresh() + _, _, employeeUrl, _, _, _, _, _, _, _ = self._calc_urls() + res = httpHelper.fetch_employees({}, self.token, employeeUrl) + + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_employee(da.data) + next = 2 + while da.next: + r = httpHelper.fetch_employees( + {}, self.token, employeeUrl + "?page=" + str(next)) + if r.status_code == 200: + da = ParsedRequest(r.content) + self.create_employee(da.data) + else: + da.next = None + next = next + 1 + self.loaded_employees = True + else: + self.errorHandler(res) + + def errorHandler(self, res): + if res.status_code == 401: + self.logout() + else: + data = res.json() + err = "" + for key in data: + err += ' '.join(data[key]) + raise ValidationError(err) + + def sync_terminals(self): + if not self.token: + self.refresh() + _, _, _, _, _, _, _, terminalsUrl, _, _ = self._calc_urls() + res = httpHelper.fetch_terminals({}, self.token, terminalsUrl) + + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_termainl(da.data) + next = 2 + while da.next: + r = httpHelper.fetch_terminals( + {}, self.token, terminalsUrl + "?page=" + str(next)) + if r.status_code == 200: + da = ParsedRequest(r.content) + self.create_termainl(da.data) + else: + da.next = None + next = next + 1 + else: + self.errorHandler(res) + + def sync_departments(self): + if not self.token: + self.refresh() + _, _, _, departmentsUrl, _, _, _, _, _, _ = self._calc_urls() + res = httpHelper.fetch_departments({}, self.token, departmentsUrl) + + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_department(da.data) + next = 2 + while da.next: + r = httpHelper.fetch_departments( + {}, self.token, departmentsUrl + "?page=" + str(next)) + if r.status_code == 200: + da = ParsedRequest(r.content) + self.create_department(da.data) + else: + da.next = None + next = next + 1 + else: + self.errorHandler(res) + + def sync_areas(self): + if not self.token: + self.refresh() + _, _, _, _, _, _, areasUrl, _, _, _ = self._calc_urls() + res = httpHelper.fetch_areas({}, self.token, areasUrl) + try: + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_area(da.data) + next = 2 + while da.next: + res = httpHelper.fetch_areas( + {}, self.token, areasUrl + "?page=" + str(next)) + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_area(da.data) + else: + da.next = None + next = next + 1 + else: + self.errorHandler(res) + except Exception as e: + ValidationError(str(e)) + + def sync_positions(self): + if not self.token: + self.refresh() + _, _, _, _, positionsUrl, _, _, _, _, _ = self._calc_urls() + res = httpHelper.fetch_positions({}, self.token, positionsUrl) + + try: + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_position(da.data) + next = 2 + while da.next: + res = httpHelper.fetch_positions( + {}, self.token, positionsUrl + "?page=" + str(next)) + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_position(da.data) + else: + da.next = None + next = next + 1 + else: + self.errorHandler(res) + except Exception as e: + ValidationError(str(e)) + + def sync_employee_attenence(self, url): + if not self.token: + self.refresh() + res = httpHelper.fetch_empl_transctions({}, self.token, url) + + try: + if res.status_code == 200: + da = ParsedRequest(res.content) + next = 2 + self.create_attendance(da.data) + while da.next: + res = httpHelper.fetch_empl_transctions({}, self.token, da.next) + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_attendance(da.data) + else: + da.next = None + next = next + 1 + else: + self.errorHandler(res) + if res.status_code == 401: + self.refresh() + if self.token: + self.sync_employee_attenence(url) + except Exception as e: + ValidationError(str(e)) + + def sync_attenence(self): + self.login() + if not self.token: + self.refresh() + _, _, _, _, _, _, _, _, _, transctionsUrl = self._calc_urls() + + start_date = self.start_sync_date.strftime( + '%Y-%m-%d %H:%M:%S') if self.start_sync_date else "2025-10-01 00:00:00" + + url_with_filter = transctionsUrl + "?start_time=" + start_date + + res = httpHelper.fetch_empl_transctions({}, self.token, url_with_filter) + if not self.loaded_employees: + self.sync_employees() + + try: + if res.status_code == 200: + da = ParsedRequest(res.content) + next = 2 + self.create_attendance(da.data) + while da.next: + res = httpHelper.fetch_empl_transctions( + {}, self.token, transctionsUrl + "?start_time=" + start_date + "&page=" + str(next)) + if res.status_code == 200: + da = ParsedRequest(res.content) + self.create_attendance(da.data) + else: + da.next = None + next = next + 1 + else: + self.errorHandler(res) + except Exception as e: + ValidationError(str(e)) + + # def sync_attenence(self): + # self.login() + # if not self.token: + # self.refresh() + # _, _, _, _, _, _, _, _, _, transctionsUrl = self._calc_urls() + # res = httpHelper.fetch_empl_transctions({}, self.token, transctionsUrl) + # if not self.loaded_employees: + # self.sync_employees() + # + # try: + # if res.status_code == 200: + # da = ParsedRequest(res.content) + # next = 2 + # self.create_attendance(da.data) + # while da.next: + # res = httpHelper.fetch_empl_transctions( + # {}, self.token, transctionsUrl + "?page=" + str(next)) + # if res.status_code == 200: + # da = ParsedRequest(res.content) + # self.create_attendance(da.data) + # else: + # da.next = None + # next = next + 1 + # else: + # self.errorHandler(res) + # except Exception as e: + # ValidationError(str(e)) + + + def create_termainl(self, termainls): + TerminalsModel = self.env['finger.terminal'] + for tx in termainls: + tirm = TerminalsModel.search([('terminal_id', '=', tx['id'])]) + data = { + 'terminal_id': tx['id'], + 'name': tx['terminal_name'], + 'sn': tx['sn'], + 'area_name': tx['area_name'], + 'last_activity': tx['last_activity'], + 'ip_address': tx['ip_address'], + 'alias': tx['alias'], + 'server_id': self.id + } + if not tirm: + TerminalsModel.create(data) + else: + tirm.update(data) + + def create_area(self, areas): + TerminalsModel = self.env['finger.area'] + for tx in areas: + tirm = TerminalsModel.search([('area_id', '=', tx['id'])]) + data = { + 'area_id': tx['id'], + 'name': tx['area_name'], + 'code': tx['area_code'], + 'server_id': self.id + } + if not tirm: + TerminalsModel.create(data) + else: + tirm.update(data) + + def create_department(self, departments): + TerminalsModel = self.env['finger.employee.department'] + for tx in departments: + tirm = TerminalsModel.search([('department_id', '=', tx['id'])]) + data = { + 'department_id': tx['id'], + 'name': tx['dept_name'], + 'code': tx['dept_code'], + 'server_id': self.id + } + if not tirm: + TerminalsModel.create(data) + else: + tirm.update(data) + + def create_position(self, positions): + PostionModel = self.env['finger.employee.position'] + for tx in positions: + tirm = PostionModel.search([('position_id', '=', tx['id'])]) + data = { + 'position_id': tx['id'], + 'name': tx['position_name'], + 'code': tx['position_code'], + 'server_id': self.id + } + if not tirm: + PostionModel.create(data) + else: + tirm.update(data) + + def create_employee(self, employees): + EmployeeModel = self.env['finger.employee.employee'] + DepartmentModel = self.env['finger.employee.department'] + PositionModel = self.env['finger.employee.position'] + HR = self.env['hr.employee'] + + for tx in employees: + tirm = EmployeeModel.search([('emp_system_id', '=', tx['id'])]) + dep = None + pos = None + hrEmp = None + if tx['position']: + pos = PositionModel.search([('position_id', '=', tx['position'])], limit=1) + + if tx['department']: + dep = DepartmentModel.search([('department_id', '=', tx['department'])], limit=1) + + if tx['emp_code']: + hrEmp = HR.search([('emp_no', '=', tx['emp_code'])]) + + data = { + 'emp_system_id': tx['id'], + 'name': tx['first_name'], + 'code': tx['emp_code'], + 'department_id': dep.id if dep else False, + 'position_id': pos.id if pos else False, + 'hr_employee': hrEmp.id if hrEmp else False, + 'server_id': self.id, + } + + if not tirm and hrEmp: + EmployeeModel.create(data) + elif hrEmp and tirm: + tirm.update(data) + + def create_attendance(self, attendaces): + AttendanceModel = self.env['finger.system_attendance'] + TerminalModel = self.env['finger.terminal'] + EmployeeModel = self.env['finger.employee.employee'] + for tx in attendaces: + tirm = AttendanceModel.search([('attendance_id', '=', tx['id'])]) + empe = None + pos = None + # "2020-08-09 10:23:20" + new_punch_time = datetime.strptime(tx['punch_time'], "%Y-%m-%d %H:%M:%S") + timedelta(hours=-3) + new_upload_time = datetime.strptime(tx['upload_time'], "%Y-%m-%d %H:%M:%S") + timedelta(hours=-3) + # datetime_format = "%Y-%m-%d %H:%M:%S" + + if tx['emp_code']: + empe = EmployeeModel.search([('code', '=', tx['emp_code'])], limit=1) + + if tx['terminal_alias']: + dep = TerminalModel.search([('sn', '=', tx['terminal_sn'])], limit=1) + data = { + 'attendance_id': tx['id'], + 'punch_state': tx['punch_state'], + 'punch_state_display': tx['punch_state_display'], + 'area_alias': tx['area_alias'], + # 'crc': tx['crc'], + 'crc': tx['emp'], + 'verify_type': tx['verify_type'], + # 'is_attendance': tx['is_attendance'], + 'upload_time': new_upload_time, + 'punch_time': new_punch_time, + 'emp_code': empe.id if empe else None, + 'terminal_id': dep.id if dep else None, + 'server_id': self.id, + 'hr_comployee_id': empe.hr_employee.id if empe and empe.hr_employee else None, + 'emp': empe.code if empe else None + } + + if not tirm and empe: + # print('9999999999999999999999999999999999999999999999999',tirm) + AttendanceModel.create(data) + elif empe and tirm: + tirm.update(data) + + def action_attendance_download(self): + now = datetime.now() + yesterday = datetime.now() - timedelta(hours=48) + now = now.strftime("%Y-%m-%d %H:%M:%S") + yesterday = yesterday.strftime("%Y-%m-%d %H:%M:%S") + _, _, _, _, _, _, _, _, _, transctionsUrl = self._calc_urls() + for r in self: + for xm in r.employee_ids: + url = "{}?start_time={}&end_time={}&emp_code={}".format(transctionsUrl,yesterday,now, xm.code ) + # url = "{}?punch_time={}&punch_time={}&emp_code={}".format(transctionsUrl, yesterday, now, xm.code) + # print('################################# New Employee',url) + r.sync_employee_attenence(url) diff --git a/to_attendance_system/security/ir.model.access.csv b/to_attendance_system/security/ir.model.access.csv new file mode 100644 index 0000000..8db367f --- /dev/null +++ b/to_attendance_system/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id/id,perm_read,perm_write,perm_create,perm_unlink +access_finger_system_attendance,finger_system_attendance,to_attendance_system.model_finger_system_attendance,,1,1,1,0 +access_finger_biotime_api,finger_biotime_api,to_attendance_system.model_finger_biotime_api,,1,1,1,0 +access_finger_employee_department,finger_employee_department,to_attendance_system.model_finger_employee_department,,1,1,1,0 +access_finger_area,finger_area,to_attendance_system.model_finger_area,,1,1,1,0 +access_finger_employee_position,finger_employee_position,to_attendance_system.model_finger_employee_position,,1,1,1,0 +access_finger_employee_employee,finger_employee_employee,to_attendance_system.model_finger_employee_employee,,1,1,1,0 +access_finger_terminal,finger_terminal,to_attendance_system.model_finger_terminal,,1,1,1,0 diff --git a/to_attendance_system/views/views.xml b/to_attendance_system/views/views.xml new file mode 100644 index 0000000..a1e6355 --- /dev/null +++ b/to_attendance_system/views/views.xml @@ -0,0 +1,248 @@ + + + + + + + finger.biotime_api.form + finger.biotime_api + +
+
+
+ + + + + + + + + + + + + + +
+
+
+ + + finger.biotime_api.tree + finger.biotime_api + + + + + + + + + + + + finger.terminal.tree + finger.terminal + + + + + + + + + + + + finger.terminal.tree + finger.terminal + + + + + + + + + + + + System connector + finger.biotime_api + list,form + + + + + System Terminals + finger.terminal + list,form + + + + + + + + + + + finger.area.tree + finger.area + + + + + + + + + + + + Employee areas + finger.area + list,form + + + + + + + + + finger.employee.department.tree + finger.employee.department + + + + + + + + + + + + Employee departments + finger.employee.department + list,form + + + + + + + + + finger.employee.position.tree + finger.employee.position + + + + + + + + + + + + Employee Positions + finger.employee.position + list,form + + + + + + + + + finger.employee.employee.tree + finger.employee.employee + + + + + + + + + + + + System Employees + finger.employee.employee + list,form + + + + + + + + + finger.system_attendance.tree + finger.system_attendance + + + + + + + + + + + + + + + + + + + + System attendances + finger.system_attendance + list,form + + + + + + +
+
diff --git a/to_attendance_system/wizard/__init__.py b/to_attendance_system/wizard/__init__.py new file mode 100644 index 0000000..5ad064f --- /dev/null +++ b/to_attendance_system/wizard/__init__.py @@ -0,0 +1 @@ +from . import attendaces_wizard \ No newline at end of file diff --git a/to_attendance_system/wizard/attendaces_wizard.py b/to_attendance_system/wizard/attendaces_wizard.py new file mode 100644 index 0000000..a2333ec --- /dev/null +++ b/to_attendance_system/wizard/attendaces_wizard.py @@ -0,0 +1,53 @@ +from odoo import models, fields, api, _ +from odoo.exceptions import UserError + +import logging + +_logger = logging.getLogger(__name__) + + +class AttendanceWizard(models.TransientModel): + _name = 'finger.attendance_wizard' + _description = 'Attendance Wizard' + + @api.model + def _get_all_systems_ids(self): + all_systems = self.env['finger.biotime_api'].search([('state', '=', 'authentic')]) + if all_systems: + return all_systems.ids + else: + return [] + + system_ids = fields.Many2many('finger.biotime_api', string='System', default=_get_all_systems_ids, + domain=[('state', '=', 'confirmed')]) + + def download_attendance_manually(self): + if not self.system_ids: + raise UserError(_('You must select at least one device to continue!')) + self.system_ids.action_attendance_download() + + def download_system_attendance(self): + systems = self.env['finger.biotime_api'].search([('state', '=', 'authentic')]) + systems.action_attendance_download() + + def cron_sync_attendance(self): + self.with_context(synch_ignore_constraints=True).sync_attendance() + + def sync_attendance(self): + + synch_ignore_constraints = self.env.context.get('synch_ignore_constraints', False) + HR = self.env['hr.employee'] + attendance = self.env['attendance.attendance'] + attendance_ids = self.env['finger.system_attendance'].search([('state', '=', 'draft')]) + + for tx in attendance_ids: + if tx.emp_code and tx.emp_code.hr_employee: + attendance.create({ + 'employee_id': tx.emp_code.hr_employee.id, + 'name': tx.punch_time, + 'action': 'sign_in' if tx.punch_state in ["0", "2", "4"] else 'sign_out', + 'action_date': tx.punch_time, + }) + tx.write({ + 'state': 'confirmed' + }) diff --git a/to_attendance_system/wizard/attendaces_wizard.xml b/to_attendance_system/wizard/attendaces_wizard.xml new file mode 100644 index 0000000..3c48232 --- /dev/null +++ b/to_attendance_system/wizard/attendaces_wizard.xml @@ -0,0 +1,42 @@ + + + + + + Attendance Device Synchronization wizard + finger.attendance_wizard + +
+
+ This wizard will synchronize all data from all of your systems + into Odoo. +
+ Download employee into Odoo; Map those with Odoo + Employees and create + additional Employees from device data; Download attendance data + from the devices and create Odoo attendance data from such data +
+ + + + + +
+
+
+
+
+ + + + + +
+
\ No newline at end of file