Merge branch 'dev_odex_hr' of https://github.com/expsa/odex_30 into dev_odex_hr

This commit is contained in:
Mostafa 2026-01-04 01:33:51 -08:00
commit 92e5bc18aa
170 changed files with 9453 additions and 224 deletions

View File

@ -244,6 +244,7 @@ class employee_overtime_request(models.Model):
'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)],
'res_model': 'employee.overtime.request',
'res_id': self.id
})
record.account_id = account_debit_id.id
record.journal_id = journal_id.id

View File

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

View File

@ -0,0 +1,4 @@
from . import test_overtime_process
from . import test_employee_department_jobs
from . import test_hr_clearance_form
from . import test_hr_personal_permission

View File

@ -0,0 +1,88 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from odoo import fields
from dateutil.relativedelta import relativedelta
class TestEmployeeDepartmentJobs(TransactionCase):
def setUp(self):
super(TestEmployeeDepartmentJobs, self).setUp()
self.manager_employee = self.env['hr.employee'].create({
'name': 'Big Manager',
})
self.dep1 = self.env['hr.department'].create({
'name': 'IT Department',
'manager_id': self.manager_employee.id,
})
self.dep2 = self.env['hr.department'].create({
'name': 'HR Department',
'manager_id': self.manager_employee.id,
})
self.job1 = self.env['hr.job'].create({'name': 'Developer', 'department_id': self.dep1.id})
self.job2 = self.env['hr.job'].create(
{'name': 'HR Officer', 'department_id': self.dep2.id, 'no_of_recruitment': 1})
self.job_full = self.env['hr.job'].create(
{'name': 'Manager', 'department_id': self.dep2.id, 'no_of_recruitment': 0})
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee Job',
'department_id': self.dep1.id,
'job_id': self.job1.id,
'parent_id': self.manager_employee.id, # ربطه بالمدير
'first_hiring_date': fields.Date.today() - relativedelta(years=2),
'joining_date': fields.Date.today() - relativedelta(years=1),
})
def test_prevent_job_change_no_vacancy(self):
req = self.env['employee.department.jobs'].new({
'employee_id': self.employee.id,
'new_department_id': self.dep2.id,
'new_job_id': self.job_full.id, # 0 vacancies
})
with self.assertRaises(UserError, msg="Should raise error when no recruitment spots available"):
req.not_reused_same_dep_job()
def test_update_employee_data_on_approval(self):
request = self.env['employee.department.jobs'].create({
'employee_id': self.employee.id,
'promotion_type': 'both',
'new_department_id': self.dep2.id,
'new_job_id': self.job2.id,
'date': fields.Date.today(),
'state': 'hr_manager',
})
request.store_level_group_and_degree_values()
request.approved()
self.assertEqual(self.employee.department_id.id, self.dep2.id, "Department not updated")
self.assertEqual(self.employee.job_id.id, self.job2.id, "Job not updated")
self.assertEqual(self.employee.joining_date, fields.Date.today(), "Joining date not updated")
def test_service_duration_calculation(self):
today = fields.Date.today()
one_year_one_month_ago = today - relativedelta(years=1, months=1)
self.employee.joining_date = one_year_one_month_ago
request = self.env['employee.department.jobs'].create({
'employee_id': self.employee.id,
'date': today,
})
request.store_level_group_and_degree_values()
request._compute_duration()
self.assertEqual(request.service_year, 1, "Service year calculation wrong")
self.assertEqual(request.service_month, 1, "Service month calculation wrong")
self.assertEqual(request.service_day, 0, "Service day calculation wrong")

View File

@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from unittest.mock import patch
import base64
class TestHrClearanceForm(TransactionCase):
def setUp(self):
super(TestHrClearanceForm, self).setUp()
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee Clearance',
'first_hiring_date': '2020-01-01',
})
self.clearance = self.env['hr.clearance.form'].create({
'employee_id': self.employee.id,
'clearance_type': 'final',
})
def test_bank_attachment_constraint(self):
self.clearance.state = 'admin_manager'
with self.assertRaises(UserError, msg="Should require attachment for Final Clearance"):
self.clearance.wait()
file_content = b'This is a test file content'
encoded_content = base64.b64encode(file_content)
attachment = self.env['ir.attachment'].create({
'name': 'Bank Clearance.pdf',
'datas': encoded_content,
'res_model': 'hr.clearance.form',
'res_id': self.clearance.id,
})
self.clearance.bank_attachment_id = [(6, 0, [attachment.id])]
self.clearance.wait()
self.assertEqual(self.clearance.state, 'wait', "State should move to wait after attachment")
def test_custody_check_blocking(self):
with patch('odoo.models.Model.search') as mock_search:
def side_effect(domain, **kwargs):
if domain == [('state', '=', 'installed'), ('name', '=', 'exp_employee_custody')]:
return [1]
if len(domain) > 0 and domain[0][0] == 'employee_id' and domain[0][2] == self.employee.id:
return [1]
return []
mock_search.side_effect = side_effect
if 'custom.employee.custody' in self.env:
with self.assertRaises(UserError, msg="Should block clearance if custody exists"):
self.clearance.check_custody()
else:
pass

View File

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError
from odoo import fields
from datetime import datetime, timedelta
class TestHrPersonalPermission(TransactionCase):
def setUp(self):
super(TestHrPersonalPermission, self).setUp()
self.permission_type = self.env['hr.personal.permission.type'].create({
'name': 'Medical Permission',
'daily_hours': 4.0,
'monthly_hours': 10.0,
'approval_by': 'direct_manager',
})
self.calendar = self.env['resource.calendar'].create({
'name': 'Standard 8 Hours',
'is_full_day': True,
'hours_per_day': 8.0,
'full_min_sign_in': 7.0,
'full_max_sign_in': 9.0,
'full_min_sign_out': 15.0,
'full_max_sign_out': 17.0,
})
self.employee = self.env['hr.employee'].create({
'name': 'Ahmed Test Employee',
'resource_calendar_id': self.calendar.id,
'first_hiring_date': fields.Date.today() - timedelta(days=365),
})
def test_01_duration_calculation(self):
start_time = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0)
end_time = start_time + timedelta(hours=1.5)
permission = self.env['hr.personal.permission'].create({
'employee_id': self.employee.id,
'permission_type_id': self.permission_type.id,
'date_from': start_time,
'date_to': end_time,
'permission_number': 10.0,
})
self.assertEqual(permission.duration, 1.5, "Duration calculation is wrong")
def test_02_daily_limit_constraint(self):
self.permission_type.daily_hours = 2.0
start_time = datetime.now().replace(hour=9, minute=0, second=0, microsecond=0)
end_time = start_time + timedelta(hours=3)
with self.assertRaises(UserError, msg="Should prevent permission > daily limit"):
self.env['hr.personal.permission'].create({
'employee_id': self.employee.id,
'permission_type_id': self.permission_type.id,
'date_from': start_time,
'date_to': end_time,
'permission_number': 10.0,
})
def test_03_monthly_limit_constraint(self):
self.permission_type.monthly_hours = 4.0
base_date = datetime.now().replace(day=1, hour=9, minute=0, second=0, microsecond=0)
self.env['hr.personal.permission'].create({
'employee_id': self.employee.id,
'permission_type_id': self.permission_type.id,
'date_from': base_date,
'date_to': base_date + timedelta(hours=2),
'state': 'approve',
'permission_number': 4.0,
})
base_date_2 = base_date + timedelta(days=1)
self.env['hr.personal.permission'].create({
'employee_id': self.employee.id,
'permission_type_id': self.permission_type.id,
'date_from': base_date_2,
'date_to': base_date_2 + timedelta(hours=2),
'state': 'approve',
'permission_number': 2.0,
})
base_date_3 = base_date + timedelta(days=2)
with self.assertRaises(UserError, msg="Should prevent permission > monthly limit"):
self.env['hr.personal.permission'].create({
'employee_id': self.employee.id,
'permission_type_id': self.permission_type.id,
'date_from': base_date_3,
'date_to': base_date_3 + timedelta(hours=1),
'permission_number': 0.0, # الرصيد انتهى
})
def test_04_check_attendance_hours(self):
start_time = datetime.now().replace(hour=18, minute=0, second=0, microsecond=0)
end_time = start_time + timedelta(hours=1)
with self.assertRaises(ValidationError, msg="Should prevent permission outside attendance hours"):
self.env['hr.personal.permission'].create({
'employee_id': self.employee.id,
'permission_type_id': self.permission_type.id,
'date_from': start_time,
'date_to': end_time,
'permission_number': 10.0,
})
def test_05_overlap_with_holiday(self):
if 'hr.holidays' not in self.env:
return
today = fields.Date.today()
holiday_status = self.env['hr.holidays.status'].search([], limit=1)
if not holiday_status:
holiday_status = self.env['hr.holidays.status'].create({'name': 'Test Leave'})
self.env['hr.holidays'].create({
'employee_id': self.employee.id,
'holiday_status_id': holiday_status.id,
'date_from': today,
'date_to': today,
'type': 'remove',
'state': 'validate',
})
start_time = datetime.now().replace(hour=10, minute=0, second=0, microsecond=0)
with self.assertRaises(UserError, msg="Should prevent permission during approved holiday"):
perm = self.env['hr.personal.permission'].create({
'employee_id': self.employee.id,
'permission_type_id': self.permission_type.id,
'date_from': start_time,
'date_to': start_time + timedelta(hours=1),
'permission_number': 10.0,
})
perm.check_holiday_mission()

View File

@ -0,0 +1,188 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from odoo import fields
from datetime import date, timedelta
class TestOvertimeProcess(TransactionCase):
def setUp(self):
super(TestOvertimeProcess, self).setUp()
self.account_type_expenses = self.env['account.account'].search([('account_type', '=', 'expense')], limit=1)
if not self.account_type_expenses:
user_type_expense = self.env.ref('account.data_account_type_expenses')
self.account_type_expenses = self.env['account.account'].create({
'name': 'Overtime Expense',
'code': 'X6000',
'account_type': 'expense',
'reconcile': True,
})
self.overtime_account = self.account_type_expenses
self.journal_account = self.env['account.account'].search([('account_type', '=', 'asset_cash')], limit=1)
self.journal = self.env['account.journal'].create({
'name': 'Overtime Journal',
'type': 'cash',
'code': 'OTJ',
'default_account_id': self.journal_account.id,
})
self.working_hours = self.env['resource.calendar'].create({
'name': 'Standard 40 Hours',
'hours_per_day': 8.0,
# 'max_overtime_hour': 10.0,
# 'overtime_factor_daily': 1.5,
# 'overtime_factor_holiday': 2.0,
# 'work_days': 20,
# 'work_hour': 8,
# 'journal_overtime_id': self.journal.id,
# 'account_overtime_id': self.overtime_account.id,
})
self.working_hours.write({
'max_overtime_hour': 20.0,
'overtime_factor_daily': 1.5,
'overtime_factor_holiday': 2.0,
'work_days': 30,
'work_hour': 8,
'journal_overtime_id': self.journal.id,
'account_overtime_id': self.overtime_account.id,
})
self.employee = self.env['hr.employee'].create({
'name': 'Ahmed Tester',
'state': 'open',
'first_hiring_date': date.today() - timedelta(days=365), # موظف منذ سنة
})
self.contract = self.env['hr.contract'].create({
'name': 'Contract for Ahmed',
'employee_id': self.employee.id,
'state': 'open',
'wage': 5000, # الراتب الأساسي
'resource_calendar_id': self.working_hours.id,
# 'total_allowance': 1000,
# 'salary': 5000,
})
self.contract.write({
'total_allowance': 1000,
'salary': 5000,
})
def test_overtime_period_constraint_cross_month(self):
# Arrange
date_from = date(2025, 1, 31)
date_to = date(2025, 2, 1) # شهر مختلف
# Act & Assert
with self.assertRaises(UserError):
request = self.env['employee.overtime.request'].create({
'employee_id': self.employee.id,
'request_date': date_from,
'date_from': date_from,
'date_to': date_to,
'line_ids_over_time': [(0, 0, {
'employee_id': self.employee.id,
'over_time_workdays_hours': 5.0,
})]
})
request.line_ids_over_time.get_max_remain_hours()
def test_max_hours_constraint_and_exception(self):
# Arrange
self.working_hours.write({'max_overtime_hour': 10.0})
date_from = date(2025, 3, 1)
date_to = date(2025, 3, 5)
with self.assertRaises(UserError):
self.env['employee.overtime.request'].create({
'employee_id': self.employee.id,
'date_from': date_from,
'date_to': date_to,
'exception': False,
'line_ids_over_time': [(0, 0, {
'employee_id': self.employee.id,
'over_time_workdays_hours': 15.0,
})]
})
request = self.env['employee.overtime.request'].create({
'employee_id': self.employee.id,
'date_from': date_from,
'date_to': date_to,
'exception': True,
'line_ids_over_time': [(0, 0, {
'employee_id': self.employee.id,
'over_time_workdays_hours': 15.0,
})]
})
# Assert
self.assertTrue(request.line_ids_over_time.exception, "Exception flag should be propagated to lines")
def test_calculate_daily_rate(self):
expected_hourly_rate = 8500.0 / 240.0
# Act
request = self.env['employee.overtime.request'].create({
'employee_id': self.employee.id,
'date_from': date(2025, 4, 1),
'date_to': date(2025, 4, 1),
'line_ids_over_time': [(0, 0, {
'employee_id': self.employee.id,
'over_time_workdays_hours': 2.0,
})]
})
line = request.line_ids_over_time[0]
# Assert
self.assertAlmostEqual(line.daily_hourly_rate, expected_hourly_rate, places=2,
msg="Daily hourly rate calculation is incorrect")
expected_total_price = expected_hourly_rate * 2.0
self.assertAlmostEqual(line.price_hour, expected_total_price, places=2,
msg="Total price calculation is incorrect")
def test_accounting_transfer_validation(self):
# Arrange
request = self.env['employee.overtime.request'].create({
'employee_id': self.employee.id,
'date_from': date(2025, 5, 1),
'date_to': date(2025, 5, 2),
'transfer_type': 'accounting',
'line_ids_over_time': [(0, 0, {
'employee_id': self.employee.id,
'over_time_workdays_hours': 10.0,
})]
})
request.state = 'executive_office'
# Act
request.validated()
# Assert
self.assertEqual(request.state, 'validated', "State should be validated")
line = request.line_ids_over_time[0]
self.assertTrue(line.move_id, "Account Move should be created")
self.assertEqual(line.move_id.state, 'draft', "Move should be in draft state initially")
debit_line = line.move_id.line_ids.filtered(lambda l: l.debit > 0)
credit_line = line.move_id.line_ids.filtered(lambda l: l.credit > 0)
self.assertTrue(debit_line and credit_line, "Should have debit and credit lines")
self.assertEqual(debit_line.account_id, self.overtime_account, "Debit account mismatch")
self.assertEqual(credit_line.account_id, self.journal.default_account_id, "Credit account mismatch")
self.assertAlmostEqual(debit_line.debit, line.price_hour, places=2, msg="Debit amount mismatch")

