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

372 lines
19 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from lxml import etree
from datetime import date
from collections import defaultdict
from odoo import api, fields, models, _
from odoo.tools.misc import file_path
class L10nAuPayEvent0004(models.Model):
_name = "l10n_au.payevnt.0004"
_description = "Pay Event 0004"
name = fields.Char(string="Name", compute="_compute_name")
payslip_batch_id = fields.Many2one("hr.payslip.run", string="Payslip Batch")
payslip_ids = fields.Many2many("hr.payslip", string="Payslip")
currency_id = fields.Many2one(
"res.currency",
string="Currency",
related="payslip_batch_id.currency_id",
readonly=True)
payevent_type = fields.Selection(
selection=[
("submit", "Submit"),
("update", "Update")],
string="Type",
default="submit",
required=True)
previous_id_bms = fields.Char(string="Previous BMS ID")
ffr = fields.Boolean(
string="Full File Replacement",
help="Indicates if this report should replace the previous report with the same transaction identifier")
previous_report_id = fields.Many2one(
"l10n_au.payevnt.0004", string="Previous Report",
help="Report which you are updating")
intermediary = fields.Many2one("res.partner", string="Intermediary")
submit_date = fields.Date(
string="Submit Date",
help="Enter manual submit date if you want to submit the report at a particular date")
submission_id = fields.Char(
string="Submission ID",
readonly=True,
help="Submission ID of the report")
# XML report fields
state = fields.Selection([("generate", "generate"), ("get", "get"), ("sent", "sent")], default="generate")
xml_file = fields.Binary("XML File", readonly=True, attachment=False)
xml_filename = fields.Char()
xml_validation_state = fields.Selection([
("normal", "N/A"),
("done", "Valid"),
("invalid", "Invalid"),
], default="normal", compute="_compute_validation_state", store=True)
error_message = fields.Char("Error Message", compute="_compute_validation_state", store=True)
warning_message = fields.Char(compute="_compute_warning_message")
# constraints ffr, cannot be true if type is update
_sql_constraints = [
("ffr", "CHECK(ffr = false OR payevent_type = 'submit')", "Full File Replacement cannot be true if type is 'update'."),
("l10n_au_l10n_au_previous_report", "CHECK(previous_report_id != id)", "A report can't update iself.")
]
@api.depends("l10n_au_payevnt_emp_ids")
def _compute_payee_record_count(self):
for report in self:
report.l10n_au_payee_record_count = len(report.l10n_au_payevnt_emp_ids)
@api.depends("xml_file")
def _compute_validation_state(self):
payevent_xsd_root = etree.parse(file_path("l10n_au_hr_payroll/data/l10n_au_payevnt_0004.xsd"))
payeventemp_xsd_root = etree.parse(file_path("l10n_au_hr_payroll/data/l10n_au_payevntemp_0004.xsd"))
payevent_schema = etree.XMLSchema(payevent_xsd_root)
payeventemp_schema = etree.XMLSchema(payeventemp_xsd_root)
no_xml_file_records = self.filtered(lambda record: not record.xml_file)
no_xml_file_records.update({
"xml_validation_state": "normal",
"error_message": False})
for record in self - no_xml_file_records:
xml_root = etree.fromstring(base64.b64decode(record.xml_file))
payevent_root = xml_root.find("{http://www.sbr.gov.au/ato/payevnt}PAYEVNT")
payeventemp_roots = xml_root.findall("{http://www.sbr.gov.au/ato/payevntemp}PAYEVNTEMP")
try:
payevent_schema.assertValid(payevent_root)
for payeventemp_root in payeventemp_roots:
payeventemp_schema.assertValid(payeventemp_root)
record.xml_validation_state = "done"
except etree.DocumentInvalid as err:
record.xml_validation_state = "invalid"
record.error_message = str(err)
@api.depends("payslip_batch_id", "payslip_ids", "payevent_type")
def _compute_name(self):
for report in self:
if report.payslip_batch_id:
report.name = report.payslip_batch_id.name
elif report.payslip_ids:
if len(report.payslip_ids) > 1:
report.name = ", ".join(payslip.employee_id.name for payslip in report.payslip_ids[:3])
if len(report.payslip_ids) > 3:
report.name += ",..."
report.name = report.payslip_ids[0].employee_id.name
else:
report.name = ""
def _compute_warning_message(self):
for report in self:
company_warnings, user_warnings = [], []
if not self.env.company.zip:
company_warnings.append("Postcode.")
if not self.env.company.country_id:
company_warnings.append("Country.")
if not self.env.user.work_email:
user_warnings.append("Work Email.")
if not self.env.user.work_phone:
user_warnings.append("Work Phone.")
message = ""
if company_warnings:
message += "\n".join(["Missing required company information:"] + company_warnings)
if user_warnings:
message += "\n".join(["Missing required user information:"] + user_warnings)
report.warning_message = message
def _get_complex_rendering_data(self, payslips_ids):
today = fields.Date.today()
financial_year = today.year if today.month > 6 else today.year - 1
start_financial_year = date(financial_year, 7, 1)
employees = payslips_ids.employee_id
# == Date and Run Date ==
if self.payevent_type == "submit":
run_date = self.payslip_batch_id.paid_date or self.create_date
submit_date = self.payslip_batch_id.paid_date or self.create_date
elif self.payevent_type == "update":
run_date = self.payslip_batch_id.paid_date or self.create_date
submit_date = self.submit_date or self.create_date
if self.submit_date < start_financial_year:
submit_date = start_financial_year
# == Totals == (may not be reported in an update event)
line_codes = ["GROSS", "WITHHOLD.TOTAL"]
all_line_values = payslips_ids._get_line_values(line_codes, vals_list=['total', 'quantity'])
mapped_total = {
code: sum(all_line_values[code][p.id]['total'] for p in payslips_ids) for code in line_codes
}
paygw = -mapped_total["WITHHOLD.TOTAL"]
gross = mapped_total["GROSS"]
child_garnish = 0.0
child_withhold = 0.0
extra_data = {
"PaymentRecordTransactionD": submit_date.date(),
"MessageTimestampGenerationDt": run_date.isoformat(),
"PayAsYouGoWithholdingTaxWithheldA": paygw,
"TotalGrossPaymentsWithholdingA": gross,
"ChildSupportGarnisheeA": child_garnish,
"ChildSupportWithholdingA": child_withhold,
}
# Employees extra data
unknown_date = date(1800, 1, 1)
min_date = date(1950, 1, 1)
for employee in employees:
payslips = payslips_ids.filtered(lambda p: p.employee_id == employee)
start_date = max(min_date, employee.first_contract_date) or unknown_date
remunerations = []
deductions = []
for payslip in payslips:
Remuneration = defaultdict(lambda: False)
worked_lines_ids = payslip.worked_days_line_ids
input_lines_ids = payslip.input_line_ids
contract_id = payslip.contract_id
# == Gross, income type, paygw ==
Remuneration["IncomeStreamTypeC"] = payslip.l10n_au_income_stream_type
# == Foreign income == (required for FEI, IAA, WHM )
if payslip.l10n_au_income_stream_type in ["FEI", "IAA", "WHM"]:
Remuneration["AddressDetailsCountryC"] = employee.private_country_id.code.lower()
Remuneration["IncomeTaxForeignWithholdingA"] = payslip.l10n_au_foreign_tax_withheld
Remuneration["IndividualNonBusinessExemptForeignEmploymentIncomeA"] = payslip.l10n_au_exempt_foreign_income
Remuneration["IncomeTaxPayAsYouGoWithholdingTaxWithheldA"] = -all_line_values["WITHHOLD.TOTAL"][payslip.id]["total"]
Remuneration["GrossA"] = all_line_values["GROSS"][payslip.id]["total"]
# == Paid Leave ==
leave_lines = worked_lines_ids.filtered(lambda l: l.work_entry_type_id.is_leave)
Remuneration["PaidLeaveCollection"] = []
for leave in leave_lines:
Remuneration["PaidLeaveCollection"].append({
"TypeC": leave.work_entry_type_id.code,
"PaymentA": leave.amount,
})
# == Allowance ==
allowance_lines = input_lines_ids.sudo().filtered(lambda l: l.input_type_id.l10n_au_is_allowance)
Remuneration["AllowanceCollection"] = []
for allowance in allowance_lines:
Remuneration["AllowanceCollection"].append({
"TypeC": allowance.code,
"OtherAllowanceTypeDe": allowance.name if allowance.code == "OD" else "",
"PaymentA": allowance.amount,
})
# == Overtime ==
overtime_work_entry_type = self.env.ref("hr_work_entry.overtime_work_entry_type")
overtime_lines = worked_lines_ids.filtered(lambda l: l.work_entry_type_id == overtime_work_entry_type)
Remuneration["OvertimePaymentA"] = sum(overtime_lines.mapped("amount"))
# == Bonuses and commissions ==
bonus_commission_input_type = self.env.ref("l10n_au_hr_payroll.input_gross_bonuses_and_commissions")
bonus_commissions_lines = input_lines_ids.filtered(lambda l: l.input_type_id == bonus_commission_input_type)
Remuneration["GrossBonusesAndCommissionsA"] = sum(bonus_commissions_lines.mapped("amount"))
# == Directors fees ==
directors_fee_input_type = self.env.ref("l10n_au_hr_payroll.input_gross_director_fee")
directors_fee_lines = input_lines_ids.filtered(lambda l: l.input_type_id == directors_fee_input_type)
Remuneration["GrossDirectorsFeesA"] = sum(directors_fee_lines.mapped("amount"))
# == CDEP ==
cdep_input_type = self.env.ref("l10n_au_hr_payroll.input_gross_cdep")
cdep_lines = input_lines_ids.filtered(lambda l: l.input_type_id == cdep_input_type)
Remuneration["IndividualNonBusinessCommunityDevelopmentEmploymentProjectA"] = sum(cdep_lines.mapped("amount"))
# == Salary sacrifice ==
Remuneration["SalarySacrificeCollection"] = [
{"TypeC": "S", "PaymentA": contract_id.l10n_au_salary_sacrifice_superannuation},
{"TypeC": "O", "PaymentA": contract_id.l10n_au_salary_sacrifice_other},
]
# == Lump Sum (Loempia sum) ==
Remuneration["LumpSumCollection"] = []
remunerations.append(Remuneration)
# == DEDUCTIONS ==
if contract_id.l10n_au_workplace_giving:
deductions.append({
"RemunerationTypeC": "W",
"RemunerationA": contract_id.l10n_au_workplace_giving,
})
if contract_id.employee_id.l10n_au_child_support_garnishee_amount:
deductions.append({
"RemunerationTypeC": "G",
"RemunerationA": contract_id.employee_id.l10n_au_child_support_garnishee_amount,
})
if contract_id.employee_id.l10n_au_child_support_deduction:
deductions.append({
"RemunerationTypeC": "D",
"RemunerationA": contract_id.employee_id.l10n_au_child_support_deduction,
})
employee_data = {
"EmploymentStartD": start_date,
"Remuneration": remunerations,
"Deduction": deductions,
}
extra_data.update({
employee.id: employee_data
})
return extra_data
def _get_rendering_data(self, payslips_ids):
extra_data = self._get_complex_rendering_data(payslips_ids)
employer = defaultdict(str, {
"SoftwareInformationBusinessManagementSystemId": "125b8925-9a97-4178-8dee-78d3fdeb0437", # placeholder
"AustralianBusinessNumberId": self.env.company.vat or False,
"WithholdingPayerNumberId": self.env.company.l10n_au_wpn_number if not self.env.company.vat else "",
"OrganisationDetailsOrganisationBranchC": self.env.company.l10n_au_branch_code,
"PreviousSoftwareInformationBusinessManagementSystemId": self.previous_id_bms, # placeholder
"DetailsOrganisationalNameT": self.env.company.name,
"PersonUnstructuredNameFullNameT": self.env.user.name,
"ElectronicMailAddressT": self.env.user.work_email,
"TelephoneMinimalN": self.env.user.work_phone,
"PostcodeT": self.env.company.zip,
"CountryC": self.env.company.country_id.code.lower(),
"PaymentRecordTransactionD": extra_data["PaymentRecordTransactionD"],
"InteractionRecordCt": len(payslips_ids),
"MessageTimestampGenerationDt": extra_data["MessageTimestampGenerationDt"],
"InteractionTransactionId": "", # filled later
"AmendmentI": "true" if self.ffr else "false",
"SignatoryIdentifierT": self.env.user.name,
"SignatureD": date.today(),
"StatementAcceptedI": "true",
})
if self.payevent_type == "submit":
employer.update({
"PayAsYouGoWithholdingTaxWithheldA": extra_data["PayAsYouGoWithholdingTaxWithheldA"],
"TotalGrossPaymentsWithholdingA": extra_data["TotalGrossPaymentsWithholdingA"],
"ChildSupportGarnisheeA": extra_data["ChildSupportGarnisheeA"],
"ChildSupportWithholdingA": extra_data["ChildSupportWithholdingA"],
})
intermediary = defaultdict(str)
intermediary_id = self.intermediary
if intermediary_id:
intermediary.update({
"AustralianBusinessNumberId": intermediary_id.vat,
"TaxAgentNumberId": intermediary_id.l10n_au_ran_number,
"PersonUnstructuredNameFullNameT": intermediary_id.name,
"ElectronicMailAddressT": intermediary_id.email,
"TelephoneMinimalN": intermediary_id.phone,
"SignatoryIdentifierT": intermediary_id.name,
"SignatureD": False,
"StatementAcceptedI": False,
})
employees = []
for payslip in payslips_ids:
employee = payslip.employee_id
values = defaultdict(str, {
"TaxFileNumberId": employee.l10n_au_tfn,
"AustralianBusinessNumberId": employee.l10n_au_abn,
"EmploymentPayrollNumberId": employee.registration_number or str(employee.id),
"PreviousPayrollIDEmploymentPayrollNumberId": employee.l10n_au_previous_id_bms,
"FamilyNameT": ' '.join(employee.name.split(' ')[1:]),
"GivenNameT": employee.name.split(' ')[0],
"OtherGivenNameT": "",
"Dm": employee.birthday.day,
"M": employee.birthday.month,
"Y": employee.birthday.year,
"Line1T": employee.private_street,
"Line2T": employee.private_street2,
"LocalityNameT": employee.private_city,
"StateOrTerritoryC": employee.private_state_id.code,
"PostcodeT": employee.private_zip,
"CountryC": employee.private_country_id.code.lower() if employee.private_country_id else False,
"ElectronicMailAddressT": employee.work_email,
"TelephoneMinimalN": employee.work_phone,
"EmploymentStartD": extra_data[employee.id]["EmploymentStartD"],
"EmploymentEndD": False,
"PaymentBasisC": payslip.contract_id.l10n_au_employment_basis_code,
"CessationTypeC": payslip.contract_id.l10n_au_cessation_type_code,
"TaxTreatmentC": payslip.contract_id.l10n_au_tax_treatment_code,
"TaxOffsetClaimTotalA": employee.l10n_au_nat_3093_amount,
"StartD": payslip.date_from,
"EndD": payslip.date_to,
"RemunerationPayrollEventFinalI": str(payslip.struct_id.code == "AUTERM").lower(),
# Remuneration collection
"Remuneration": extra_data[employee.id]["Remuneration"],
# Deductions
"Deduction": extra_data[employee.id]["Deduction"],
# Super Contributions
"EntitlementTypeC": False,
"EmployerContributionsYearToDateA": False,
# Fringe Benefits
"FringeBenefitsReportableExemptionC": False,
"A": False,
})
employees.append(values)
# sequence at the end to avoid generating if there was an error
self.submission_id = self.env['ir.sequence'].next_by_code("payevent0004.transaction")
employer["InteractionTransactionId"] = self.submission_id
return {"employer": employer, "employees": employees, "intermediary": intermediary}
def action_generate_xml(self):
self.ensure_one()
self.xml_filename = '%s-PAYEVNT.0004.xml' % (self.name)
report = self.env['ir.qweb']._render('l10n_au_hr_payroll.payevent_0004_xml_report', self._get_rendering_data(self.payslip_ids))
# Prettify xml string
root = etree.fromstring(report, parser=etree.XMLParser(remove_blank_text=True, resolve_entities=False))
xml_str = etree.tostring(root, pretty_print=True, encoding='utf-8', xml_declaration=True)
self.xml_file = base64.b64encode(xml_str)
self.state = 'get'
class L10nAuPayevntEmp(models.Model):
_name = "l10n_au.payevnt.emp.0004"
_description = "Pay Event Employee"
employee_id = fields.Many2one(
"hr.employee", string="Employee", required=True)
payslip_id = fields.Many2one(
"hr.payslip", string="Payslip", required=True)
payslip_currency_id = fields.Many2one(
"res.currency", related="payslip_id.currency_id", readonly=True)
payevnt_0004_id = fields.Many2one(
"l10n_au.payevnt.0004", string="Pay Event 0004")