1
0
forked from Mapan/odoo17e
odoo17e-kedaikipas58/addons/l10n_au_hr_payroll/models/hr_payslip.py
2024-12-10 09:04:09 +07:00

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