View File

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

View File

@ -0,0 +1,159 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import ValidationError
from odoo.tests import tagged
from datetime import date
@tagged('post_install', '-at_install')
class TestCustodyReceiving(TransactionCase):
def setUp(self):
super(TestCustodyReceiving, self).setUp()
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee for Custody',
'emp_no': 'EMP001',
})
self.deduction_category = self.env['hr.salary.rule.category'].create({
'name': 'Deduction',
'code': 'DED',
'rule_type': 'deduction',
})
self.salary_rule = self.env['hr.salary.rule'].create({
'name': 'Custody Deduction Rule',
'sequence': 1,
'code': 'CUST_DED',
'category_id': self.deduction_category.id,
'condition_select': 'none',
'amount_select': 'fix',
'amount_fix': 0.0,
})
self.contract = self.env['hr.contract'].create({
'name': 'Contract Test',
'employee_id': self.employee.id,
'wage': 5000.0,
'state': 'open',
})
self.custody_delivered = self.env['custom.employee.custody'].create({
'employee_id': self.employee.id,
'state': 'approve',
})
self.custody_line = self.env['employee.custody.line'].create({
'employee_custody_line': self.custody_delivered.id,
'name': 'Laptop Dell',
'serial': 'SN123456',
'quantity': 1.0,
'receiving_quantity': 0.0,
'amount': 0.0,
})
def test_01_custody_receiving_workflow_full_return(self):
receiving = self.env['hr.custody.receiving'].create({
'employee_id': self.employee.id,
'note': 'Returning Laptop',
})
receiving._get_custody_line_domain()
self.assertTrue(receiving.return_custody_line_ids, "Should fetch pending custody lines")
line = receiving.return_custody_line_ids[0]
self.assertEqual(line.custody_line_id, self.custody_line, "Should link to correct original line")
self.assertEqual(line.quantity, 1.0, "Should default to remaining quantity")
receiving.send()
self.assertEqual(receiving.state, 'submit')
receiving.dr_manager()
self.assertEqual(receiving.state, 'direct')
receiving.dr_hr_manager()
self.assertEqual(receiving.state, 'admin')
receiving.warehouse_keeper()
self.assertEqual(receiving.state, 'approve')
receiving.done()
self.assertEqual(receiving.state, 'done')
self.assertEqual(self.custody_line.receiving_quantity, 1.0, "Original line receiving qty should be updated")
self.assertEqual(self.custody_delivered.state, 'done', "Original custody should be done as all items returned")
def test_02_custody_receiving_with_deduction(self):
receiving = self.env['hr.custody.receiving'].create({
'employee_id': self.employee.id,
'salary_rule_id': self.salary_rule.id,
})
receiving._get_custody_line_domain()
line = receiving.return_custody_line_ids[0]
line.deduction_amount = 500.0
receiving.compute_deduction_amount()
self.assertEqual(receiving.deduction_amount, 500.0)
self.assertTrue(receiving.salary_rule_flag)
receiving.state = 'approve'
receiving.done()
self.assertTrue(receiving.advantage_line_id, "Should create advantage line")
self.assertEqual(receiving.advantage_line_id.amount, 500.0, "Advantage amount mismatch")
self.assertEqual(receiving.advantage_line_id.contract_advantage_id, self.contract)
def test_03_validation_over_quantity(self):
receiving = self.env['hr.custody.receiving'].create({
'employee_id': self.employee.id,
})
receiving._get_custody_line_domain()
line = receiving.return_custody_line_ids[0]
line.quantity = 2.0
receiving.state = 'approve'
with self.assertRaises(ValidationError):
receiving.done()
def test_04_reset_to_draft(self):
receiving = self.env['hr.custody.receiving'].create({
'employee_id': self.employee.id,
'salary_rule_id': self.salary_rule.id,
})
receiving._get_custody_line_domain()
receiving.return_custody_line_ids[0].deduction_amount = 100.0
receiving.compute_deduction_amount()
receiving.state = 'approve'
receiving.done()
self.assertEqual(receiving.state, 'done')
self.assertTrue(receiving.advantage_line_id)
receiving.set_to_draft()
self.assertEqual(receiving.state, 'draft')
self.assertFalse(receiving.advantage_line_id.exists(), "Advantage line should be deleted")
self.assertEqual(self.custody_line.receiving_quantity, 0.0, "Original qty should be reverted")
self.assertEqual(self.custody_delivered.state, 'approve')
def test_05_unlink_protection(self):
receiving = self.env['hr.custody.receiving'].create({
'employee_id': self.employee.id,
})
receiving.send()
with self.assertRaises(ValidationError):
receiving.unlink()

View File

