forked from Mapan/odoo17e
432 lines
19 KiB
Python
432 lines
19 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
from collections import defaultdict
|
|
from datetime import datetime, date
|
|
from math import floor
|
|
from dateutil.relativedelta import relativedelta
|
|
|
|
from odoo import api, fields, models, _
|
|
|
|
PERIODS_PER_YEAR = {
|
|
"daily": 260,
|
|
"weekly": 52,
|
|
"bi-weekly": 26,
|
|
"semi-monthly": 24,
|
|
"monthly": 12,
|
|
"bi-monthly": 6,
|
|
"quarterly": 4,
|
|
"semi-annually": 2,
|
|
"annually": 1,
|
|
}
|
|
|
|
|
|
class HrPayslip(models.Model):
|
|
_inherit = "hr.payslip"
|
|
|
|
l10n_au_income_stream_type = fields.Selection(
|
|
related="contract_id.l10n_au_income_stream_type")
|
|
l10n_au_foreign_tax_withheld = fields.Float(
|
|
string="Foreign Tax Withheld",
|
|
help="Foreign tax withheld for the current financial year")
|
|
l10n_au_exempt_foreign_income = fields.Float(
|
|
string="Exempt Foreign Income",
|
|
help="Exempt foreign income for the current financial year")
|
|
l10n_au_allowance_withholding = fields.Float(
|
|
string="Withholding for Allowance",
|
|
help="Amount to be withheld from allowances")
|
|
l10n_au_schedule_pay = fields.Selection(related="contract_id.schedule_pay", store=True, index=True)
|
|
l10n_au_termination_type = fields.Selection([
|
|
("normal", "Non-Genuine Redundancy"),
|
|
("genuine", "Genuine Redundancy"),
|
|
], required=True, default="normal", string="Termination Type")
|
|
|
|
def _get_base_local_dict(self):
|
|
res = super()._get_base_local_dict()
|
|
res.update({
|
|
"year_slips": self._l10n_au_get_year_to_date_slips(self.date_from),
|
|
"ytd_total": self._l10n_au_get_year_to_date_totals(self.date_from),
|
|
})
|
|
return res
|
|
|
|
def _get_daily_wage(self):
|
|
period = self.struct_id.schedule_pay
|
|
wage = self.contract_id.wage
|
|
if period == "daily":
|
|
return wage
|
|
if period == "weekly":
|
|
return wage / 5
|
|
if period == "bi-weekly":
|
|
return wage / 10
|
|
if period == "monthly":
|
|
return wage * 3 / 13 / 5
|
|
if period == "quarterly":
|
|
return wage / 13 / 5
|
|
return wage
|
|
|
|
def _compute_input_line_ids(self):
|
|
for payslip in self:
|
|
if not payslip.struct_id or payslip.company_id.country_id.code != "AU":
|
|
continue
|
|
# this only works if the payslip is saved after struct type is changed, because it depends on the structure
|
|
# type that was selected before.
|
|
new_types = payslip.struct_id.type_id.l10n_au_default_input_type_ids
|
|
# Remove the lines not default for new structure and keep user defined allowances
|
|
to_remove_lines = payslip.input_line_ids.filtered(lambda i: i.input_type_id not in new_types and i.l10n_au_is_default_allowance)
|
|
to_remove_vals = [(2, line.id) for line in to_remove_lines]
|
|
to_add_vals = []
|
|
# Add default lines not already on the payslip
|
|
for default_allowance in new_types.filtered(lambda x: x not in payslip.input_line_ids.input_type_id):
|
|
to_add_vals.append((0, 0, {
|
|
'amount': default_allowance.l10n_au_default_amount,
|
|
'input_type_id': default_allowance.id,
|
|
'l10n_au_is_default_allowance': True,
|
|
}))
|
|
input_line_vals = to_remove_vals + to_add_vals
|
|
payslip.update({'input_line_ids': input_line_vals})
|
|
# automatic description for other types
|
|
for line in payslip.input_line_ids.filtered(lambda line: line.code == "OD"):
|
|
line.name = line.input_type_id.name.split("-")[1].strip()
|
|
return super()._compute_input_line_ids()
|
|
|
|
def _l10n_au_get_financial_year_start(self, date):
|
|
if date.month < 7:
|
|
return date + relativedelta(years=-1, month=7, day=1)
|
|
return date + relativedelta(month=7, day=1)
|
|
|
|
def _l10n_au_get_financial_year_end(self, date):
|
|
if date.month < 7:
|
|
return date + relativedelta(month=6, day=30)
|
|
return date + relativedelta(years=1, month=6, day=30)
|
|
|
|
@api.model
|
|
def _get_dashboard_warnings(self):
|
|
res = super()._get_dashboard_warnings()
|
|
|
|
employees_default_title = _('Employees')
|
|
self.env.cr.execute("""
|
|
SELECT DISTINCT e.id
|
|
FROM hr_employee e
|
|
WHERE (
|
|
e.work_phone IS NULL
|
|
OR e.private_street IS NULL
|
|
OR e.private_city IS NULL
|
|
OR e.birthday IS NULL
|
|
)
|
|
AND e.company_id = any(%s)
|
|
AND e.active
|
|
""", [self.env.companies.ids])
|
|
if self.env.cr.rowcount:
|
|
employees_missing_info = [e[0] for e in self.env.cr.fetchall()]
|
|
res.append({
|
|
'string': _('Employees with missing required information'),
|
|
'count': len(employees_missing_info),
|
|
'action': self._dashboard_default_action(employees_default_title, 'hr.employee', employees_missing_info),
|
|
})
|
|
|
|
self.env.cr.execute("""
|
|
SELECT DISTINCT e.id
|
|
FROM hr_employee e
|
|
WHERE e.l10n_au_tfn_declaration = '111111111'
|
|
AND e.create_date < NOW() - INTERVAL '28 days'
|
|
AND e.company_id = any(%s)
|
|
AND e.active
|
|
""", [self.env.companies.ids])
|
|
if self.env.cr.rowcount:
|
|
employees_late_tfn = [e[0] for e in self.env.cr.fetchall()]
|
|
res.append({
|
|
'string': _('Employees who have not provided a TFN declaration after 28 days'),
|
|
'count': len(employees_late_tfn),
|
|
'action': self._dashboard_default_action(employees_default_title, 'hr.employee', employees_late_tfn),
|
|
})
|
|
|
|
return res
|
|
|
|
def _l10n_au_get_year_to_date_slips(self, date_from):
|
|
start_year = self._l10n_au_get_financial_year_start(date_from)
|
|
year_slips = self.env["hr.payslip"].search([
|
|
("employee_id", "=", self.employee_id.id),
|
|
("company_id", "=", self.company_id.id),
|
|
("state", "in", ["paid", "done"]),
|
|
("date_from", ">=", start_year),
|
|
("date_from", "<=", date_from),
|
|
], order="date_from")
|
|
if self.env.context.get('l10n_au_include_current_slip'):
|
|
year_slips |= self
|
|
return year_slips
|
|
|
|
def _l10n_au_get_year_to_date_totals(self, date_from):
|
|
year_slips = self._l10n_au_get_year_to_date_slips(date_from)
|
|
totals = {
|
|
"slip_lines": defaultdict(lambda: defaultdict(float)),
|
|
"worked_days": defaultdict(lambda: defaultdict(float)),
|
|
"periods": len(year_slips),
|
|
}
|
|
for line in year_slips.line_ids:
|
|
totals["slip_lines"][line.category_id.name]["total"] += line.total
|
|
totals["slip_lines"][line.category_id.name][line.code] += line.total
|
|
for line in year_slips.worked_days_line_ids:
|
|
totals["worked_days"][line.work_entry_type_id]["amount"] += line.amount
|
|
return totals
|
|
|
|
@api.model
|
|
def _l10n_au_compute_weekly_earning(self, amount, period):
|
|
if period == "monthly" and round(amount % 1, 2) == 0.33:
|
|
amount += 0.01
|
|
weekly_amount = self._l10n_au_convert_amount(amount, period, "weekly")
|
|
return floor(weekly_amount) + 0.99
|
|
|
|
@api.model
|
|
def _l10n_au_convert_amount(self, amount, period_from, period_to):
|
|
coefficient = PERIODS_PER_YEAR[period_from] / PERIODS_PER_YEAR[period_to]
|
|
return amount * coefficient
|
|
|
|
def _l10n_au_compute_withholding_amount(self, period_earning, period, coefficients):
|
|
self.ensure_one()
|
|
employee_id = self.employee_id
|
|
contract = self.contract_id
|
|
# if custom withholding rate
|
|
if contract.l10n_au_withholding_variation:
|
|
return period_earning * contract.l10n_au_withholding_variation_amount / 100
|
|
|
|
weekly_earning = self._l10n_au_compute_weekly_earning(period_earning, period)
|
|
weekly_withhold = 0.0
|
|
|
|
if employee_id.l10n_au_scale == "4":
|
|
coefficients = self._rule_parameter("l10n_au_withholding_no_tfn")
|
|
weekly_withhold = floor(weekly_earning) * coefficients["foreign"] if employee_id.is_non_resident else coefficients["national"]
|
|
return self._l10n_au_convert_amount(weekly_withhold, "weekly", period)
|
|
coefficients = coefficients[employee_id.l10n_au_scale]
|
|
for coef in coefficients:
|
|
if coef[0] == "inf" or weekly_earning < coef[0]:
|
|
weekly_withhold = coef[1] * weekly_earning - coef[2]
|
|
break
|
|
|
|
amount = round(weekly_withhold)
|
|
period_amount = self._l10n_au_convert_amount(amount, "weekly", period)
|
|
if period in ["daily", "monthly"]:
|
|
period_amount = round(period_amount)
|
|
return period_amount
|
|
|
|
def _l10n_au_compute_medicare_adjustment(self, period_earning, period, params):
|
|
self.ensure_one()
|
|
params = params.copy()
|
|
employee_id = self.employee_id
|
|
if employee_id.children and employee_id.marital in ["cohabitant", "married"]:
|
|
params["MLFT"] += employee_id.children * params["ADDC"]
|
|
|
|
params["MLFT"] = round(params["MLFT"] / params["WFTD"], 2)
|
|
params["SOP"] = round(params["MLFT"] * params["SOPM"] / params["SOPD"])
|
|
weekly_earning = self._l10n_au_compute_weekly_earning(period_earning, period)
|
|
|
|
adjustment = 0.0
|
|
if weekly_earning < params["WEST"]:
|
|
adjustment = (weekly_earning - params["WLA"]) * params["SOPM"]
|
|
elif weekly_earning < params["MLFT"]:
|
|
adjustment = weekly_earning * params["ML"]
|
|
elif weekly_earning < params["SOP"]:
|
|
adjustment = (params["MLFT"] * params["ML"]) - ((weekly_earning - params["MLFT"]) * params["SOPD"])
|
|
|
|
amount = round(adjustment)
|
|
period_amount = self._l10n_au_convert_amount(amount, "weekly", period)
|
|
if period in ["daily", "monthly"]:
|
|
period_amount = round(period_amount)
|
|
return period_amount
|
|
|
|
def _l10n_au_compute_lumpsum_withhold(self, lumpsum):
|
|
'''
|
|
Withholding for back payments is calculated by apportioning it over the number of periods it applies for and
|
|
summing the difference in withholding over every period.
|
|
'''
|
|
self.ensure_one()
|
|
return lumpsum * 0.47
|
|
|
|
def _l10n_au_compute_loan_withhold(self, period_earning, period, coefficients):
|
|
self.ensure_one()
|
|
weekly_earning = self._l10n_au_compute_weekly_earning(period_earning, period)
|
|
weekly_withhold = 0.0
|
|
if weekly_earning <= coefficients[0][1]:
|
|
return 0.0
|
|
|
|
for coef in coefficients:
|
|
if coef[1] == "inf" or weekly_earning <= coef[1]:
|
|
weekly_withhold = coef[2] / 100 * weekly_earning
|
|
break
|
|
|
|
amount = round(weekly_withhold)
|
|
period_amount = self._l10n_au_convert_amount(amount, "weekly", period)
|
|
if period in ["daily", "monthly"]:
|
|
period_amount = round(period_amount)
|
|
|
|
return period_amount
|
|
|
|
def _l10n_au_compute_termination_withhold(self, employee_id, ytd_total):
|
|
self.ensure_one()
|
|
etp_withholding = self._rule_parameter("l10n_au_etp_withholding")
|
|
etp_cap = self._rule_parameter("l10n_au_etp_cap")
|
|
etp_whoic_cap = self._rule_parameter("l10n_au_whoic_cap") - ytd_total["slip_lines"]["Gross"]["total"]
|
|
genuine_redundancy = self.env.ref("l10n_au_hr_payroll.input_genuine_redundancy")
|
|
early_retirement = self.env.ref("l10n_au_hr_payroll.input_early_retirement_scheme")
|
|
|
|
preservation_age = datetime.strptime(self._rule_parameter("l10n_au_preservation_age"), "%Y-%m-%d").date()
|
|
under_over = (employee_id.birthday or date.today()) > preservation_age
|
|
|
|
base = self._rule_parameter("l10n_au_tax_free_base")
|
|
yearly = self._rule_parameter("l10n_au_tax_free_year")
|
|
complete_years_of_service = relativedelta(self.date_to, employee_id.first_contract_date).years
|
|
tax_free_base_limit = base + yearly * complete_years_of_service
|
|
|
|
rate_over_cap = etp_withholding["over_cap"]
|
|
|
|
withholding = 0.0
|
|
non_taxable_amount = 0.0
|
|
for inpt in self.input_line_ids.sorted(key=lambda i: i.input_type_id.l10n_au_etp_cap, reverse=True):
|
|
if not inpt.input_type_id.l10n_au_is_etp:
|
|
continue
|
|
taxable_amount = inpt.amount
|
|
# 1. if the input is a genuine_redundancy or early retirement, calculate the taxable amount
|
|
if inpt.input_type_id in [genuine_redundancy, early_retirement]:
|
|
non_taxable_amount = min(tax_free_base_limit, inpt.amount)
|
|
taxable_amount = max(0, inpt.amount - tax_free_base_limit)
|
|
# 2. get the correct cap and calculate taxable amount under and over cap
|
|
rate_up_to_cap = etp_withholding[inpt.input_type_id.l10n_au_etp_type]["over" if under_over else "under"]
|
|
cap_to_use = min(etp_cap, etp_whoic_cap)
|
|
amount_under_cap = min(cap_to_use, taxable_amount)
|
|
amount_over_cap = max(0, taxable_amount - cap_to_use)
|
|
# 3. calculate withholding
|
|
withholding += amount_under_cap * rate_up_to_cap / 100
|
|
withholding += amount_over_cap * rate_over_cap / 100
|
|
etp_cap -= taxable_amount
|
|
return round(withholding), non_taxable_amount
|
|
|
|
def _l10n_au_get_leaves_for_withhold(self):
|
|
self.ensure_one()
|
|
cutoff_dates = [datetime(1978, 8, 16).date(), datetime(1993, 8, 17).date()]
|
|
leaves_by_date = {
|
|
"annual": {
|
|
"pre_1978": 0.0,
|
|
"pre_1993": 0.0,
|
|
"post_1993": 0.0,
|
|
},
|
|
"long_service": {
|
|
"pre_1978": 0.0,
|
|
"pre_1993": 0.0,
|
|
"post_1993": 0.0,
|
|
},
|
|
"leaves_amount": 0.0,
|
|
}
|
|
leaves = self.env["hr.leave.allocation"].search([
|
|
("employee_id", "=", self.employee_id.id),
|
|
("state", "=", "validate"),
|
|
])
|
|
daily_wage = self._get_daily_wage()
|
|
for leave in leaves:
|
|
if leave.leaves_taken == leave.number_of_days:
|
|
continue
|
|
leave_type = leaves_by_date[leave.holiday_status_id.l10n_au_leave_type]
|
|
amount = (leave.number_of_days - leave.leaves_taken) * daily_wage
|
|
if leave.date_from < cutoff_dates[0]:
|
|
leave_type["pre_1978"] += amount
|
|
elif leave.date_from < cutoff_dates[1]:
|
|
leave_type["pre_1993"] += amount
|
|
else:
|
|
leave_type["post_1993"] += amount
|
|
leaves_by_date["leaves_amount"] += amount
|
|
return leaves_by_date
|
|
|
|
def _l10n_au_calculate_marginal_withhold(self, year_slips, leave_amount, coefficients):
|
|
self.ensure_one()
|
|
period = self.contract_id.schedule_pay
|
|
amount_per_period = leave_amount / PERIODS_PER_YEAR[period]
|
|
|
|
last_payslip = year_slips[-2] if len(year_slips) > 1 else False
|
|
if not last_payslip:
|
|
last_payslip = self
|
|
|
|
normal_withhold = self._l10n_au_compute_withholding_amount(self.basic_wage, period, coefficients)
|
|
leave_withhold = self._l10n_au_compute_withholding_amount(self.basic_wage + amount_per_period, period, coefficients)
|
|
|
|
extra_withhold = leave_withhold - normal_withhold
|
|
return extra_withhold * PERIODS_PER_YEAR[period]
|
|
|
|
def _l10n_au_calculate_long_service_leave_withholding(self, year_slips, leave_withholding_rate, long_service_leaves):
|
|
self.ensure_one()
|
|
coefficients = self._rule_parameter("l10n_au_withholding_coefficients")["regular"]
|
|
pre_1978 = long_service_leaves["pre_1978"]
|
|
pre_1993 = long_service_leaves["pre_1993"]
|
|
post_1993 = long_service_leaves["post_1993"]
|
|
|
|
flat_part = pre_1993
|
|
marginal_part = pre_1978 * 0.05
|
|
|
|
if self.l10n_au_termination_type == "normal":
|
|
marginal_part += post_1993
|
|
else:
|
|
flat_part += post_1993
|
|
|
|
marginal_withhold = round(self._l10n_au_calculate_marginal_withhold(year_slips, marginal_part, coefficients))
|
|
flat_withhold = round(flat_part * float(leave_withholding_rate) / 100)
|
|
return flat_withhold + marginal_withhold
|
|
|
|
def _l10n_au_calculate_annual_leave_withholding(self, year_slips, leave_withholding_rate, annual_leaves):
|
|
self.ensure_one()
|
|
coefficients = self._rule_parameter("l10n_au_withholding_coefficients")["regular"]
|
|
pre_1993 = annual_leaves["pre_1993"]
|
|
post_1993 = annual_leaves["post_1993"]
|
|
|
|
flat_part = pre_1993
|
|
marginal_part = 0.0
|
|
|
|
if self.l10n_au_termination_type == "normal":
|
|
marginal_part += post_1993
|
|
else:
|
|
flat_part += post_1993
|
|
|
|
marginal_withhold = round(self._l10n_au_calculate_marginal_withhold(year_slips, marginal_part, coefficients))
|
|
flat_withhold = round(flat_part * float(leave_withholding_rate) / 100)
|
|
|
|
return flat_withhold + marginal_withhold
|
|
|
|
def _l10n_au_compute_unused_leaves_withhold(self, year_slips):
|
|
self.ensure_one()
|
|
leaves = self._l10n_au_get_leaves_for_withhold()
|
|
l10n_au_leave_withholding = self._rule_parameter("l10n_au_leave_withholding")
|
|
withholding = 0.0
|
|
# 2. Calculate long service leave withholding
|
|
long_service_leaves = leaves["long_service"]
|
|
withholding += self._l10n_au_calculate_long_service_leave_withholding(year_slips, l10n_au_leave_withholding, long_service_leaves)
|
|
# 3. Calculate annual leave withholding
|
|
annual_leaves = leaves["annual"]
|
|
withholding += self._l10n_au_calculate_annual_leave_withholding(year_slips, l10n_au_leave_withholding, annual_leaves)
|
|
return leaves["leaves_amount"], withholding, 0.0
|
|
|
|
def _l10n_au_compute_child_support(self, net_earnings):
|
|
self.ensure_one()
|
|
pea = self._rule_parameter("l10n_au_pea")
|
|
employee_id = self.employee_id
|
|
withhold = 0.0
|
|
|
|
# garnishee child support does not apply the pea, first apply the lumpsum deductions
|
|
# then the regular deductions
|
|
lumpsum_child_support = sum(self.input_line_ids.sudo().filtered(lambda inpt: inpt.input_type_id.code == 'CHILD_SUPPORT').mapped('amount'))
|
|
lumpsum_child_support = min(net_earnings, lumpsum_child_support)
|
|
withhold = lumpsum_child_support
|
|
net_earnings -= withhold
|
|
|
|
if net_earnings:
|
|
if employee_id.l10n_au_child_support_garnishee == "fixed":
|
|
withhold += min(net_earnings, employee_id.l10n_au_child_support_garnishee_amount)
|
|
elif employee_id.l10n_au_child_support_garnishee == "percentage":
|
|
withhold += net_earnings * employee_id.l10n_au_child_support_garnishee_amount
|
|
net_earnings -= withhold
|
|
|
|
if net_earnings > pea:
|
|
net_over_pea = net_earnings - pea
|
|
withhold += min(net_over_pea, employee_id.l10n_au_child_support_deduction)
|
|
return withhold
|
|
|
|
def _l10n_au_has_extra_pay(self):
|
|
self.ensure_one()
|
|
pay_day = int(self.contract_id.l10n_au_pay_day)
|
|
today = fields.Date.today().replace(month=1, day=1)
|
|
return today.weekday() == pay_day
|