Files

221 lines
9.5 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import calendar
from collections import defaultdict
from dateutil.relativedelta import relativedelta
from itertools import chain
from odoo import Command, api, fields, models
class HrPayslip(models.Model):
_inherit = "hr.payslip"
l10n_ae_basic_salary = fields.Monetary(string="Basic Salary", compute="_compute_l10n_ae_basic_salary", currency_field="currency_id")
l10n_ae_hourly_wage = fields.Monetary(string="Hourly Wage", compute="_compute_l10n_ae_hourly_wage", currency_field="currency_id")
l10n_ae_hours_worked = fields.Float(string="Hours Worked", compute="_compute_l10n_ae_worked_values")
l10n_ae_total_paid_hours = fields.Float(string="Total Paid Hours", compute="_compute_l10n_ae_worked_values")
@api.depends(
'worked_days_line_ids.number_of_hours',
'worked_days_line_ids.code',
'worked_days_line_ids.is_paid',
)
def _compute_l10n_ae_worked_values(self):
all_lines = self.env['hr.payslip.worked_days']._read_group(
domain=[('payslip_id', 'in', self.ids)],
groupby=['payslip_id', 'code', 'is_paid'],
aggregates=['number_of_hours:sum'],
)
values_by_payslip = defaultdict(lambda: {'l10n_ae_hours_worked': 0, 'l10n_ae_total_paid_hours': 0})
for line in all_lines:
# line = (hr.payslip(), code, is_paid, number_of_hours)
payslip_id = line[0]
if line[1] == 'WORK100':
values_by_payslip[payslip_id.id]['l10n_ae_hours_worked'] += line[3]
if line[2]:
values_by_payslip[payslip_id.id]['l10n_ae_total_paid_hours'] += line[3]
for record in self:
record.update(values_by_payslip[record.id])
@api.depends(
'version_id.wage_type',
'version_id.resource_calendar_id.hours_per_day',
'version_id.l10n_ae_housing_allowance',
'version_id.l10n_ae_transportation_allowance',
'version_id.l10n_ae_other_allowances'
)
def _compute_l10n_ae_hourly_wage(self):
for record in self:
if record.version_id.wage_type == 'hourly':
record.l10n_ae_hourly_wage = record.version_id.hourly_wage
else:
hours = sum(record.worked_days_line_ids.mapped('number_of_days')) * record.version_id.resource_calendar_id.hours_per_day
gross = record.version_id.wage + record.version_id.l10n_ae_housing_allowance + record.version_id.l10n_ae_transportation_allowance + record.version_id.l10n_ae_other_allowances
record.l10n_ae_hourly_wage = gross / hours if hours > 0 else 0
def _get_l10n_ae_total_work_hours(self):
self.ensure_one()
calendar = self.version_id.resource_calendar_id
if calendar:
date_from = fields.Datetime.to_datetime(self.date_from)
date_to = fields.Datetime.to_datetime(self.date_to) + relativedelta(days=1) - relativedelta(microseconds=1)
hours = calendar.get_work_duration_data(date_from, date_to).get('hours', 0)
return hours
return self.sum_worked_hours
def _get_l10n_ae_hourly_allowance_value(self, allowance_type):
self.ensure_one()
total_hours = self._get_l10n_ae_total_work_hours()
if allowance_type not in ('housing', 'transportation', 'other') or total_hours <= 0:
return 0
field = f'l10n_ae_{allowance_type}_allowance{"s" if allowance_type == "other" else ""}'
return self.version_id[field] / total_hours
@api.depends(
'sum_worked_hours',
'l10n_ae_hours_worked',
'version_id.work_entry_source',
'version_id.wage'
)
def _compute_l10n_ae_basic_salary(self):
for record in self:
if record.version_id.work_entry_source == 'calendar':
record.l10n_ae_basic_salary = record.version_id.wage
else:
total_hours = record._get_l10n_ae_total_work_hours()
record.l10n_ae_basic_salary = (
record.l10n_ae_hours_worked * (record.version_id.wage / total_hours)
if total_hours > 0 else 0
)
def _l10n_ae_get_eos_daily_salary(self):
years = relativedelta(self.date_to, self.employee_id._get_first_version_date()).years
ratio = 21 / 30 if years <= 5 else 1
days_in_month = calendar.monthrange(self.date_from.year, self.date_from.month)[1] or 30
salary = 0
if self.version_id.l10n_ae_is_computed_based_on_daily_salary:
salary = self.version_id.l10n_ae_eos_daily_salary
else:
salary = (self.version_id.wage / 12) / days_in_month
return salary * ratio
@api.model
def _l10n_ae_get_wps_formatted_amount(self, val):
currency = self.env.ref('base.AED')
return f'{currency.round(val):.{currency.decimal_places}f}'
def _l10n_ae_get_wps_data(self):
rows = []
input_codes = [
"HOUALLOWINP",
"CONVALLOWINP",
"MEDALLOWINP",
"ANNUALPASSALLOWINP",
"OVERTIMEALLOWINP",
"OTALLOWINP",
"LEAVEENCASHINP",
]
inputs_dict = self._get_line_values(input_codes)
for payslip in self:
employee = payslip.employee_id
unpaid_leave_days = payslip.worked_days_line_ids.filtered(
lambda x: x.work_entry_type_id in payslip.struct_id.unpaid_work_entry_type_ids)
unpaid_leave_day_count = sum(unpaid_leave_days.mapped('number_of_days'))
evp_inputs = [inputs_dict[code][payslip.id]['total'] for code in input_codes]
total_evp = sum(evp_inputs)
bank_account = employee.primary_bank_account_id
l10n_ae_routing_code = bank_account.bank_id.l10n_ae_routing_code
rows.append([
"EDR",
(employee.identification_id or '').zfill(14),
(l10n_ae_routing_code or '').zfill(9),
employee.primary_bank_account_id.acc_number or '',
payslip.date_from.strftime('%Y-%m-%d'),
payslip.date_to.strftime('%Y-%m-%d'),
(payslip.date_to - payslip.date_from).days + 1,
self._l10n_ae_get_wps_formatted_amount(payslip.net_wage - total_evp),
self._l10n_ae_get_wps_formatted_amount(total_evp),
unpaid_leave_day_count
])
if not payslip.currency_id.is_zero(total_evp):
rows.append([
"EVP",
(employee.identification_id or '').zfill(14),
(l10n_ae_routing_code or '').zfill(9),
*map(self._l10n_ae_get_wps_formatted_amount, (max(0, v) for v in evp_inputs))
])
return rows
@api.depends('employee_id', 'version_id', 'struct_id', 'date_from', 'date_to')
def _compute_input_line_ids(self):
super()._compute_input_line_ids()
balance_by_employee = self._get_salary_advance_balances_by_employee()
uae_employee_struct = self.env.ref('l10n_ae_hr_payroll.uae_employee_payroll_structure')
for slip in self:
if not slip.employee_id or slip.country_code != "AE":
continue
if slip.struct_id == uae_employee_struct:
balance = balance_by_employee[slip.employee_id]
if balance <= 0:
continue
sal_adv_rec_type = self.env.ref('l10n_ae_hr_payroll.l10n_ae_input_advance_recovery')
new_input_lines = [
Command.unlink(line.id)
for line in slip.input_line_ids.filtered_domain([('input_type_id', '=', sal_adv_rec_type.id)])
]
new_input_lines.append(Command.create({
'name': self.env._('Salary Advance Recovery'),
'amount': balance,
'input_type_id': sal_adv_rec_type.id,
}))
slip.write({'input_line_ids': new_input_lines})
def _get_salary_advance_balances_by_employee(self):
payslips_by_employee = self._read_group(
domain=[
('state', 'in', ('validated', 'paid')),
('employee_id', 'in', self.employee_id.ids),
('input_line_ids.code', 'in', ('ADV', 'ADVREC')),
],
groupby=['employee_id'],
aggregates=['id:recordset']
)
balance_by_employee = defaultdict(float)
for employee_id, payslips in payslips_by_employee:
for input_line in chain.from_iterable(payslip.input_line_ids for payslip in payslips):
if input_line.code == 'ADV':
balance_by_employee[employee_id] += input_line.amount
elif input_line.code == 'ADVREC':
balance_by_employee[employee_id] -= input_line.amount
return balance_by_employee
def action_payslip_payment_report(self, export_format='l10n_ae_wps'):
action = super().action_payslip_payment_report()
if self.company_id.country_code != 'AE':
return action
action.update({
'context': {
**action['context'],
'default_export_format': export_format,
},
})
return action
def compute_sheet(self):
ae_payslips = self.filtered(lambda payslip: payslip.country_code == 'AE')
ae_payslips._compute_l10n_ae_hourly_wage()
ae_payslips._compute_l10n_ae_worked_values()
ae_payslips._compute_l10n_ae_basic_salary()
return super().compute_sheet()