@ -0,0 +1,2 @@
# -*- coding: utf-8 -*-
from . import models

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8"?>
<odoo>
<data>
<record id="apprisal_kpi_group" model="res.groups">
<field name="name">Menu apprisal hide/show</field>
</record>
<record id="group_appraisal_responsabil" model="res.groups">
<field name="name">Goals Responsible</field>
<field name="category_id" ref="exp_hr_appraisal.module_category_hr_appraisal"/>
<field name="implied_ids" eval="[(4, ref('base.group_user')),(4, ref('exp_hr_appraisal.group_appraisal_employee'))]"/>
</record>
<record id="extended_kpi_category_rule" model="ir.rule">
<field name="name">Extended KPI Category Rule</field>
<field name="model_id" ref="kpi_scorecard.model_kpi_category"/>
<field name="domain_force">[
'|',
('company_id','=', False),
('company_id', 'in', company_ids),
]
</field>
<field name="groups"
eval="[(4, ref('base.group_user')), (4, ref('exp_hr_appraisal.group_appraisal_employee')),(4, ref('exp_hr_appraisal.group_appraisal_manager')),(4, ref('exp_hr_appraisal.group_appraisal_user'))]"/>
</record>
<record id="extended_kpi_item_rule" model="ir.rule">
<field name="name">Extended KPI Category Rule</field>
<field name="model_id" ref="kpi_scorecard.model_kpi_item"/>
<field name="domain_force">[
'|',
('company_id','=', False),
('company_id', 'in', company_ids),
]
</field>
<field name="groups"
eval="[(4, ref('base.group_user')), (4, ref('exp_hr_appraisal.group_appraisal_employee')),(4, ref('exp_hr_appraisal.group_appraisal_manager')),(4, ref('exp_hr_appraisal.group_appraisal_user'))]"/>
</record>
<!--add record rule for skill apprisal,employee apprisal -->
<record id="hr_employee_appraisal_kpi_employee_rule" model="ir.rule">
<field name="name">Employee: views its Skill appraisals only</field>
<field name="model_id" ref="model_skill_appraisal"/>
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="hr_employee_appraisal_goal_kpi_employee_rule" model="ir.rule">
<field name="name">Employee: views its Goal appraisals only</field>
<field name="model_id" ref="model_employee_performance_evaluation"/>
<field name="domain_force">[('employee_id.user_id','=',user.id)]</field>
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
</record>
<record id="hr_employee_kpi_appraisal_manager_rule" model="ir.rule">
<field name="name">Manager: views Skill appraisals of its subordinates</field>
<field name="model_id" ref="model_skill_appraisal"/>
<field name="domain_force">['|','|',('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])]
</field>
<field name="groups"
eval="[(4, ref('hr_base.group_department_manager')),(4, ref('hr_base.group_division_manager'))]"/>
</record>
<record id="hr_employee_kpi_appraisal_goals_manager_rule" model="ir.rule">
<field name="name">Manager: views Goals appraisals of its subordinates</field>
<field name="model_id" ref="model_employee_performance_evaluation"/>
<field name="domain_force">['|','|',('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])]
</field>
<field name="groups"
eval="[(4, ref('hr_base.group_department_manager')),(4, ref('hr_base.group_division_manager'))]"/>
</record>
<record id="hr_employee_skill_appraisal_all_rule" model="ir.rule">
<field name="name"> Manager: views Skills appraisals of all subordinates </field>
<field name="model_id" ref="model_employee_performance_evaluation"/>
<field name="domain_force">[(1 ,'=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_base.group_executive_manager')),
(4, ref('hr_base.group_general_manager')),
(4, ref('exp_hr_appraisal.group_appraisal_manager')),
(4, ref('hr.group_hr_user'))]"/>
</record>
<record id="hr_employee_goal_appraisal_all_rule" model="ir.rule">
<field name="name"> Manager: views Goals appraisals of all subordinates </field>
<field name="model_id" ref="model_skill_appraisal"/>
<field name="domain_force">[(1 ,'=', 1)]</field>
<field name="groups" eval="[(4, ref('hr_base.group_executive_manager')),
(4, ref('hr_base.group_general_manager')),
(4, ref('exp_hr_appraisal.group_appraisal_manager')),
(4, ref('hr.group_hr_user'))]"/>
</record>
<!--#################################################################################################################################################################-->
<!-- end -->
</data>
</odoo>

View File

@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_mark2 access_marks2 model_mark_mark base.group_user 1 1 1 1
3 access_mark3 access_marks3 model_mark_mark exp_hr_appraisal.group_appraisal_employee 1 1 1 1
4 access_mark4 access_marks4 model_mark_mark exp_hr_appraisal.group_appraisal_manager 1 1 1 1
5 access_mark5 access_marks5 model_mark_mark exp_hr_appraisal.group_appraisal_user 1 1 1 1
6 access_kpi_p7 access_kpi_ps8 model_kpi_period_notes base.group_user 1 1 1 1
7 access_kpi_p7 access_kpi_ps8 model_kpi_period_notes exp_hr_appraisal.group_appraisal_manager 1 1 1 1
8 access_kpi_p7 access_kpi_ps8 model_kpi_period_notes exp_hr_appraisal.group_appraisal_user 1 1 1 1
9 access_kpi_p7 access_kpi_ps8 model_kpi_period_notes exp_hr_appraisal.group_appraisal_employee 1 1 1 1
10 access_kpi_p535 access_kpi_ps185 model_skill_skill exp_hr_appraisal.group_appraisal_manager 1 1 1 1
11 access_kpi_p535 access_kpi_ps185 model_skill_skill exp_hr_appraisal.group_appraisal_user 1 1 1 1
12 access_kpi_p54435 access_kpi_ps18555 model_skill_item base.group_user 1 1 1 1
13 access_kpi_p544355 access_kpi_ps189555 model_skill_item_table base.group_user 1 1 1 1
14 access_kpi_p544385 access_kpi_ps179555 model_skill_item_employee_table base.group_user 1 1 1 1
15 access_kpi_p53577 access_kpi_ps17785 model_item_item base.group_user 1 1 1 1
16 access_kpi_p53577 access_kpi_ps17785 model_item_item exp_hr_appraisal.group_appraisal_user 1 1 1 1
17 access_kpi_p53577 access_kpi_ps17785 model_item_item exp_hr_appraisal.group_appraisal_manager 1 1 1 1
18 access_kpi_p53577 access_kpi_ps17785 model_item_item exp_hr_appraisal.group_appraisal_employee 1 1 1 1
19 access_kpi_p5357b7p access_kpbi_ps17785p model_skill_appraisal base.group_user 1 1 1 1
20 access_kpi_p5357b7 access_kpbi_ps17785 model_skill_appraisal exp_hr_appraisal.group_appraisal_manager 1 1 1 1
21 access_kpi_p5357b7 access_kpbi_ps17785 model_skill_appraisal exp_hr_appraisal.group_appraisal_user 1 1 1 1
22 access_kpi_p5357b7 access_kpbi_ps17785 model_skill_appraisal exp_hr_appraisal.group_appraisal_employee 1 0 0 1
23 access_kpi_p5357b7d access_kpbi_ps1778d5 model_skill_appraisal hr_base.group_division_manager 1 1 1 1
24 access_kpi_p535d7b7 access_kpbi_ps17d785 model_skill_appraisal hr_base.group_department_manager 1 1 1 1
25 access_kpi_p5357bd7 access_kpbi_ps1778d5 model_skill_appraisal hr.group_hr_user 1 1 1 0
26 access_kpi_emp_performansel1 access_kpbi_emp_perfomance_evalution39 model_employee_performance_evaluation base.group_user 1 1 1 0
27 access_kpi_emp_performanse1 access_kpbi_emp_perfomance_evalution3 model_employee_performance_evaluation exp_hr_appraisal.group_appraisal_manager 1 1 1 1
28 access_kpi_emp_performanse1 access_kpbi_emp_perfomance_evalution3 model_employee_performance_evaluation exp_hr_appraisal.group_appraisal_user 1 1 1 1
29 access_kpi_emp_performanse1 access_kpbi_emp_perfomance_evalution3 model_employee_performance_evaluation exp_hr_appraisal.group_appraisal_employee 1 0 0 1
30 access_kpi_emp_performanse11 access_kpbi_emp_perfomance_evalution33 model_employee_performance_evaluation hr_base.group_division_manager 1 1 1 1
31 access_kpi_emp_performanse12 access_kpbi_emp_perfomance_evalutionr3 model_employee_performance_evaluation hr_base.group_department_manager 1 1 1 1
32 access_kpi_emp_performanse13 access_kpbi_emp_perfomance_evalution53 model_employee_performance_evaluation hr.group_hr_user 1 1 1 0
33 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
34 access_kpi_emp_goals11 access_kpbi_emp_goals1 model_years_employee_goals base.group_user 1 1 1 0
35 access_kpi_emp_goals1 access_kpbi_emp_goals11 model_years_employee_goals exp_hr_appraisal.group_appraisal_manager 1 1 1 1
36 access_kpi_emp_goals5 access_kpbi_emp_goals151 model_years_employee_goals exp_hr_appraisal.group_appraisal_user 1 1 1 0
37 access_kpi_emp_goals access_kpbi_emp_goals1 model_years_employee_goals hr_base.group_department_manager 1 1 0 0
38 access_kpi_emp_goals_period access_kpbi_emp_period1 model_period_goals base.group_user 1 1 1 1
39 access_kpi_perecentage access_kpbi_perecentage1 model_job_class_apprisal base.group_user 1 1 1 1
40 access_kpi_category access_kpi_category kpi_scorecard.model_kpi_category base.group_user 1 1 1 1
41 access_kpi_category access_kpbi_category1 kpi_scorecard.model_kpi_category kpi_scorecard.group_kpi_admin 1 1 1 1
42 access_kpi_category access_kpbi_category1 kpi_scorecard.model_kpi_category exp_hr_appraisal.group_appraisal_manager 1 1 1 1
43 access_kpi_category1 access_kpbi_category2 kpi_scorecard.model_kpi_category exp_hr_appraisal.group_appraisal_user 1 1 1 0
44 access_kpi_category21 access_kpbi_category25 kpi_scorecard.model_kpi_category exp_hr_appraisal.group_appraisal_employee 1 0 0 0
45 access_kpi_period access_kpbi_period1 kpi_scorecard.model_kpi_period base.group_user 1 0 0 0
46 access_kpi_period access_kpbi_period1 kpi_scorecard.model_kpi_period kpi_scorecard.group_kpi_admin 1 1 1 1
47 access_kpi_period access_kpbi_period1 kpi_scorecard.model_kpi_period exp_hr_appraisal.group_appraisal_manager 1 1 1 1
48 access_kpi_category1 access_kpbi_period2 kpi_scorecard.model_kpi_period exp_hr_appraisal.group_appraisal_user 1 1 1 0
49 access_kpi_category21 access_kpbi_period25 kpi_scorecard.model_kpi_period exp_hr_appraisal.group_appraisal_employee 1 0 0 0
50 access_kpi_item access_kpbi_item1 kpi_scorecard.model_kpi_item base.group_user 1 0 0 0
51 access_kpi_item access_kpbi_item1 kpi_scorecard.model_kpi_item kpi_scorecard.group_kpi_admin 1 1 1 1
52 access_kpi_item access_kpbi_item1 kpi_scorecard.model_kpi_item exp_hr_appraisal.group_appraisal_manager 1 1 1 1
53 access_kpi_item1 access_item2 kpi_scorecard.model_kpi_item exp_hr_appraisal.group_appraisal_user 1 1 1 0
54 access_kpi_item21 access_kpbi_item25 kpi_scorecard.model_kpi_item exp_hr_appraisal.group_appraisal_employee 1 0 0 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@ -0,0 +1,22 @@
@media (min-width: 768px){
.rtl .navbar-right{
float: left !important;
}
.rtl .navbar-right .dropdown .dropdown-menu{
right: auto !important;
left: 0 !important;
}
.rtl .navbar-left{
float: right !important;
}
.rtl .navbar-left .dropdown .dropdown-menu{
left: auto !important;
right: 0 !important;
}
.navbar-nav.navbar-right:last-child{
margin-left: auto;
}
.rtl .pull-left{
float: right !important;
}
}

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="apprisal_percentag_form_view" model="ir.ui.view">
<field name="name">apprisal.apprisal_percentag.form</field>
<field name="model">job.class.apprisal</field>
<field name="arch" type="xml">
<form string="Apprisal Percentag">
<sheet>
<group>
<group>
<field name="name"/>
<field name="percentage_kpi" widget="percentage" />
<field name="percentage_skills" widget="percentage" />
<field name="job_ids"/>
</group>
</group>
</sheet>
</form>
</field>
</record>
<record id="apprisal_percentag_tree_view" model="ir.ui.view">
<field name="name">apprisal.apprisal_percentag.tree</field>
<field name="model">job.class.apprisal</field>
<field name="arch" type="xml">
<list string="ModelTitle">
<field name="name"/>
<field name="percentage_kpi"/>
<field name="percentage_skills"/>
</list>
</field>
</record>
<!-- <record id="apprisal_percentag_search_view" model="ir.ui.view">-->
<!-- <field name="name">ProjectName.apprisal_percentag.search</field>-->
<!-- <field name="model">ProjectName.apprisal_percentag</field>-->
<!-- <field name="arch" type="xml">-->
<!-- <search string="Apprisal Percentag">-->
<!-- <group expand="1" string="Group By">-->
<!-- <filter string="Example Field" name="example_field" domain="[]"-->
<!-- context="{'group_by':'example_field'}"/>-->
<!-- </group>-->
<!-- </search>-->
<!-- </field>-->
<!-- </record>-->
<record id="apprisal_percentag_act_window1" model="ir.actions.act_window">
<field name="name">Apprisal Percentag</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">job.class.apprisal</field>
<field name="view_mode">list,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
There is no examples click here to add new ModelTitle.
</p>
</field>
</record>
<menuitem name="Appraisal Percentage"
id="menu_kpi_percentage"
parent="exp_hr_appraisal.appraisal_configuration"
action="apprisal_percentag_act_window1"
sequence="1" groups="exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_user,exp_hr_appraisal.group_appraisal_employee"
/>
</data>
</odoo>

View File

@ -0,0 +1,223 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="employee_apprisal_extend" model="ir.ui.view">
<field name="name">employee.apprisal.form.extend</field>
<field name="model">hr.employee.appraisal</field>
<field name="inherit_id" ref="exp_hr_appraisal.hr_appraisal_form_view"/>
<field name="priority" eval="8"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='appraisal_result']" position="replace">
</xpath>
<xpath expr="//field[@name='appraisal_date']" position="after">
<field name="year_id"/>
<field name="goals_mark"/>
<field name="skill_mark"/>
<field name="total_score"/>
<field name="appraisal_result"/>
</xpath>
<xpath expr="//field[@name='great_level']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='level_achieved']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='level_achieved_percentage']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='manager_appraisal_line_id']" position="replace">
<!-- <attribute name="invisible">1</attribute>-->
</xpath>
<xpath expr="//field[@name='appraisal_plan_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='appraisal_type']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='standard_appraisal_employee_line_ids']" position="replace">
<!-- <attribute name="invisible">1</attribute>-->
</xpath>
<xpath expr="//field[@name='is_manager']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='date_from']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='date_to']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//sheet/group[2]" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//button[@name='recompute_values_level_achieved']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='employee_id']" position="after">
<field name="manager_id"/>
<field name="department_id"/>
<field name="job_id"/>
</xpath>
<xpath expr="//sheet/group[1]" position="after">
<group>
<group>
<button name="compute_apprisal" string="Compute Apprisal" type="object" class="oe_highlight"
icon="fa-cogs"/>
</group>
</group>
<notebook>
<page string="Goals">
<field name="goal_ids">
<list create="0" delete="0" editable="bottom">
<field name="kpi_id" width="12"
options='{"no_open": False,"no_create_edit": True,"no_create":True}'/>
<field name="weight" sum="Total Weight" width="12"/>
<field name="year_target" sum="Total Target" width="12"/>
<field name="done" width="12"/>
<field name="choiec" width="12"/>
</list>
</field>
</page>
<page string="Skills">
<field name="skill_ids">
<list create="0" delete="0" editable="bottom">
<field readonly="1" force_save='1' name="item_id" width="12"
options='{"no_open": True,"no_create_edit": True}'/>
<field readonly="1" force_save='1' name="name" width="12"/>
<field readonly="1" force_save='1' name="level" width="12"/>
<field force_save='1' name="mark_avg" width="12"/>
</list>
</field>
</page>
<page string="Notes">
<field widget="html" required="0" name="notes"/>
</page>
</notebook>
</xpath>
</field>
</record>
<record id="employee_apprisal_view_tree" model="ir.ui.view">
<field name="name">employee_apprisal.extend.view.tree</field>
<field name="model">hr.employee.appraisal</field>
<field name="inherit_id" ref="exp_hr_appraisal.hr_appraisal_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='appraisal_plan_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='employee_id']" position="after">
<field name="manager_id"/>
<field name="department_id"/>
<field name="job_id"/>
<field name="year_id"/>
</xpath>
<xpath expr="//field[@name='state']" position="before">
<field name="skill_mark"/>
<field name="goals_mark"/>
<field name="total_score"/>
<field name="apprisal_result"/>
</xpath>
</field>
</record>
<record id="employee_apprisal_view_tree2" model="ir.ui.view">
<field name="name">employee_apprisal.extend.view.tree</field>
<field name="model">hr.group.employee.appraisal</field>
<field name="inherit_id" ref="exp_hr_appraisal.employee_appraisal_tree_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='totals_great_level']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='totals_level_achieved']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='totals_level_achieved_percentage']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='totals_appraisal_result']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='appraisal_plan_id']" position="replace">
<field name="year_id"/>
</xpath>
</field>
</record>
<!-- Inherit Form View to Modify it -->
<record id="group_employee_apprisal_extend" model="ir.ui.view">
<field name="name">employee.apprisal.group.extend</field>
<field name="model">hr.group.employee.appraisal</field>
<field name="inherit_id" ref="exp_hr_appraisal.employee_appraisal_form_view"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='date']" position="after">
<field name="year_id"/>
</xpath>
<xpath expr="//field[@name='appraisal_plan_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='appraisal_type']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='totals_great_level']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='totals_level_achieved']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='totals_level_achieved_percentage']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='totals_appraisal_result']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='state']" position="attributes">
<attribute name="statusbar_visible">
draft,gen_appraisal,finish_appraisal,hr_approval,gm_approval,done
</attribute>
</xpath>
<xpath expr="//field[@name='appraisal_id']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//separator[2]" position="replace">
</xpath>
<xpath expr="//separator[1]" position="replace">
</xpath>
<xpath expr="//field[@name='employee_ids']" position="replace">
<notebook>
<page string="Employees">
<field name="employee_ids" string="" readonly="state != 'draft'" >
<list>
<field name="name" string="Employee name"/>
<field name="department_id" string="Department"/>
<field name="job_id" string="Job title"/>
</list>
</field>
</page>
<page string="Apprisal">
<field name="appraisal_ids" string=""
readonly="state != 'draft'" invisible="state == 'draft'">
<list editable="bottom">
<field name="employee_id" width='12' string="Employee"/>
<field name="department_id" width='12' string=""/>
<field name="appraisal_date" width='12' string=""/>
<field name="year_id" width='12' string=""/>
<field name="skill_mark" width='12' string=""/>
<field name="goals_mark" width='12' string=""/>
<field name="total_score" width='12' string=""/>
<field name="apprisal_result" width='12' string=""/>
</list>
</field>
</page>
</notebook>
</xpath>
</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_emplo_evalu_form" model="ir.ui.view">
<field name="name">evalution.employee.goals.form1</field>
<field name="model">employee.performance.evaluation</field>
<field name="arch" type="xml">
<form string="Evaluation Employee Goals">
<header>
<button string="Send" groups='hr_base.group_division_manager' invisible="state != 'draft'" class="oe_highlight" type="object" name="send"/>
<button string="Accept" invisible="state != 'dir_manager'" class="oe_highlight" type="object" name="action_approval"/>
<button string="refuse" invisible="state != 'dir_manager'" class="oe_highlight" type="object" name="action_refuse"/>
<button string="Accept" groups='hr_base.group_department_manager' invisible="state != 'wait_dir_manager'" class="oe_highlight" type="object" name="action_approval"/>
<button string="refuse" groups='hr_base.group_department_manager' invisible="state != 'wait_dir_manager'" class="oe_highlight" type="object" name="action_refuse"/>
<button string="Accept" groups='hr.group_hr_user' invisible="state != 'wait_hr_manager'" class="oe_highlight" type="object" name="action_approval"/>
<button string="refuse" groups='hr.group_hr_user' invisible="state != 'wait_hr_manager'" class="oe_highlight" type="object" name="action_refuse"/>
<button string="Reset To Draft" invisible="state not in ['approve', 'refuse']" class="oe_highlight" type="object" name="reset_draft"/>
<field name="state" required="1" statusbar_visible="draft,wait_dir_manager,wait_hr_manager,approve,refuse" widget="statusbar"/>
</header>
<sheet>
<group>
<group>
<field name="employee_id" required="1"/>
<field name="manager_id" required="1"/>
<field name="department_id" required="1"/>
<field name="job_id" required="1"/>
</group>
<group>
<field name="date_apprisal"/>
<field name="year_id" required="1" options='{"no_open": True,"no_create_edit": True,"no_create":True}'/>
<field name="period_goals_id" domain="[('kpi_goal_period_id', '=', year_id),('kpi_period_id','=',False)]" invisible="not year_id" required="1" options='{"no_open": False,"no_create_edit": True,"no_create":True}'/>
<field name="mark_apprisal" decoration-bf="1" required="1"/>
<!-- <field name="total" decoration-bf="1" />-->
</group>
</group>
<notebook>
<page string="Employee Goals">
<button string="Select Goals" class="oe_highlight" type="object" name="onchange_emp_goal_ids"/>
<field name="emp_goal_ids">
<list create="0" delete="0" editable="bottom">
<field name="kpi_id" width="12" options='{"no_open": False,"no_create_edit": True,"no_create":True}'/>
<field name="weight" sum="Total Weight" width="12"/>
<field name="target" sum="Total Target" width="12"/>
<field name="done" width="12"/>
<field decoration-bf="1" name="mark_evaluation" width="12"/>
</list>
</field>
</page>
<page string="Recommendations">
<field widget="html" required="0" name="recommendations"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_evalution_goals_employee_tree1" model="ir.ui.view">
<field name="name">evalution.employee.goals.tree1</field>
<field name="model">employee.performance.evaluation</field>
<field name="arch" type="xml">
<list string="Year Employee Goals" decoration-info="state == 'draft'" decoration-danger="state == 'refuse'" decoration-success="state== 'approve'" >
<field name="employee_id"/>
<field name="department_id"/>
<field name="job_id"/>
<field name="year_id"/>
<field name="period_goals_id"/><field name="date_apprisal"/>
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-danger="state == 'refuse'" decoration-success="state== 'approve'"/>
<field name="mark_apprisal" sum='Total' decoration-bf="1"/>
</list>
</field>
</record>
<!-- Menu Action -->
<record id="action_evalution_goal_emp" model="ir.actions.act_window">
<field name="name">Evaluation Employee Goals</field>
<field name="res_model">employee.performance.evaluation</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_evalution_employee_goals_" sequence="2" groups="exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_user,exp_hr_appraisal.group_appraisal_employee" name="Employee Goals Appraisal" parent="exp_hr_appraisal.appraisal_menu_id" action="action_evalution_goal_emp"/>
</odoo>

View File

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="kpi_category_inherit" model="ir.ui.view">
<field name="name">kpi.category.form</field>
<field name="model">kpi.category</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_category_view_form"/>
<field name="priority" eval="8"/>
<field name="arch" type="xml">
<xpath expr="//notebook/page[1]" position="attributes">
<attribute name="groups">kpi_scorecard.group_kpi_admin</attribute>
</xpath>
</field>
</record>
<menuitem name="Goals"
id="menu_kpi_categories"
parent="exp_hr_appraisal.appraisal_configuration"
action="kpi_scorecard.kpi_category_action"
sequence="1"
groups="kpi_scorecard.group_kpi_admin,exp_hr_appraisal.group_appraisal_employee,exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_user"/>
<menuitem name="KPI"
id="menu_kpi_kpi"
parent="exp_hr_appraisal.appraisal_configuration"
action="kpi_scorecard.kpi_item_action"
sequence="1"
groups="kpi_scorecard.group_kpi_admin,exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_employee,exp_hr_appraisal.group_appraisal_user"
/>
<menuitem name="Periods"
id="menu_kpi_period"
parent="exp_hr_appraisal.appraisal_configuration"
action="kpi_scorecard.kpi_period_action"
sequence="1"
groups="kpi_scorecard.group_kpi_admin,exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_user,exp_hr_appraisal.group_appraisal_employee"
/>
<menuitem name="Goals and Skills" id="menu_kpi_goal_skill" parent="exp_hr_appraisal.appraisal_menu_id" sequence="3"/>
<!-- todo start -->
<!-- hide menu -->
<record model="ir.ui.menu" id="exp_hr_appraisal.appraisal_plan_menu">
<field name="groups_id" eval="[(6,0,[ref('exp_hr_appraisal_kpi.apprisal_kpi_group')])]"/>
</record>
<record model="ir.ui.menu" id="exp_hr_appraisal.appraisal_setting_menu">
<field name="groups_id" eval="[(6,0,[ref('exp_hr_appraisal_kpi.apprisal_kpi_group')])]"/>
</record>
<record model="ir.ui.menu" id="exp_hr_appraisal.appraisal_menu">
<field name="groups_id" eval="[(6,0,[ref('exp_hr_appraisal_kpi.apprisal_kpi_group')])]"/>
</record>
<record model="ir.ui.menu" id="exp_hr_appraisal.appraisal_degree_menu">
<field name="groups_id" eval="[(6,0,[ref('exp_hr_appraisal_kpi.apprisal_kpi_group')])]"/>
</record>
<!-- todo end -->
</odoo>

View File

@ -0,0 +1,51 @@
<odoo>
<!-- Inherit Form View to Modify it -->
<record id="kpi_item_tree_extend" model="ir.ui.view">
<field name="name">kpi.item.tree.extend</field>
<field name="model">kpi.item</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_item_view_tree"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='formula_warning']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
</field>
</record>
<record id="hpi_item_extend" model="ir.ui.view">
<field name="name">kpi.form.extend</field>
<field name="model">kpi.item</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_item_view_form"/>
<field name="arch" type="xml">
<!-- add new tab -->
<xpath expr="//field[@name='formula_warning']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='formula']" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//h2" position="attributes">
<attribute name="invisible">1</attribute>
</xpath>
<xpath expr="//field[@name='category_id']" position="after">
<field name="department_item_id"/>
<field name="responsible_item_id"/>
<field name="method_of_calculate"/>
</xpath>
<xpath expr="//notebook" position="inside">
<page string="Marks" groups="base.group_user,exp_hr_appraisal.group_appraisal_user,exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_employee">
<field name="mark_ids">
<list editable="bottom">
<field name="choiec" width="12"/>
<field name="target" string="From(Done)" width="12"/>
<field name="to" width="12"/>
</list>
</field>
</page>
</xpath>
<xpath expr="//notebook/page[2]" position="attributes">
<attribute name="groups">kpi_scorecard.group_kpi_admin</attribute>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record id="kpi_period_form_extend" model="ir.ui.view">
<field name="name">kpi.period.form.extend</field>
<field name="model">kpi.period</field>
<field name="inherit_id" ref="kpi_scorecard.kpi_period_view_form"/>
<field name="arch" type="xml">
<xpath expr="//page[1]" position="attributes">
<attribute name="groups">kpi_scorecard.group_kpi_admin</attribute>
</xpath>
<xpath expr="//page[2]" position="attributes">
<attribute name="groups">kpi_scorecard.group_kpi_admin</attribute>
</xpath>
<xpath expr="//button[@name='%(kpi_scorecard.kpi_copy_template_action)d']" position="attributes">
<attribute name="groups">kpi_scorecard.group_kpi_admin</attribute>
</xpath>
<xpath expr="//page[2]" position="after">
<page string="Goals Period">
<field name="kpi_goals_periods_ids">
<list editable="bottom">
<field name="sequence" width="4"/>
<field name="name" width="8"/>
<field name="date_start_k" width="12"/>
<field name="date_end_k" width="12"/>
<button string="Create Apprisal" width="" class="oe_highlight" type="object" name="create_apprisal_goals_employee"/>
</list>
</field>
</page>
<page string="Skills Period">
<field name="kpi_periods_ids">
<list editable="bottom">
<field name="name" width="12"/>
<field name="date_start_k" width="12"/>
<field name="date_end_k" width="12"/>
</list>
</field>
</page>
</xpath>
</field>
</record>
</odoo>

View File

@ -0,0 +1,109 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Skill Form View -->
<record id="view_skill_form" model="ir.ui.view">
<field name="name">skill.form</field>
<field name="model">skill.skill</field>
<field name="arch" type="xml">
<form string="Skill">
<sheet>
<group>
<field name="name"/>
<field name="description"/>
</group>
<notebook>
<page string="Items">
<field widget="section_and_note_one2many" name="items_ids">
<list editable="bottom">
<field name="sequence" widget="handle"/>
<field name="display_type" invisible="1"/>
<field name="item_id"/>
<field widget="section_and_note_text" name="name"/>
<field name="level"/>
<control>
<create name="add_product_control" string="Add a Item "/>
<create name="add_section_control" string="Add a section " context="{'default_display_type': 'line_section'}"/>
<create name="add_note_control" string="Add a note" context="{'default_display_type': 'line_note'}"/>
</control>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Skill Tree View -->
<record id="view_skill_tree" model="ir.ui.view">
<field name="name">skill.tree</field>
<field name="model">skill.skill</field>
<field name="arch" type="xml">
<list string="Skills">
<field name="name"/>
<field name="description"/>
</list>
</field>
</record>
<record id="skill_search_view" model="ir.ui.view">
<field name="name">kpi.skill.search</field>
<field name="model">skill.skill</field>
<field name="arch" type="xml">
<search string="Skill">
<field name="name"/>
</search>
</field>
</record>
<record id="skill_act_window" model="ir.actions.act_window">
<field name="name">Skills</field>
<field name="type">ir.actions.act_window</field>
<field name="res_model">skill.skill</field>
<field name="view_mode">tree,form</field>
<field name="help" type="html">
<p class="oe_view_nocontent_create">
There is no examples click here to add new Skill.
</p>
</field>
</record>
<!-- form inherit -->
<!-- Inherit Form View to Modify it -->
<record id="hr_job_from_extend" model="ir.ui.view">
<field name="name">hr.job.extend</field>
<field name="model">hr.job</field>
<field name="inherit_id" ref="hr.view_hr_job_form"/>
<field name="arch" type="xml">
<xpath expr="//notebook" position="inside">
<page string="Skills">
<field name="item_job_ids" domain="[('display_type','=',False)]"/>
</page>
</xpath>
</field>
</record>
<record id="item_tree_view_tree" model="ir.ui.view">
<field name="name">item.tree.view.tree</field>
<field name="model">skill.item</field>
<field name="arch" type="xml">
<list string="item_tree_tree">
<field name="skill_id"/>
<field name="item_id"/>
<field name="name"/>
<field name="level"/>
<field name="mark" invisible="1"/>
</list>
</field>
</record>
<!-- Inherit the menu -->
<!-- <record model="ir.ui.menu" id="hr_base_reports.appraisal_report_menu">-->
<!-- <field name="groups_id" eval="[(4, ref('exp_hr_appraisal.group_appraisal_user,exp_hr_appraisal.group_appraisal_manager'))]"/>-->
<!-- </record>-->
<!-- end from -->
<menuitem name="Skills" id="skill_menu" sequence="1" parent="exp_hr_appraisal_kpi.menu_kpi_goal_skill" groups="exp_hr_appraisal.group_appraisal_user,exp_hr_appraisal.group_appraisal_manager"
action="skill_act_window"/>
</odoo>

View File

@ -0,0 +1,87 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_skill_appraisal_form" model="ir.ui.view">
<field name="name">skill.appraisal.form</field>
<field name="model">skill.appraisal</field>
<field name="arch" type="xml">
<form string="Skill Appraisal">
<header>
<button string="Send" groups='hr_base.group_division_manager' invisible="state not in ['draft']" class="oe_highlight" type="object" name="send"/>
<button string="Accept" invisible="state not in ['dir_manager']" class="oe_highlight" type="object" name="action_approval"/>
<button string="refuse" invisible="state not in ['dir_manager']" class="oe_highlight" type="object" name="action_refuse"/>
<button string="Accept" groups='hr_base.group_department_manager' invisible="state not in ['wait_dir_manager']" class="oe_highlight" type="object" name="action_approval"/>
<button string="refuse" groups='hr_base.group_department_manager' invisible="state not in ['wait_dir_manager']" class="oe_highlight" type="object" name="action_refuse"/>
<button string="Accept" groups='hr.group_hr_user' invisible="state not in ['wait_hr_manager']" class="oe_highlight" type="object" name="action_approval"/>
<button string="refuse" groups='hr.group_hr_user' invisible="state not in ['wait_hr_manager']" class="oe_highlight" type="object" name="action_refuse"/>
<button string="Reset To Draft" invisible="state not in ['refuse','approve']" class="oe_highlight" type="object" name="reset_draft"/>
<field name="state" required='1' statusbar_visible="draft,dir_manager,wait_dir_manager,wait_hr_manager,approve,refuse" widget="statusbar"/>
</header>
<sheet>
<group>
<group>
<field name="employee_id" readonly="state not in ['draft']" required='1'/>
<field readonly="state not in ['draft']" name="department_id" required='1'/>
<field readonly="state not in ['draft']" name="job_id" required='1'/>
<field readonly="state not in ['draft']" name="manager_id" required='1'/>
</group>
<group>
<field readonly="state not in ['draft']" name="date_apprisal"/>
<field readonly="state not in ['draft']" name="year_id" required="1" options='{"no_open": True,"no_create_edit": True,"no_create":True}'/>
<field readonly="state != 'draft'" invisible=" not year_id" name="period" domain="[('kpi_period_id', '=',year_id),('kpi_goal_period_id','=',False)]" required="1" options='{"no_open": True,"no_create_edit": True,"no_create":True}'/>
<field readonly="state not in ['draft']" required='1' decoration-bf="1" name="avarage"/>
</group>
</group>
<notebook>
<page string="Items">
<field name="items_ids">
<list create="0" delete='0' editable="bottom">
<field readonly="1" force_save='1' name="item_id" width="12" options='{"no_open": True,"no_create_edit": True}'/>
<field readonly="1" force_save='1' name="name" width="12" />
<field readonly="1" force_save='1' name="level" width="12"/>
<field force_save='1' invisible="parent.state not in ['wait_dir_manager','draft']" name="mark" width="12"/>
</list>
</field>
</page>
<page string="Recommendations">
<field readonly="state not in ['draft']" widget="html" required="0" name="recommendations"/>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_skill_appraisal_tree" model="ir.ui.view">
<field name="name">skill.appraisal.tree</field>
<field name="model">skill.appraisal</field>
<field name="arch" type="xml">
<list string="Skill Appraisal" decoration-info="state == 'draft'" decoration-danger="state == 'refuse'" decoration-success="state== 'approve'">
<field name="employee_id"/>
<field name="department_id"/>
<field name="job_id"/>
<field name="manager_id"/>
<field name="period"/>
<field name="date_apprisal"/>
<field name="state" widget="badge" decoration-info="state == 'draft'" decoration-danger="state == 'refuse'" decoration-success="state== 'approve'"/>
<field name="avarage" decoration-bf="1"/>
</list>
</field>
</record>
<!-- Menu Action -->
<record id="action_skill_appraisal" model="ir.actions.act_window">
<field name="name">Skill Appraisal</field>
<field name="res_model">skill.appraisal</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu Item -->
<menuitem id="menu_skill_appraisal_list" groups="exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_user,exp_hr_appraisal.group_appraisal_employee" sequence="2" name="Employee Skill Appraisal" parent="exp_hr_appraisal.appraisal_menu_id" action="action_skill_appraisal"/>
</odoo>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<!-- Form View -->
<record id="view_emplo_goals_form" model="ir.ui.view">
<field name="name">years.employee.goals.form1</field>
<field name="model">years.employee.goals</field>
<field name="arch" type="xml">
<form string="Year Employee Goals">
<header>
<button string="Start Apprisal" invisible="state not in ['draft']" class="oe_highlight" type="object" name="apprisal"/>
<button string="Close" invisible="state not in ['apprisal']" class="oe_highlight" type="object" name="action_close"/>
<button string="Set To Dratt" invisible="state not in ['close','apprisal']" type="object" name="action_set_to_dratt"/>
<field name="state" required="1" statusbar_visible="draft,apprisal,close" widget="statusbar"/>
</header>
<sheet>
<group>
<group>
<field name="year_id" required="1"/>
<field name="employee_id" required="1"/>
<field name="department_id" required="1"/>
<field required="1" name="job_id"/>
</group>
<group>
<field required="1" name="category_id"/>
<field required="1" name="kpi_id" domain="[('category_id', '=',category_id)]"/>
<field required="1" name="responsible_item_id"/>
<field required="1" name="year_target"/>
<field required="1" invisible='1' name="method_of_calculate"/>
<field name="done" invisible="state in ['draft']" readonly="method_of_calculate not in ['undefined']"/>
<field name="choiec" invisible="state in ['draft']"/>
<field required="1" name="weight"/>
<field name="first_period_traget" invisible="1"/>
<field name="second_period_traget" invisible="1"/>
<field name="third_period_traget" invisible="1"/>
<field name="fourth_period_traget" invisible="1"/>
</group>
</group>
<notebook>
<page string="Period">
<field name="goals_period_ids" readonly="state == 'close'">
<list create="0" editable="bottom">
<field name="period_goals_id" width="12" options='{"no_open": False,"no_create_edit": True,"no_create":True}'/>
<field sum='Totat Traget' name="target" width="12"/>
<field name="done" width="12" column_invisible="parent.state == 'draft'" />
<field name="mark_evaluation" width="12" column_invisible="parent.state == 'draft'"/>
</list>
</field>
</page>
</notebook>
</sheet>
<chatter/>
</form>
</field>
</record>
<!-- Tree View -->
<record id="view_year_goals_employee_tree1" model="ir.ui.view">
<field name="name">years.employee.goals.tree1</field>
<field name="model">years.employee.goals</field>
<field name="arch" type="xml">
<list string="Year Employee Goals">
<field name="employee_id"/>
<field name="department_id"/>
<field name="job_id"/>
<field name="year_id"/>
<field name="category_id"/>
<field name="kpi_id"/>
<field name="responsible_item_id"/>
<field name="weight"/>
<field name="done" />
<field name="year_target" sum="Total"/>
<field name="state" decoration-info="state == 'draft'" decoration-primary="state== 'apprisal'" decoration-success="state== 'close'" widget="badge"/>
</list>
</field>
</record>
<!-- Menu Action -->
<record id="action_year_goal_emp" model="ir.actions.act_window">
<field name="name">Employee Goals</field>
<field name="res_model">years.employee.goals</field>
<field name="view_mode">list,form</field>
</record>
<!-- Menu Item -->
<menuitem groups="exp_hr_appraisal_kpi.group_appraisal_responsabil,exp_hr_appraisal.group_appraisal_manager,exp_hr_appraisal.group_appraisal_user" id="menu_year_employee_goals_list" sequence="2" name="Employee Goals" parent="exp_hr_appraisal_kpi.menu_kpi_goal_skill" action="action_year_goal_emp"/>
</odoo>

View File

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

View File

@ -28,10 +28,10 @@ class HrPayrollStructure(models.Model):
rule_ids = fields.Many2many('hr.salary.rule', 'hr_structure_salary_rule_rel', 'struct_id', 'rule_id',
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):

View File

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

View File

@ -0,0 +1,224 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError
from odoo import fields
from datetime import date, datetime, timedelta
class TestPayrollRules(TransactionCase):
def setUp(self):
super(TestPayrollRules, self).setUp()
self.env.user.tz = 'UTC'
self.company = self.env.ref('base.main_company')
self.category_basic = self.env['hr.salary.rule.category'].create({
'name': 'Basic',
'code': 'BASIC',
})
self.structure = self.env['hr.payroll.structure'].create({
'name': 'Test Structure',
'code': 'TEST_STRUCT',
'company_id': self.company.id,
})
attendances = []
for day in range(5): # 0=Monday to 4=Friday
attendances.append((0, 0, {
'name': 'Morning',
'dayofweek': str(day),
'hour_from': 8,
'hour_to': 12,
'day_period': 'morning',
}))
attendances.append((0, 0, {
'name': 'Afternoon',
'dayofweek': str(day),
'hour_from': 13,
'hour_to': 17,
'day_period': 'afternoon',
}))
self.calendar = self.env['resource.calendar'].create({
'name': 'Standard 40 Hours UTC',
'tz': 'UTC',
'hours_per_day': 8.0,
'attendance_ids': attendances,
'company_id': self.company.id,
})
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee Payroll',
'company_id': self.company.id,
'resource_calendar_id': self.calendar.id,
})
if self.employee.resource_id:
self.employee.resource_id.write({
'calendar_id': self.calendar.id,
'tz': 'UTC',
})
self.contract = self.env['hr.contract'].create({
'name': 'Contract for Test',
'employee_id': self.employee.id,
'struct_id': self.structure.id,
'wage': 5000.0,
'state': 'open',
'date_start': date.today() - timedelta(days=100),
'resource_calendar_id': self.calendar.id,
'schedule_pay': 'monthly',
'company_id': self.company.id,
})
self.payslip = self.env['hr.payslip'].create({
'employee_id': self.employee.id,
'contract_id': self.contract.id,
'struct_id': self.structure.id,
'date_from': date.today().replace(day=1),
'date_to': (date.today().replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1),
'company_id': self.company.id,
})
def test_satisfy_condition_python(self):
rule = self.env['hr.salary.rule'].create({
'name': 'Python Condition Rule',
'sequence': 10,
'code': 'PY_COND',
'category_id': self.category_basic.id,
'condition_select': 'python',
'condition_python': 'result = contract.wage > 3000',
'amount_select': 'fix',
'amount_fix': 100.0,
})
localdict = {'contract': self.contract, 'employee': self.employee}
self.assertTrue(rule._satisfy_condition(localdict))
rule.condition_python = 'result = contract.wage > 6000'
self.assertFalse(rule._satisfy_condition(localdict))
def test_satisfy_condition_range(self):
rule = self.env['hr.salary.rule'].create({
'name': 'Range Condition Rule',
'sequence': 10,
'code': 'RANGE_COND',
'category_id': self.category_basic.id,
'condition_select': 'range',
'condition_range': 'contract.wage',
'condition_range_min': 1000,
'condition_range_max': 6000,
'amount_select': 'fix',
'amount_fix': 100.0,
})
localdict = {'contract': self.contract}
self.assertTrue(rule._satisfy_condition(localdict))
self.contract.wage = 8000
self.assertFalse(rule._satisfy_condition(localdict))
def test_compute_rule_percentage(self):
rule = self.env['hr.salary.rule'].create({
'name': 'Percentage Rule',
'sequence': 10,
'code': 'PERCENT',
'category_id': self.category_basic.id,
'amount_select': 'percentage',
'amount_percentage_base': 'contract.wage',
'amount_percentage': 10.0,
'quantity': '1.0',
})
localdict = {'contract': self.contract}
amount, qty, rate = rule._compute_rule(localdict)
self.assertEqual(amount, 5000.0)
self.assertEqual(amount * qty * rate / 100.0, 500.0)
def test_compute_rule_python_code(self):
rule = self.env['hr.salary.rule'].create({
'name': 'Python Code Rule',
'sequence': 10,
'code': 'PY_CODE',
'category_id': self.category_basic.id,
'amount_select': 'code',
'amount_python_compute': 'result = contract.wage + 500',
})
localdict = {'contract': self.contract}
amount, qty, rate = rule._compute_rule(localdict)
self.assertEqual(amount, 5500.0)
def test_get_contract(self):
old_contract = self.env['hr.contract'].create({
'name': 'Old Contract',
'employee_id': self.employee.id,
'wage': 4000,
'state': 'close',
'date_start': date.today() - timedelta(days=400),
'date_end': date.today() - timedelta(days=200),
'resource_calendar_id': self.calendar.id,
'struct_id': self.structure.id,
})
date_from = date.today().replace(day=1)
date_to = (date.today().replace(day=1) + timedelta(days=32)).replace(day=1) - timedelta(days=1)
contract_ids = self.env['hr.payslip'].get_contract(self.employee, date_from, date_to)
self.assertIn(self.contract.id, contract_ids)
self.assertNotIn(old_contract.id, contract_ids)
def test_get_worked_day_lines(self):
today = date.today()
days_ahead = 0 - today.weekday()
if days_ahead <= 0:
days_ahead += 7
next_monday = today + timedelta(days=days_ahead)
date_from = next_monday
date_to = next_monday + timedelta(days=4)
try:
worked_days = self.env['hr.payslip'].get_worked_day_lines(self.contract, date_from, date_to)
work_entry = next((item for item in worked_days if item['code'] == 'WORK100'), None)
self.assertIsNotNone(work_entry, "WORK100 entry not found in result")
except AttributeError as e:
print(f"Skipping test_get_worked_day_lines due to missing dependency: {e}")
def test_payslip_line_compute_total(self):
line = self.env['hr.payslip.line'].create({
'slip_id': self.payslip.id,
'name': 'Test Line',
'code': 'TEST',
'contract_id': self.contract.id,
'salary_rule_id': self.env['hr.salary.rule'].search([], limit=1).id,
'employee_id': self.employee.id,
'quantity': 2.0,
'amount': 500.0,
'rate': 50.0,
'category_id': self.category_basic.id,
})
self.assertEqual(line.total, 500.0)
def test_get_inputs(self):
rule_with_input = self.env['hr.salary.rule'].create({
'name': 'Rule with Input',
'code': 'INPUT_RULE',
'category_id': self.category_basic.id,
'amount_select': 'fix',
'amount_fix': 0.0,
'sequence': 50,
})
self.env['hr.rule.input'].create({
'name': 'Commission Input',
'code': 'COMMISSION',
'input_id': rule_with_input.id,
})
self.structure.write({'rule_ids': [(4, rule_with_input.id)]})
date_from = date.today()
date_to = date.today()
inputs = self.env['hr.payslip'].get_inputs(self.contract, date_from, date_to)
found_input = next((i for i in inputs if i['code'] == 'COMMISSION'), None)
self.assertIsNotNone(found_input)
self.assertEqual(found_input['contract_id'], self.contract.id)

View File

@ -1175,7 +1175,7 @@ class HrOfficialMissionEmployee(models.Model):
def check_dates(self):
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)
)

View File

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

View File

@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError
from odoo.tests import tagged
from datetime import date, timedelta
@tagged('post_install', '-at_install')
class TestOfficialMission(TransactionCase):
def setUp(self):
super(TestOfficialMission, self).setUp()
self.company = self.env.company
self.account = self.env['account.account'].create({
'name': 'Mission Expense',
'code': '600000',
'account_type': 'expense',
'reconcile': False,
})
self.journal = self.env['account.journal'].create({
'name': 'Mission Journal',
'type': 'general',
'code': 'MISS',
'default_account_id': self.account.id,
})
self.emp_type = self.env['hr.contract.type'].create({'name': 'Permanent'})
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee',
'work_email': 'test@example.com',
'employee_type_id': self.emp_type.id,
})
self.contract = self.env['hr.contract'].create({
'name': 'Test Contract',
'employee_id': self.employee.id,
'wage': 5000,
'state': 'open',
})
self.employee.contract_id = self.contract
self.mission_type = self.env['hr.official.mission.type'].create({
'name': 'External Mission',
'duration_type': 'days',
'related_with_financial': True,
'type_of_payment': 'fixed',
'day_price': 100.0,
'journal_id': self.journal.id,
'account_id': self.account.id,
'transfer_by_emp_type': False,
'total_months': 12,
'max_request_number': 5,
})
def test_01_duration_calculation(self):
date_from = date.today()
date_to = date.today() + timedelta(days=4)
mission = self.env['hr.official.mission'].create({
'mission_type': self.mission_type.id,
'date_from': date_from,
'date_to': date_to,
})
mission._get_mission_no()
expected_days = 5
self.assertEqual(mission.date_duration, expected_days, "Duration in days calculated incorrectly")
def test_02_workflow_and_financials(self):
mission = self.env['hr.official.mission'].create({
'mission_type': self.mission_type.id,
'date_from': date.today(),
'date_to': date.today() + timedelta(days=2),
'move_type': 'accounting',
})
mission_line = self.env['hr.official.mission.employee'].create({
'official_mission_id': mission.id,
'employee_id': self.employee.id,
'date_from': mission.date_from,
'date_to': mission.date_to,
'hour_from': 8.0,
'hour_to': 16.0,
})
mission_line.days = 3
mission_line.amount = 300.0
self.assertEqual(mission_line.days, 3, "Employee line days incorrect")
self.assertEqual(mission_line.amount, 300.0, "Employee amount calculation incorrect")
mission.send()
self.assertEqual(mission.state, 'send')
mission.accounting_manager()
mission.depart_manager()
mission.approve()
self.assertEqual(mission.state, 'approve', "Mission should be approved")
self.assertTrue(mission_line.account_move_id, "Journal Entry should be created")
self.assertEqual(mission_line.account_move_id.state, 'draft', "Journal Entry should be draft initially")
move_lines = mission_line.account_move_id.line_ids
debit_line = move_lines.filtered(lambda l: l.debit > 0)
self.assertEqual(debit_line.debit, 300.0, "Journal Entry amount mismatch")
def test_03_overlap_constraint(self):
mission1 = self.env['hr.official.mission'].create({
'mission_type': self.mission_type.id,
'date_from': date.today(),
'date_to': date.today() + timedelta(days=5),
})
line1 = self.env['hr.official.mission.employee'].create({
'official_mission_id': mission1.id,
'employee_id': self.employee.id,
'date_from': date.today(),
'date_to': date.today() + timedelta(days=5),
'hour_from': 8,
'hour_to': 16,
})
mission1.state = 'approve'
mission2 = self.env['hr.official.mission'].create({
'mission_type': self.mission_type.id,
'date_from': date.today() + timedelta(days=2),
'date_to': date.today() + timedelta(days=6),
})
with self.assertRaises(ValidationError):
self.env['hr.official.mission.employee'].create({
'official_mission_id': mission2.id,
'employee_id': self.employee.id,
'date_from': date.today() + timedelta(days=2),
'date_to': date.today() + timedelta(days=6),
'hour_from': 8,
'hour_to': 16,
})
def test_04_employees_required(self):
mission = self.env['hr.official.mission'].create({
'mission_type': self.mission_type.id,
'date_from': date.today(),
'date_to': date.today(),
})
with self.assertRaises(UserError):
mission.send()

View File

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

View File

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

View File

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

View File

@ -79,8 +79,6 @@ class HrSalaryRules(models.Model):
if rec.category_id.rule_type != 'deduction' and rec.rules_type == 'insurnce':
raise UserError(_("The Salary Rule is Not Deduction"))
# Override function compute rule in hr salary rule
def _compute_rule(self, localdict):
payslip = localdict.get('payslip')
contract = localdict.get('contract')

View File

@ -0,0 +1,5 @@
from . import test_salary_scale
from . import test_salary_rule_computation
from . import test_employee_promotions
from . import test_payroll_flow
from . import test_employee_reward

View File

@ -0,0 +1,148 @@
from odoo.tests.common import TransactionCase, Form
from odoo.exceptions import UserError
from odoo.tests import tagged
from datetime import date
@tagged('post_install', '-at_install')
class TestEmployeePromotions(TransactionCase):
def setUp(cls):
super(TestEmployeePromotions, cls).setUp()
cls.structure_scale = cls.env['hr.payroll.structure'].create({
'name': 'General Scale',
'type': 'scale',
'code': 'SCL_TEST_01'
})
cls.level_1 = cls.env['hr.payroll.structure'].create({
'name': 'Level 1',
'type': 'level',
'salary_scale_id': cls.structure_scale.id,
'code': 'LVL_TEST_01'
})
cls.group_A = cls.env['hr.payroll.structure'].create({
'name': 'Group A',
'type': 'group',
'salary_scale_id': cls.structure_scale.id,
'salary_scale_level_id': cls.level_1.id,
'code': 'GRP_TEST_A'
})
cls.degree_1 = cls.env['hr.payroll.structure'].create({
'name': 'Degree 1',
'type': 'degree',
'salary_scale_id': cls.structure_scale.id,
'salary_scale_group_id': cls.group_A.id,
'base_salary': 5000.0,
'code': 'DEG_TEST_1'
})
cls.level_2 = cls.env['hr.payroll.structure'].create({
'name': 'Level 2',
'type': 'level',
'salary_scale_id': cls.structure_scale.id,
'code': 'LVL_TEST_02'
})
cls.group_B = cls.env['hr.payroll.structure'].create({
'name': 'Group B',
'type': 'group',
'salary_scale_id': cls.structure_scale.id,
'salary_scale_level_id': cls.level_2.id,
'code': 'GRP_TEST_B'
})
cls.degree_2 = cls.env['hr.payroll.structure'].create({
'name': 'Degree 2',
'type': 'degree',
'salary_scale_id': cls.structure_scale.id,
'salary_scale_group_id': cls.group_B.id,
'base_salary': 7000.0,
'code': 'DEG_TEST_2'
})
cls.employee = cls.env['hr.employee'].create({
'name': 'Test Employee',
'salary_scale': cls.structure_scale.id,
'salary_level': cls.level_1.id,
'salary_group': cls.group_A.id,
'salary_degree': cls.degree_1.id,
})
cls.contract = cls.env['hr.contract'].create({
'name': 'Test Contract',
'employee_id': cls.employee.id,
'wage': 5000.0,
'state': 'open',
'salary_level': cls.level_1.id,
'salary_group': cls.group_A.id,
'salary_degree': cls.degree_1.id,
})
cls.employee.contract_id = cls.contract
def test_01_promotion_workflow_full_cycle(self):
promotion_form = Form(self.env['employee.promotions'])
promotion_form.date = date.today()
promotion_form.employee_id = self.employee
self.assertEqual(promotion_form.old_degree, self.degree_1, "Should auto-fill old degree from employee")
promotion_form.new_level = self.level_2
promotion_form.new_group = self.group_B
promotion_form.new_degree = self.degree_2
promotion = promotion_form.save()
promotion.confirm()
self.assertEqual(promotion.state, 'confirm')
promotion.hr_manager()
self.assertEqual(promotion.state, 'hr_manager')
promotion.approved()
self.assertEqual(promotion.state, 'approved')
self.assertEqual(self.employee.contract_id.salary_degree, self.degree_2,
"Contract degree should be updated to new degree")
self.assertEqual(self.employee.contract_id.salary, 7000.0,
"Contract salary should be updated to new base salary")
def test_02_redraft_reverts_values(self):
promotion = self.env['employee.promotions'].create({
'date': date.today(),
'employee_id': self.employee.id,
'old_degree': self.degree_1.id,
'old_level_2': self.level_1.id,
'old_group_2': self.group_A.id,
'old_degree_2': self.degree_1.id,
'new_degree': self.degree_2.id,
'new_level': self.level_2.id,
'new_group': self.group_B.id,
})
promotion.approved()
self.assertEqual(self.employee.contract_id.salary_degree, self.degree_2)
promotion.re_draft()
self.assertEqual(promotion.state, 'draft')
self.assertEqual(self.employee.contract_id.salary_degree, self.degree_1,
"Should revert to old degree on re-draft")
def test_03_unlink_restriction(self):
promotion = self.env['employee.promotions'].create({
'date': date.today(),
'employee_id': self.employee.id,
'state': 'confirm'
})
with self.assertRaises(UserError):
promotion.unlink()
promotion.state = 'draft'
promotion.unlink()
self.assertFalse(promotion.exists())

View File

@ -0,0 +1,134 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from odoo.tests import tagged
from datetime import date
@tagged('post_install', '-at_install')
class TestEmployeeReward(TransactionCase):
def setUp(self):
super(TestEmployeeReward, self).setUp()
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee',
})
self.account_debit = self.env['account.account'].create({
'name': 'Debit Account',
'code': '100001',
'account_type': 'expense',
'reconcile': True,
})
self.account_credit = self.env['account.account'].create({
'name': 'Credit Account',
'code': '200001',
'account_type': 'liability_payable',
'reconcile': True,
})
self.journal = self.env['account.journal'].create({
'name': 'Reward Journal',
'type': 'general',
'code': 'REW',
'default_account_id': self.account_credit.id,
})
self.contract = self.env['hr.contract'].create({
'name': 'Contract for Test',
'employee_id': self.employee.id,
'wage': 5000.0,
'state': 'open',
})
def test_01_reward_workflow_and_calculation(self):
reward = self.env['hr.employee.reward'].create({
'allowance_reason': 'Excellent Performance',
'date': date.today(),
'reward_type': 'amount',
'amount': 1000.0,
'transfer_type': 'accounting',
'account_id': self.account_debit.id,
'journal_id': self.journal.id,
})
reward_line = self.env['lines.ids.reward'].create({
'employee_reward_id': reward.id,
'employee_id': self.employee.id,
'percentage': 50.0,
})
self.assertEqual(reward_line.amount, 500.0, "Amount calculation is wrong based on percentage")
reward.action_submit()
self.assertEqual(reward.state, 'submitted', "State should be submitted")
self.assertEqual(reward_line.reward_state, 'submitted', "Line state should match parent")
reward.action_hrm()
self.assertEqual(reward.state, 'hrm', "State should be hrm")
reward.action_done()
self.assertEqual(reward.state, 'done', "State should be done")
self.assertTrue(reward_line.move_id, "Journal Entry should be created")
self.assertEqual(reward_line.move_id.state, 'draft', "Move should be created in draft")
move_lines = reward_line.move_id.line_ids
debit_line = move_lines.filtered(lambda l: l.debit > 0)
credit_line = move_lines.filtered(lambda l: l.credit > 0)
self.assertEqual(debit_line.account_id, self.account_debit, "Debit account mismatch")
self.assertEqual(credit_line.account_id, self.account_credit, "Credit account mismatch")
self.assertEqual(debit_line.debit, 500.0, "Debit amount incorrect")
def test_02_constraint_reward_once_yearly(self):
reward_1 = self.env['hr.employee.reward'].create({
'allowance_reason': 'First Reward',
'date': date.today(),
'reward_type': 'amount',
'amount': 1000.0,
'reward_once': True,
'transfer_type': 'accounting',
'account_id': self.account_debit.id,
'journal_id': self.journal.id,
})
self.env['lines.ids.reward'].create({
'employee_reward_id': reward_1.id,
'employee_id': self.employee.id,
'percentage': 100.0,
})
reward_1.action_submit()
reward_1.action_hrm()
reward_1.action_done()
reward_2 = self.env['hr.employee.reward'].create({
'allowance_reason': 'Second Reward',
'date': date.today(),
'reward_type': 'amount',
'amount': 500.0,
'reward_once': True,
})
with self.assertRaises(UserError):
self.env['lines.ids.reward'].create({
'employee_reward_id': reward_2.id,
'employee_id': self.employee.id,
'percentage': 100.0,
})
def test_03_positive_amount_check(self):
reward = self.env['hr.employee.reward'].create({
'allowance_reason': 'Negative Test',
'date': date.today(),
'amount': 100.0,
})
with self.assertRaises(UserError):
reward.amount = -50.0
reward.chick_amount_positive()

View File

@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase, Form
from odoo.exceptions import UserError
from odoo.tests import tagged
from datetime import date, datetime, timedelta
from dateutil.relativedelta import relativedelta
class TestPayrollAdvanceFlow(TransactionCase):
def setUp(cls):
super(TestPayrollAdvanceFlow, cls).setUp()
cls.company = cls.env.company
cls.account_salary = cls.env['account.account'].create({
'name': 'Basic Salary Account',
'code': '600001',
'account_type': 'expense',
'reconcile': True,
})
cls.account_payable = cls.env['account.account'].create({
'name': 'Salaries Payable',
'code': '200001',
'account_type': 'liability_payable',
'reconcile': True,
})
cls.journal = cls.env['account.journal'].create({
'name': 'Salary Journal',
'type': 'general',
'code': 'SAL',
'default_account_id': cls.account_payable.id,
})
cls.rule_basic = cls.env['hr.salary.rule'].create({
'name': 'Basic Salary',
'sequence': 1,
'code': 'BASIC',
'category_id': cls.env.ref('exp_hr_payroll.ALW').id,
'condition_select': 'none',
'amount_select': 'code',
'amount_python_compute': 'result = contract.wage',
'rule_debit_account_id': cls.account_salary.id,
})
cls.rule_net = cls.env['hr.salary.rule'].create({
'name': 'Net Salary',
'sequence': 100,
'code': 'NET',
'category_id': cls.env.ref('exp_hr_payroll.DED').id,
'condition_select': 'none',
'amount_select': 'code',
'amount_python_compute': 'result = categories.BASIC + categories.ALW + categories.DED',
'rule_credit_account_id': cls.account_payable.id,
})
cls.structure = cls.env['hr.payroll.structure'].create({
'name': 'Standard Structure',
'type': 'scale',
'code': 'STRUCT_001',
'rule_ids': [(4, cls.rule_basic.id), (4, cls.rule_net.id)],
'transfer_type': 'one_by_one',
})
cls.employee = cls.env['hr.employee'].create({
'name': 'Test Employee Payroll',
'first_hiring_date': date.today() - relativedelta(years=1),
'state': 'open',
})
cls.contract = cls.env['hr.contract'].create({
'name': 'Contract For Test',
'employee_id': cls.employee.id,
'state': 'program_directory',
'wage': 5000.0,
'salary_scale': cls.structure.id,
'journal_id': cls.journal.id,
'date_start': date.today() - relativedelta(years=1),
})
def test_01_payslip_compute_and_transfer(self):
date_from = date.today().replace(day=1)
date_to = date.today() + relativedelta(months=+1, day=1, days=-1)
payslip = self.env['hr.payslip'].create({
'name': 'Test Payslip',
'employee_id': self.employee.id,
'date_from': date_from,
'date_to': date_to,
'contract_id': self.contract.id,
'struct_id': self.structure.id
})
payslip.compute_sheet()
self.assertEqual(payslip.state, 'computed', "State should be 'computed' after computing sheet")
basic_line = payslip.line_ids.filtered(lambda l: l.code == 'BASIC')
self.assertEqual(basic_line.total, 5000.0, "Basic salary should be 5000")
payslip.compute_totals()
self.assertEqual(payslip.total_allowances, 5000.0, "Total allowances should be calculated correctly")
payslip.confirm()
self.assertEqual(payslip.state, 'confirmed')
payslip.transfer()
self.assertEqual(payslip.state, 'transfered')
self.assertTrue(payslip.move_id, "Journal Entry should be created")
self.assertEqual(payslip.move_id.state, 'draft', "Move should be created in draft state initially")
def test_02_payslip_loans_integration(self):
payslip = self.env['hr.payslip'].create({
'name': 'Loan Payslip',
'employee_id': self.employee.id,
'date_from': date.today().replace(day=1),
'date_to': date.today() + relativedelta(months=+1, day=1, days=-1),
})
self.env['payslip.loans'].create({
'payslip_loan': payslip.id,
'name': 'Car Loan',
'code': 'LOAN01',
'amount': 500.0,
'date': date.today(),
'account_id': self.account_payable.id,
})
payslip.compute_totals()
self.assertEqual(payslip.total_loans, 500.0, "Total loans field should calculate sum of loan lines")
self.assertEqual(payslip.total_sum, 500.0, "Total sum logic check (depends on allowances setup)")
def test_03_payslip_run_batch_process(self):
date_start = date.today().replace(day=1)
date_end = date.today() + relativedelta(months=+1, day=1, days=-1)
payslip_run = self.env['hr.payslip.run'].create({
'name': 'Monthly Run',
'date_start': date_start,
'date_end': date_end,
'salary_scale': self.structure.id,
})
payslip_run.check_date_start()
self.assertEqual(payslip_run.date_end, date_end)
payslip_run.compute_sheet()
self.assertTrue(payslip_run.slip_ids, "Payslips should be generated for eligible employees")
generated_slip = payslip_run.slip_ids[0]
self.assertEqual(generated_slip.employee_id, self.employee)
self.assertEqual(generated_slip.state, 'computed')
payslip_run.confirm()
self.assertEqual(generated_slip.state, 'confirmed')
payslip_run.transfer()
self.assertEqual(payslip_run.state, 'transfered')
self.assertTrue(payslip_run.move_id or generated_slip.move_id, "Accounting move should be generated")
def test_04_payslip_withdraw_and_reset(self):
payslip = self.env['hr.payslip'].create({
'name': 'Withdraw Test',
'employee_id': self.employee.id,
'date_from': date.today(),
'date_to': date.today(),
})
payslip.compute_sheet()
payslip.confirm()
payslip.withdraw()
self.assertEqual(payslip.state, 'draft', "State should return to draft after withdraw")
self.assertFalse(payslip.move_id, "Account move should be unlinked/deleted")

View File

@ -0,0 +1,162 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
from odoo import fields
from datetime import date, timedelta
class TestSalaryRuleComputation(TransactionCase):
def setUp(self):
super(TestSalaryRuleComputation, self).setUp()
self.employee = self.env['hr.employee'].create({'name': 'Test Employee'})
self.category_basic = self.env['hr.salary.rule.category'].create({
'name': 'Basic', 'code': 'BASIC', 'rule_type': 'allowance'
})
self.category_allowance = self.env['hr.salary.rule.category'].create({
'name': 'Allowance', 'code': 'ALW', 'rule_type': 'allowance'
})
self.structure = self.env['hr.payroll.structure'].create({
'name': 'Test Structure', 'code': 'TEST_STRUCT', 'type': 'scale', 'parent_id': False
})
self.rule_basic = self.env['hr.salary.rule'].create({
'name': 'Basic Salary', 'code': 'BASIC',
'category_id': self.category_basic.id,
'amount_select': 'fix',
'fixed_amount': 1000,
'sequence': 1,
'salary_type': 'fixed',
})
self.rule_housing = self.env['hr.salary.rule'].create({
'name': 'Housing Allowance', 'code': 'HOUSING',
'category_id': self.category_allowance.id,
'amount_select': 'fix',
'fixed_amount': 500,
'sequence': 2,
'salary_type': 'fixed',
})
self.contract = self.env['hr.contract'].create({
'name': 'Contract Test',
'employee_id': self.employee.id,
'salary_scale': self.structure.id,
'salary': 5000.0,
'wage': 5000.0,
'state': 'open',
})
def test_01_compute_fixed_rule(self):
localdict = {'contract': self.contract, 'employee': self.employee}
amount, qty, rate = self.rule_basic._compute_rule(localdict)
self.assertEqual(amount, 1000.0, "Basic salary fixed amount should be 1000")
def test_02_compute_percentage_with_related_rules(self):
rule_percent = self.env['hr.salary.rule'].create({
'name': 'Social Security', 'code': 'SOC',
'category_id': self.category_allowance.id,
'amount_select': 'percentage',
'amount_percentage': 10.0,
'quantity': '1.0',
'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])],
'salary_type': 'fixed',
})
localdict = {'contract': self.contract, 'employee': self.employee}
amount, qty, rate = rule_percent._compute_rule(localdict)
self.assertEqual(amount, 150.0, "Percentage calculation failed. Expected 150.0")
def test_03_percentage_with_exception_advantage(self):
today = fields.Date.today()
yesterday = today - timedelta(days=1)
self.env['contract.advantage'].create({
'contract_advantage_id': self.contract.id,
'employee_id': self.employee.id,
'benefits_discounts': self.rule_basic.id,
'type': 'exception',
'amount': 200,
'date_from': yesterday,
'date_to': today + timedelta(days=30),
})
self.contract.invalidate_recordset(['advantages'])
rule_percent = self.env['hr.salary.rule'].create({
'name': 'Social Security Modified', 'code': 'SOC_MOD',
'category_id': self.category_allowance.id,
'amount_select': 'percentage',
'amount_percentage': 10.0,
'quantity': '1.0',
'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])],
'salary_type': 'fixed',
})
localdict = {'contract': self.contract, 'employee': self.employee}
amount, qty, rate = rule_percent._compute_rule(localdict)
self.assertEqual(amount, 130.0, f"Exception logic failed. Expected 130.0, Got {amount}")
def test_04_date_range_validation(self):
old_date = fields.Date.today() - timedelta(days=365)
self.env['contract.advantage'].create({
'contract_advantage_id': self.contract.id,
'employee_id': self.employee.id,
'benefits_discounts': self.rule_basic.id,
'type': 'exception',
'amount': 200,
'date_from': old_date,
'date_to': old_date + timedelta(days=30),
})
rule_percent = self.env['hr.salary.rule'].create({
'name': 'Social Security Date', 'code': 'SOC_DATE',
'category_id': self.category_allowance.id,
'amount_select': 'percentage',
'amount_percentage': 10.0,
'quantity': '1.0',
'related_benefits_discounts': [(6, 0, [self.rule_basic.id, self.rule_housing.id])],
'salary_type': 'fixed',
})
localdict = {'contract': self.contract, 'employee': self.employee}
amount, qty, rate = rule_percent._compute_rule(localdict)
self.assertEqual(amount, 150.0, "Date validation failed. Expired exception should be ignored.")
def test_05_salary_type_related_levels(self):
level_1 = self.env['hr.payroll.structure'].create({
'name': 'Level 1', 'code': 'LVL1', 'type': 'level', 'parent_id': False
})
self.contract.salary_level = level_1.id
rule_level = self.env['hr.salary.rule'].create({
'name': 'Level Rule', 'code': 'LVL_RULE',
'category_id': self.category_basic.id,
'amount_select': 'fix',
'salary_type': 'related_levels',
})
self.env['related.salary.amount'].create({
'salary_rule_id': rule_level.id,
'salary_scale_level': level_1.id,
'salary': 2500.0,
})
rule_level.invalidate_recordset(['salary_amount_ids'])
self.contract.invalidate_recordset(['salary_level'])
localdict = {'contract': self.contract, 'employee': self.employee}
amount, qty, rate = rule_level._compute_rule(localdict)
self.assertEqual(amount, 2500.0, "Related Level salary calculation failed.")

View File

@ -0,0 +1,96 @@
from odoo.tests.common import TransactionCase
from odoo import fields
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError
class TestHrContractSalaryScale(TransactionCase):
def setUp(self):
super().setUp()
self.employee = self.env['hr.employee'].create({
'name': 'Test Employee',
})
allow_cat = self.env['hr.salary.rule.category'].create({
'name': 'Allowance',
'code': 'ALW',
'rule_type': 'allowance'
})
ded_cat = self.env['hr.salary.rule.category'].create({
'name': 'Deduction',
'code': 'DED',
'rule_type': 'deduction'
})
self.structure = self.env['hr.payroll.structure'].create({
'name': 'Scale Structure',
'type': 'scale',
'code': 'SCALE_TEST',
'parent_id': False,
})
self.allow_rule = self.env['hr.salary.rule'].create({
'name': 'Allowance Rule',
'code': 'ALLOW1',
'category_id': allow_cat.id,
'amount_select': 'fix',
'amount_fix': 1000,
'quantity': '1.0',
'condition_select': 'none'
})
self.ded_rule = self.env['hr.salary.rule'].create({
'name': 'Deduction Rule',
'code': 'DED1',
'category_id': ded_cat.id,
'amount_select': 'fix',
'amount_fix': 200,
'quantity': '1.0',
'condition_select': 'none'
})
self.structure.rule_ids = [(6, 0, [self.allow_rule.id, self.ded_rule.id])]
self.contract = self.env['hr.contract'].create({
'name': 'Test Contract',
'employee_id': self.employee.id,
'salary_scale': self.structure.id,
'salary': 5000,
})
def test_compute_function_basic(self):
self.contract.compute_function()
self.assertEqual(self.contract.total_allowance, 1000.0)
def test_salary_group_constraint(self):
group = self.env['hr.payroll.structure'].create({
'name': 'Group A',
'gread_min': 3000,
'gread_max': 6000,
'code': 'SCALE_TEST'
})
self.contract.salary_group = group.id
self.contract.salary = 5000
def test_compute_move_type_one_by_one(self):
self.structure.transfer_type = 'one_by_one'
self.contract.salary_scale = self.structure.id
self.contract.compute_move_type()
self.assertTrue(self.contract.required_condition)
def test_exception_amount_greater_than_rule(self):
rule = self.allow_rule
advantage = self.env['contract.advantage'].create({
'contract_advantage_id': self.contract.id,
'benefits_discounts': rule.id,
'type': 'exception',
'amount': 2000,
'date_from': fields.Date.today(),
})

View File

@ -163,10 +163,8 @@ class Employee(models.Model):
employee.current_leave_state = leave_data.get(employee.id, {}).get('current_leave_state')
employee.current_leave_id = leave_data.get(employee.id, {}).get('current_leave_id')
# Assign is_absent for compatibility with standard hr_holidays module
# employee.is_absent_today = leave_data.get(employee.id) and leave_data.get(employee.id).get('current_leave_state') == 'validate'
is_absent = leave_data.get(employee.id) and leave_data.get(employee.id).get('current_leave_state') == 'validate'
employee.is_absent = leave_data.get(employee.id) and leave_data.get(employee.id).get('current_leave_state') == 'validate'
employee.is_absent = is_absent
def _compute_leaves_count(self):
leaves = self.env['hr.holidays'].read_group([
('employee_id', 'in', self.ids),

View File

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

View File

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

View File

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

View File

@ -0,0 +1,179 @@
# -*- coding: utf-8 -*-
from odoo.tests.common import TransactionCase
from odoo.exceptions import UserError, ValidationError
from odoo.tests import tagged
from datetime import date, timedelta, datetime
@tagged('post_install', '-at_install')
class TestHRHolidaysCustom(TransactionCase):
def setUp(self):
super(TestHRHolidaysCustom, self).setUp()
self.employee = self.env['hr.employee'].create({
'name': 'Employee Test',
'gender': 'male',
'first_hiring_date': date.today() - timedelta(days=365 * 2),
'state': 'open',
})
self.replacement_employee = self.env['hr.employee'].create({
'name': 'Replacement Employee',
'gender': 'male',
'first_hiring_date': date.today() - timedelta(days=365),
'state': 'open',
})
self.contract = self.env['hr.contract'].create({
'name': 'Contract Test',
'employee_id': self.employee.id,
'wage': 5000,
'state': 'open',
'date_start': date.today() - timedelta(days=365 * 2),
'emp_type': 'saudi', # ضروري لفلتر السكيجوال
})
self.employee.contract_id = self.contract
self.replacement_contract = self.env['hr.contract'].create({
'name': 'Replacement Contract',
'employee_id': self.replacement_employee.id,
'wage': 4000,
'state': 'open',
'date_start': date.today() - timedelta(days=365),
'emp_type': 'saudi',
})
self.replacement_employee.contract_id = self.replacement_contract
self.holiday_status = self.env['hr.holidays.status'].create({
'name': 'Annual Leave Test',
'leave_type': 'annual',
'limit': False,
'number_of_days': 30,
'active': True,
'alternative_days': 2,
'alternative_chick': False,
})
self.env['hr.holidays'].create({
'name': 'Balance Record',
'holiday_status_id': self.holiday_status.id,
'employee_id': self.employee.id,
'type': 'add',
'check_allocation_view': 'balance',
'remaining_leaves': 30.0,
'state': 'validate',
})
self.env['hr.holidays'].create({
'name': 'Replacement Balance',
'holiday_status_id': self.holiday_status.id,
'employee_id': self.replacement_employee.id,
'type': 'add',
'check_allocation_view': 'balance',
'remaining_leaves': 10.0,
'state': 'validate',
})
def test_01_leave_workflow_and_ticket_creation(self):
leave_request = self.env['hr.holidays'].create({
'name': 'Leave Request with Ticket',
'employee_id': self.employee.id,
'holiday_status_id': self.holiday_status.id,
'date_from': datetime.now().strftime('%Y-%m-%d 08:00:00'),
'date_to': (datetime.now() + timedelta(days=5)).strftime('%Y-%m-%d 17:00:00'),
'number_of_days_temp': 5,
'type': 'remove',
'check_allocation_view': 'allocation',
'issuing_ticket': 'yes',
'ticket_cash_request_for': 'employee',
})
leave_request.confirm()
leave_request.hr_manager()
leave_request.approved()
leave_request.financial_manager()
self.assertTrue(leave_request.request_done)
ticket = self.env['hr.ticket.request'].search([('leave_request_id', '=', leave_request.id)])
self.assertTrue(ticket)
def test_02_replacement_employee_constraint(self):
today = datetime.now()
self.env['hr.holidays'].create({
'name': 'Replacement Employee Leave',
'employee_id': self.replacement_employee.id,
'holiday_status_id': self.holiday_status.id,
'date_from': today.strftime('%Y-%m-%d 08:00:00'),
'date_to': (today + timedelta(days=2)).strftime('%Y-%m-%d 17:00:00'),
'type': 'remove',
'state': 'validate1',
})
with self.assertRaises(UserError):
self.env['hr.holidays'].create({
'name': 'Main Employee Leave',
'employee_id': self.employee.id,
'holiday_status_id': self.holiday_status.id,
'date_from': today.strftime('%Y-%m-%d 08:00:00'),
'date_to': (today + timedelta(days=2)).strftime('%Y-%m-%d 17:00:00'),
'type': 'remove',
'replace_by': self.replacement_employee.id,
})
def test_03_check_balance_limit(self):
with self.assertRaises(UserError):
leave = self.env['hr.holidays'].create({
'name': 'Exceed Balance Leave',
'employee_id': self.employee.id,
'holiday_status_id': self.holiday_status.id,
'date_from': datetime.now().strftime('%Y-%m-%d 08:00:00'),
'date_to': (datetime.now() + timedelta(days=40)).strftime('%Y-%m-%d 17:00:00'),
'number_of_days_temp': 41,
'type': 'remove',
})
leave._check_number_of_days()
def test_04_scheduler_queue_allocation(self):
monthly_leave_type = self.env['hr.holidays.status'].create({
'name': 'Monthly Leave',
'leave_type': 'annual',
'balance_type': 'monthly',
'leave_annual_type': 'open_balance',
'company_id': self.env.company.id,
'emp_type': 'all',
'alternative_days': 2,
'alternative_chick': False,
'number_of_days': 0,
'duration_ids': [(0, 0, {
'name': 'Level 1',
'date_from': 0,
'date_to': 10,
'duration': 30
})]
})
for i in range(2):
self.env['hr.holidays'].create({
'name': f'Dummy Allocation {i}',
'holiday_status_id': monthly_leave_type.id,
'employee_id': self.replacement_employee.id,
'type': 'add',
'check_allocation_view': 'balance',
'number_of_days_temp': 0,
'state': 'confirm'
})
self.env['hr.holidays'].process_holidays_scheduler_queue()
allocation = self.env['hr.holidays'].search([
('employee_id', '=', self.employee.id),
('holiday_status_id', '=', monthly_leave_type.id),
('check_allocation_view', '=', 'balance'),
('type', '=', 'add')
])
self.assertTrue(allocation, "Scheduler should create allocation record")
self.assertGreater(allocation.remaining_leaves, 0.0)

View File

@ -0,0 +1,56 @@
.. image:: https://img.shields.io/badge/license-LGPL--3-blue.svg
:target: https://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
Advanced HR-LinkedIn Integration
================================
* Advanced HR-LinkedIn Integration module for Odoo 17.
Installation
============
* Install external python packages python-linkedin and mechanize.
Configuration
=============
* Mention the LinkedIn username and password in the section
LinkedIn Credentials under the recruitment configurations.
Company
-------
* `Cybrosys Techno Solutions <https://cybrosys.com/>`__
License
-------
General Public License, Version 3 (LGPL v3).
(https://www.odoo.com/documentation/17.0/legal/licenses.html)
Credits
-------
* Developer: Nilmar Shereef,
Jesni Banu,
(V11 & V12) Milind Mohan,
(V16) Gayathri V,
(V17) Kailas Krishna
Contact: odoo@cybrosys.com
Contacts
--------
* Mail Contact : odoo@cybrosys.com
* Website : https://cybrosys.com
Bug Tracker
-----------
Bugs are tracked on GitHub Issues. In case of trouble, please check there if your issue has already been reported.
Maintainer
==========
.. image:: https://cybrosys.com/images/logo.png
:target: https://cybrosys.com
This module is maintained by Cybrosys Technologies.
For support and more information, please visit `Our Website <https://cybrosys.com/>`__
Further information
===================
HTML Description: `<static/description/index.html>`__

View File

@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2021-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from . import controller
from . import models

View File

@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
{
'name': 'Advanced HR-LinkedIn Integration',
'summary': "Basic module for LnkedIn-HR Recruitment connector",
'description': """The LinkedIn-HR Recruitment Connector Basic Module is
designed to optimize your recruitment workflow, offering a comprehensive
suite of features to enhance candidate sourcing and selection.""",
'category': 'Generic Modules/Human Resources',
'version': "18.0.1.0.0",
'depends': ['hr_recruitment', 'auth_oauth'],
'author': 'Cybrosys Techno Solutions',
'company': 'Cybrosys Techno Solutions',
'maintainer': 'Cybrosys Techno Solutions',
'website': "https://www.cybrosys.com",
'data': [
'data/auth_linkedin_data.xml',
'security/ir.model.access.csv',
'views/recruitment_config_settings.xml',
'views/hr_job_linkedin_likes_comments_views.xml',
'views/linkedin_comments_views.xml',
'views/oauth_views.xml',
],
'external_dependencies':
{
'python': ['mechanize', 'linkedin'],
},
'images': ['static/description/banner.jpg'],
'license': 'LGPL-3',
'installable': True,
'auto_install': False,
'application': False,
}

View File

@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
from . import hr_linkedin_recruitment

View File

@ -0,0 +1,176 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <http://www.gnu.org/licenses/>.
#
#############################################################################
import logging
from werkzeug import urls
_logger = logging.getLogger(__name__)
try:
import mechanize
from linkedin_v2 import linkedin
from urllib.request import HTTPRedirectHandler as MechanizeRedirectHandler
except ImportError:
_logger.error('Odoo module hr_linkedin_recruitment depends on the several '
'external python package Please read the doc/requirement.txt '
'file inside the module.')
import json
import requests
from odoo.exceptions import ValidationError
from odoo import http, _
from odoo.http import request
from urllib.parse import urlparse
from urllib.parse import parse_qs
class LinkedinSocial(http.Controller):
@http.route('/linkedin/redirect', type='http', website=True, auth='public')
def social_linkedin_callbacks(self):
"""shares post on linkedin"""
url = request.httprequest.url
parsed_url = urlparse(url)
code = parse_qs(parsed_url.query)['code'][0]
state = parse_qs(parsed_url.query)['state'][0]
linkedin_auth_provider = request.env.ref(
'hr_linkedin_recruitment.provider_linkedin')
linked_in_url = request.env['hr.job'].browse(int(state))
recruitment = request.env['hr.job']
access_token = requests.post(
'https://www.linkedin.com/oauth/v2/accessToken',
params={
'Content-Type': 'x-www-form-urlencoded',
'grant_type': 'authorization_code',
# This is code obtained on previous step by Python script.
'code': code,
# Client ID of your created application
'client_id': linkedin_auth_provider.client_id,
# # Client Secret of your created application
'client_secret': linkedin_auth_provider.client_secret,
# This should be same as 'redirect_uri' field value of previous Python script.
'redirect_uri': linked_in_url._get_linkedin_post_redirect_uri(),
},
).json()['access_token']
li_credential = {}
linkedin_auth_provider = request.env.ref(
'hr_linkedin_recruitment.provider_linkedin')
if (linkedin_auth_provider.client_id and
linkedin_auth_provider.client_secret):
li_credential['api_key'] = linkedin_auth_provider.client_id
li_credential['secret_key'] = linkedin_auth_provider.client_secret
else:
raise ValidationError(_('LinkedIn Access Credentials are empty.!\n'
'Please fill up in Auth Provider form.'))
if request.env['ir.config_parameter'].sudo().get_param(
'recruitment.li_username'):
li_credential['un'] = request.env[
'ir.config_parameter'].sudo().get_param(
'recruitment.li_username')
else:
raise ValidationError(
_('Please fill up username in LinkedIn Credential settings.'))
if request.env['ir.config_parameter'].sudo().get_param(
'recruitment.li_password'):
li_credential['pw'] = request.env[
'ir.config_parameter'].sudo().get_param(
'recruitment.li_password')
else:
raise ValidationError(
_('Please fill up password in LinkedIn Credential settings.'))
url = 'https://api.linkedin.com/v2/ugcPosts'
li_suit_credent = {}
li_suit_credent['access_token'] = access_token
member_url = 'https://api.linkedin.com/v2/userinfo'
response = recruitment.get_urn('GET', member_url,
li_suit_credent['access_token'])
urn_response_text = response.json()
li_credential['profile_urn'] = urn_response_text['sub']
li_suit_credent['li_credential'] = li_credential
payload = json.dumps({
"author": "urn:li:person:" + li_credential['profile_urn'],
"lifecycleState": "PUBLISHED",
"specificContent": {
"com.linkedin.ugc.ShareContent": {
"shareCommentary": {
"text": linked_in_url.name
},
"shareMediaCategory": "NONE"
}
},
"visibility": {
"com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC"
}
})
headers = {
'Authorization': 'Bearer ' + access_token,
'X-Restli-Protocol-Version': '2.0.0',
'Content-Type': 'application/json',
}
if linked_in_url.name:
response = requests.request("POST", url, data=payload,
headers=headers)
share_response_text = response.json()
linked_in_url.write({
'access_token': access_token + '+' + share_response_text['id']
})
share_response_code = response.status_code
if share_response_code == 201:
linked_in_url.update_key = True
elif share_response_code == 404:
raise Warning("Resource does not exist.!")
elif share_response_code == 409:
raise Warning("Already shared!")
else:
raise Warning("Error!! Check your connection...")
else:
raise Warning("Provide a Job description....")
return_uri = 'https://www.linkedin.com/oauth/v2/authorization'
li_permissions = [' w_organization_social_feed ', ' r_liteprofile ',
' r_organization_social_feed ', ' r_ads ',
'w_member_social_feed', 'r_member_social',
'r_compliance', 'w_compliance']
auth = linkedin.LinkedInAuthentication(li_credential['api_key'],
li_credential['secret_key'],
return_uri,
li_permissions)
li_suit_credent = {}
li_suit_credent['access_token'] = access_token
li_credential['profile_urn'] = share_response_text['id']
li_suit_credent['li_credential'] = li_credential
url = urls.url_join(
http.request.env['ir.config_parameter'].sudo().get_param(
'web.base.url'),
'web#id=%(id)s&model=hr.job&action=%(action)s&view_type=form' % {
'id': state,
'action': request.env.ref('hr_recruitment.action_hr_job').id
})
return request.redirect(url)

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<!-- This record defines the configuration for LinkedIn OAuth provider.-->
<record id="provider_linkedin" model="auth.oauth.provider">
<field name="name">LinkedIn</field>
<field name="auth_endpoint">
https://www.linkedin.com/oauth/v2/authorization
</field>
<field name="scope">r_basicprofile r_emailaddress w_share
w_member_social
</field>
<field name="validation_endpoint">https://api.linkedin.com/v2/me
</field>
<field name="data_endpoint">https://api.linkedin.com/v2/me</field>
<field name="css_class">fa fa-linkedin-square</field>
<field name="body">Share post with LinkedIn</field>
</record>
</data>
</odoo>

View File

@ -0,0 +1,6 @@
## Module <hr_linkedin_recruitment>
#### 05.02.2024
#### Version 17.0.1.0.0
##### ADD
- Initial Commit for Advanced HR-LinkedIn Integration

View File

@ -0,0 +1,8 @@
Odoo integration module "hr_linkedin_recruitment" depends on the several external python package.
Please ensure that listed packaged has installed in your system:
* python-linkedin (sudo python-linkedin)
* mechanize (sudo pip install mechanize)

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <https://www.gnu.org/licenses/>.
#
#############################################################################
from . import auth_outh_provider
from . import hr_job
from . import linkedin_comments
from . import recruitment_config

View File

@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <https://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class OAuthProviderLinkedin(models.Model):
""" Adding client_secret field because some apps likes twitter,
linkedIn are using this value for its API operations """
_inherit = 'auth.oauth.provider'
client_secret = fields.Char(string='Client Secret',
help="Only need LinkedIn, Twitter etc..")

View File

@ -0,0 +1,161 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <https://www.gnu.org/licenses/>.
#
#############################################################################
import logging
import requests
from werkzeug.urls import url_encode, url_join
from odoo import fields, models, _
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class HrJobShare(models.Model):
"""recruitment of different job positions"""
_inherit = 'hr.job'
update_key = fields.Char(string='Update Key', readonly=True)
access_token = fields.Char(string='Access Token',
help='Access token for your linkedin app')
comments = fields.Boolean(default=False, string='Likes Comments',
help='Which is used to visible the like comment retrieving button')
like_comment = fields.Boolean(default=False, string='Likes comment',
help='Which is used to visible the smart buttons of likes and comments')
post_likes = fields.Integer(string='Likes Count',
help="Total Number of likes in the shared post")
post_commands = fields.Integer(string='Comments Count',
help="Total Number of Comments in the shared post")
def _get_linkedin_post_redirect_uri(self):
"""finding redirecting url"""
print('url', self.get_base_url())
return url_join(self.get_base_url(), '/linkedin/redirect')
def share_linkedin(self):
""" Button function for sharing post """
self.comments = True
linkedin_auth_provider = self.env.ref(
'hr_linkedin_recruitment.provider_linkedin')
if linkedin_auth_provider.client_id and linkedin_auth_provider.client_secret:
linkedin_client_id = linkedin_auth_provider.client_id
params = {
'response_type': 'code',
'client_id': linkedin_client_id,
'redirect_uri': self._get_linkedin_post_redirect_uri(),
'state': self.id,
'scope': 'w_member_social r_1st_connections_size r_ads '
'r_ads_reporting r_basicprofile r_organization_admin '
'r_organization_social rw_ads rw_organization_admin '
'w_member_social w_organization_social openid profile email'
}
else:
raise ValidationError(_('LinkedIn Access Credentials are empty.!\n'
'Please fill up in Auth Provider form.'))
return {
'type': 'ir.actions.act_url',
'url': 'https://www.linkedin.com/oauth/v2/authorization?%s' % url_encode(
params),
'target': 'self'
}
def share_request(self, method, page_share_url, access_token, data):
""" Function will return UPDATED KEY , [201] if sharing is OK """
headers = {'x-li-format': 'json', 'Content-Type': 'application/json'}
params = {}
params.update({'oauth2_access_token': access_token})
kw = dict(data=data, params=params, headers=headers, timeout=60)
req_response = requests.request(method.upper(), page_share_url, **kw)
return req_response
def get_urn(self, method, has_access_url, access_token):
""" Function will return TRUE if credentials user has the access to update """
headers = {'x-li-format': 'json', 'Content-Type': 'application/json'}
params = {}
params.update({'oauth2_access_token': access_token})
kw = dict(params=params, headers=headers, timeout=60)
req_response = requests.request(method.upper(), has_access_url, **kw)
return req_response
def user_response_like(self):
"""return the likes"""
return
def likes_comments(self):
"""retrieving total count of likes and comments"""
self.like_comment = True
urn = self.access_token.split('+')[1]
url = "https://api.linkedin.com/v2/socialActions/" + urn
payload = {}
headers = {
'Authorization': 'Bearer ' + self.access_token.split('+')[0],
'LinkedIn-Version': '202308',
}
response = requests.request("GET", url, headers=headers)
response_comm_like = response.json()
self.post_likes = response_comm_like['likesSummary']['totalLikes']
self.post_commands = response_comm_like["commentsSummary"][
'aggregatedTotalComments']
comment_url = "https://api.linkedin.com/v2/socialActions/" + urn + "/comments"
headers = {
'LinkedIn-Version': '202308',
'Authorization': 'Bearer ' + self.access_token.split('+')[0],
}
response = requests.request("GET", comment_url, headers=headers,
data=payload)
response_commets = response.json()
comment_id = self.env['linkedin.comments'].search([]).mapped(
'comments_id')
for record in response_commets.get("elements", []):
if record['id'] not in comment_id:
self.env['linkedin.comments'].create({
'post_id': self.id,
'comments_id': record['id'],
'linkedin_comments': record['message']['text'],
})
def user_response_commends(self):
"""return the comments of the shared post"""
return {
'type': 'ir.actions.act_window',
'target': 'current',
'name': _('Linkedin'),
'view_mode': 'tree',
'res_model': 'linkedin.comments',
'domain': [('post_id', '=', self.id)],
}
def view_shared_post(self):
"""Direct link for viewing the shared post page in linkedin"""
url = "https://api.linkedin.com/v2/me"
payload = ""
headers = {
'LinkedIn-Version': '202208',
'Authorization': 'Bearer ' + self.access_token.split('+')[0],
}
response = requests.request("GET", url, headers=headers, data=payload)
response_activity = response.json()
activity_urn = response_activity["vanityName"]
return {
'type': 'ir.actions.act_url',
'url': 'https://www.linkedin.com/in/%s' % activity_urn + '/recent-activity/',
'target': 'self'
}

View File

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <https://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class LinkedinComments(models.Model):
"""class for retrieving comments of shared post"""
_name = 'linkedin.comments'
post_id = fields.Integer(string="Post ID",
help="Post id of the particular post")
comments_id = fields.Char(string="Comments ID",
help="For specifing the comments id")
linkedin_comments = fields.Char(string="Comments",
help="To add the posts comments")

View File

@ -0,0 +1,77 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <https://www.gnu.org/licenses/>.
#
#############################################################################
import logging
import urllib
_logger = logging.getLogger(__name__)
try:
import mechanize
from mechanize import _response
from mechanize import _rfc3986
import re
class MechanizeRedirectHandler(mechanize.HTTPRedirectHandler):
def http_error_302(self, req, fp, code, msg, headers):
if 'location' in headers:
newurl = headers.getheaders('location')[0]
elif 'uri' in headers:
newurl = headers.getheaders('uri')[0]
else:
return
newurl = _rfc3986.clean_url(newurl, "latin-1")
newurl = _rfc3986.urljoin(req.get_full_url(), newurl)
new = self.redirect_request(req, fp, code, msg, headers, newurl)
if new is None:
return
if hasattr(req, 'redirect_dict'):
visited = new.redirect_dict = req.redirect_dict
if (visited.get(newurl, 0) >= self.max_repeats or
len(visited) >= self.max_redirections):
raise urllib.error.HTTPError(req.get_full_url(), code,
self.inf_msg + msg, headers,
fp)
else:
visited = new.redirect_dict = req.redirect_dict = {}
visited[newurl] = visited.get(newurl, 0) + 1
fp.read()
fp.close()
# If the redirected URL doesn't match
new_url = new.get_full_url()
if not re.search('^http(?:s)?\:\/\/.*www\.linkedin\.com', new_url):
return _response.make_response('', headers.items(), new_url,
200, 'OK')
else:
return self.parent.open(new)
http_error_301 = http_error_303 = http_error_307 = http_error_302
http_error_refresh = http_error_302
except ImportError:
_logger.warning(
'Odoo module hr_linkedin_recruitment depends on the several external'
' python package'
'Please read the doc/requirement.txt file inside the module.')

View File

@ -0,0 +1,49 @@
# -*- coding: utf-8 -*-
#############################################################################
#
# Cybrosys Technologies Pvt. Ltd.
#
# Copyright (C) 2024-TODAY Cybrosys Technologies(<https://www.cybrosys.com>)
# Author: Cybrosys Techno Solutions(<https://www.cybrosys.com>)
#
# You can modify it under the terms of the GNU LESSER
# GENERAL PUBLIC LICENSE (LGPL v3), Version 3.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU LESSER GENERAL PUBLIC LICENSE (LGPL v3) for more details.
#
# You should have received a copy of the GNU LESSER GENERAL PUBLIC LICENSE
# (LGPL v3) along with this program.
# If not, see <https://www.gnu.org/licenses/>.
#
#############################################################################
from odoo import fields, models
class ResConfigSettings(models.TransientModel):
"""config settings"""
_inherit = 'res.config.settings'
li_username = fields.Char(string="User Name", help="Your Linkedin Username")
li_password = fields.Char(string="Password", help="Your Linkedin Password")
def set_values(self):
"""super the config to set the value"""
super(ResConfigSettings, self).set_values()
self.env['ir.config_parameter'].sudo().set_param(
'recruitment.li_username', self.li_username)
self.env['ir.config_parameter'].sudo().set_param(
'recruitment.li_password', self.li_password)
def get_values(self):
"""super the config to get the value"""
res = super(ResConfigSettings, self).get_values()
res.update(
li_username=self.env['ir.config_parameter'].sudo().get_param(
'recruitment.li_username'),
li_password=self.env['ir.config_parameter'].sudo().get_param(
'recruitment.li_password'),
)
return res

View File

@ -0,0 +1,2 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_linkedin_comments,linkedin.comments,model_linkedin_comments,base.group_user,1,1,1,1
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
2 access_linkedin_comments linkedin.comments model_linkedin_comments base.group_user 1 1 1 1

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 733 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 878 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 905 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 839 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 627 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 988 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Some files were not shown because too many files have changed in this diff Show More