Files
enterprise-addons/l10n_au_hr_payroll_api/models/l10n_au_superstream.py
T

370 lines
19 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import logging
from datetime import timedelta
from markupsafe import Markup
from odoo import models, fields, _
from odoo.exceptions import ValidationError, UserError
from odoo.tools.misc import groupby, itemgetter
from odoo.fields import Domain
from .l10n_au_super_stream_utils import employerDetails, contributionDetails
_logger = logging.getLogger(__name__)
class L10n_auSuperStream(models.Model):
_inherit = "l10n_au.super.stream"
message_id = fields.Char("API Message ID", readonly=True, copy=False)
payment_status = fields.Selection(
selection=[
("draft", "Draft"),
("EMPLOYER_PAYMENT", "Employer Pending"),
("FUND_PAYMENT", "Fund Pending"),
("PAYMENT_COMPLETE", "Success"),
("pending_cancel", "Pending Cancellation"), # Intermediate state when requested Cancellation
("PAYMENT_CANCELLED", "Cancelled")],
string="Payment Status",
required=True,
default="draft",
readonly=True,
copy=False,
help="""
Payment status for the overall payment of the transaction\n
Employer Pending - Employer direct debit has been initiated by SuperChoice.
Fund Pending - Employer direct debit has successfully cleared and payment to super funds have been initiated.
Success - All fund payments have been completed. This is a final state - you do not need to keep polling.
Pending Cancellation - Intermediate state when a cancellation for payment has been requested.
Cancelled - If any payments have been dishonoured or requested to be stopped prior to completion. This is a final state - you do not need to keep polling.
"""
)
payment_ref = fields.Char("Direct Debit Payment Ref.", readonly=True, copy=False, tracking=True)
source_payment_status = fields.Selection(
selection=[
("PENDING", "Pending"),
("COMPLETED", "Success"),
("SOURCE_FAILED", "Source Failed"),
("SOURCE_CANCELLED", "Source Cancelled")
],
string="Direct Debit Status",
required=True,
default="PENDING",
readonly=True,
tracking=True,
copy=False,
help="""
Payment status for the Employer's direct debit to the Clearing House.
Pending - Employer direct debit has been initiated by SuperChoice.
Success - Employer direct debit has successfully cleared.
Source Failed - Employer direct debit has been dishonoured. The employer will need to resubmit with adequate funds in the bank account or with updated bank details.
Source Cancelled - Employer has requested for the direct debit to be stopped. If SuperChoice have already debited the employer, the funds will be refunded back to the employer's account after Day 3
"""
)
to_be_replaced = fields.Boolean(
"To be Replaced",
compute="_compute_to_be_replaced",
)
days_funds_update = fields.Integer(compute="_compute_funds_update")
def _compute_to_be_replaced(self):
for record in self:
record.to_be_replaced = bool(
record.l10n_au_super_stream_lines.filtered(
lambda x: x.dest_payment_status
in ("DESTINATION_FAILED", "DESTINATION_CANCELLED")
and not x.replaced_line_id
)
)
def _compute_funds_update(self):
last_call = self.env.ref("l10n_au_hr_payroll_api.l10n_au_super_update_funds").lastcall or fields.Datetime.now() - timedelta(days=1)
diff = fields.Datetime.now() - last_call
self.days_funds_update = diff.days if diff else 0
def action_update_funds(self):
self.env.ref("l10n_au_hr_payroll_api.l10n_au_super_update_funds").method_direct_trigger()
self._compute_funds_update()
def _validate_with_superchoice(self):
""" Validation checks with SuperChoice API before submitting the report """
iap_proxy = self.company_id._l10n_au_payroll_get_proxy_user()
if not iap_proxy:
raise ValidationError(_("Please register your payroll before submitting the report. "
"You can register it at Configuration > Settings > Payroll > Register Payroll."))
response = iap_proxy._l10n_au_payroll_request(
"/superchoice/validate_employer",
{
"data": {"employer": {"employerDetails": employerDetails(self)}}
},
handle_errors=False,
)
if "error" in response:
raise ValidationError(_("Error Validating Employer Details:\n%s", response['error']))
response = iap_proxy._l10n_au_payroll_request(
"/superchoice/validate_fund",
{
"data": {"fundDetails": contributionDetails(self, members=False)["employer"]}
},
handle_errors=False,
)
if "error" in response:
raise ValidationError(_("Error Validating Funds :\n%s", response['error']))
response = iap_proxy._l10n_au_payroll_request(
"/superchoice/contribution",
{
"data": {"contributionDetails": contributionDetails(self)},
},
handle_errors=False,
)
if "error" in response:
raise ValidationError(response["error"])
def _create_super_stream_file(self):
super()._create_super_stream_file()
# Validate contributions
self._validate_with_superchoice()
def action_open_payment(self):
refunds = self.env["account.payment"].search([
("payment_type", "=", "inbound"),
("memo", "=", self.name + " (Refund)"),
])
return (refunds + self.payment_id)._get_records_action()
def action_register_super_payment(self):
action = super().action_register_super_payment()
# Once the payment has successfully been created, we can submit to SuperChoice
iap_proxy = self.company_id._l10n_au_payroll_get_proxy_user()
if not iap_proxy:
raise ValidationError(_("Please register your payroll before submitting the report. "
"You can register it at Configuration > Settings > Payroll > Register Payroll."))
response = iap_proxy._l10n_au_payroll_request(
"/superchoice/contribution", {
"data": {"contributionDetails": contributionDetails(self)},
"validate": False
}
)
self.message_id = response["messageId"]
self.payment_status = "EMPLOYER_PAYMENT"
return action
def action_check_cancelation_type(self):
return {
"name": _("Cancel Payment"),
"view_mode": "form",
"res_model": "l10n_au.superstream.cancel",
"view_id": self.env.ref("l10n_au_hr_payroll_api.l10n_au_superstream_cancel_view_form").id,
"type": "ir.actions.act_window",
"target": "new",
"context": {
"default_l10n_au_super_stream_id": self.id,
},
}
def action_cancel(self, force_refund=False, force_cancel=False, request_cancellation=True):
""" Cancel the selected SuperStream
force_refund: Forces the creation of a refund payment even if the payment was not reconciled
in Odoo.
"""
self.update_payment_status()
if self.payment_status not in ("EMPLOYER_PAYMENT", "FUND_PAYMENT"):
raise ValidationError(_("The payment can only be cancelled if it is pending. Payment has not been processed yet."))
# If the Direct debit Payment was successful, we need to make a refund payment.
# Else, ask the user to confirm if the payment was made.
if self.source_payment_status == "COMPLETED" or self.payment_id.state == "paid" or force_refund:
# Payment was already made, so we need a refund payment
pay_method_line = self.journal_id.with_context(l10n_au_super_payment=True) \
._get_available_payment_method_lines('inbound') \
.filtered(lambda x: x.code == 'ss_dd')
if not self._get_default_payment_account(pay_method_line):
raise UserError(_(
"An Outstanding Reciepts Account for the payment method '%(payment_method)s' is required to allow reconciliation.\n"
"Unable to find a default 'Outstanding Reciepts Account'. \n"
"Please select one under Accounting > Configuration > Journals > '%(journal)s' > Incoming Payments",
payment_method=pay_method_line.name, journal=self.journal_id.name))
clearing_house_partner = self.env.ref('l10n_au_hr_payroll_account.res_partner_clearing_house', raise_if_not_found=False)
if not clearing_house_partner.property_account_payable_id:
raise UserError(_("Please set the SuperStream Payable Account for company %s.", self.company_id.name))
payment = self.env['account.payment'].create({
'date': fields.Datetime.now(),
'amount': self.amount_total,
'payment_type': 'inbound',
'partner_id': clearing_house_partner.id,
'partner_type': 'supplier',
'memo': self.name + " (Refund)",
'journal_id': self.journal_id.id,
'currency_id': self.currency_id.id,
'payment_method_line_id': pay_method_line.id,
})
payment.action_post()
payment_message = _("A refund payment (%s) has been created successfully!", payment._get_html_link())
elif force_cancel:
# Payment failed and requires cancellation
self.payment_id.action_cancel()
payment_message = _("The outgoing payment (%s) has been cancelled!", self.payment_id._get_html_link())
else:
return self.action_check_cancelation_type()
# Should not request cancellation in case of failure, instead just cancel the payment
if not request_cancellation:
return
iap_proxy = self.company_id._l10n_au_payroll_get_proxy_user()
if not iap_proxy:
raise ValidationError(_("Please register your payroll before submitting the report. "
"You can register it at Configuration > Settings > Payroll > Register Payroll."))
response = iap_proxy._l10n_au_payroll_request(
"/superchoice/cancel_payment",
{
"transaction_id": self.message_id
},
handle_errors=False,
)
if "error" in response:
raise ValidationError(response["error"])
if "success" in response:
self.payment_status = "pending_cancel"
self.message_post(body=_(
"Payment cancellation requested successfully %(line_break)s %(payment_message)s",
line_break=Markup('<br/>\n'),
payment_message=payment_message
), author_id=self.env.user.partner_id.id)
def _get_failed_lines(self):
""" Returns the SuperStream lines that have failed in the destination payment """
return self.l10n_au_super_stream_lines.filtered(
lambda x: x.dest_payment_status in ("DESTINATION_FAILED", "DESTINATION_CANCELLED") and not x.replaced_line_id
)
def update_payment_status(self):
if not self:
records_to_update = self.search([("state", "=", "done"), ("l10n_au_super_stream_lines.dest_payment_status", "=", "PENDING")])
else:
records_to_update = self
for company, records in records_to_update.grouped(lambda x: x.company_id).items():
if records.filtered(lambda x: x.state != "done"):
raise ValidationError(_("The payment status can only be updated for SuperStream records "
"that have been submitted."))
iap_proxy = company._l10n_au_payroll_get_proxy_user()
if not iap_proxy:
raise ValidationError(_("Please register your payroll before submitting the report. "
"You can register it at Configuration > Settings > Payroll > Register Payroll."))
response = iap_proxy._l10n_au_payroll_request(
"/superchoice/get_payment_status",
{
"transaction_ids": records.mapped("message_id")
},
handle_errors=True,
)
for record in records:
record_response = response.get(record.message_id, {})
if "error" in record_response:
_logger.error(record_response["error"])
record.message_post(body=record_response["error"])
else:
if not record_response["paymentTransaction"]:
return
if record.payment_status == 'pending_cancel' and record_response["paymentOverview"]["paymentStatus"] != "PAYMENT_CANCELLED":
# When payment is cancelled, the status will be updated to PAYMENT_CANCELLED after a while
# until then, we use the temporary pending_cancel status
continue
record.payment_status = record_response["paymentOverview"]["paymentStatus"]
transaction_messages = []
for ref, transactions in groupby(record_response["paymentTransaction"], key=itemgetter("paymentReference")):
# Get the first transaction
transaction = transactions[0]
# Payment from source to clearing house
domains = []
if transaction["transactionType"] == "SOURCE":
record.source_payment_status = transaction["paymentStatus"]
record.payment_ref = ref
continue
elif transaction["transactionType"] == "DESTINATION" and "usi" in transaction:
# Fund Payment can be matched using USI but optional for SMSF
domains.append([("payee_id.usi", "=", transaction["usi"])])
elif transaction["transactionType"] == "DESTINATION":
# SMSF Fund Payment can be matched using ABN
domains.append([("payee_id.abn", "=", transaction["australianBusinessNumber"])])
fund_lines = record.l10n_au_super_stream_lines.filtered_domain(Domain.OR(domains))
# Update and Log a message if payment status changes
if any(fund_line.dest_payment_status != transaction["paymentStatus"] for fund_line in fund_lines):
transaction_messages.append(_("Payment status for fund '%(fund_name)s' has been updated from '%(from_status)s' to '%(to_status)s'.%(line_break)s"
"%(b)sReference%(b_end)s: %(ref)s%(line_break)s"
"%(b)sExpected Payment Amount%(b_end)s: %(expected_amount)s%(line_break)s"
"%(b)sActual Payment Amount%(b_end)s: %(actual_amount)s%(line_break)s",
b=Markup("<b>"), b_end=Markup("</b>"),
line_break=Markup("<br/>"),
fund_name=fund_lines[0].payee_id.display_name,
from_status=fund_lines[0].dest_payment_status,
to_status=transaction["paymentStatus"], ref=ref,
expected_amount=f"{transaction['expectedPaymentAmount']['currency']} {transaction['expectedPaymentAmount']['value']}",
actual_amount=f"{transaction['actualPaymentAmount']['currency']} {transaction['actualPaymentAmount']['value']}"))
fund_lines.write({
"dest_payment_status": transaction["paymentStatus"],
"dest_payment_ref": ref
})
if transaction_messages:
record.message_post(body=Markup("<br/>").join(transaction_messages))
if record.payment_status == "PAYMENT_CANCELLED":
record.activity_unlink(["l10n_au_hr_payroll_account.l10n_au_activity_resubmit_super"])
record.activity_schedule(
"l10n_au_hr_payroll_account.l10n_au_activity_resubmit_super",
user_id=record.company_id.l10n_au_hr_super_responsible_id.user_id.id,
note=_("Payment has been successfully cancelled. Please proceed with the needed corrections and resubmit")
)
elif self._get_failed_lines():
record.activity_unlink(["l10n_au_hr_payroll_account.l10n_au_activity_resubmit_super"])
record.activity_schedule(
"l10n_au_hr_payroll_account.l10n_au_activity_resubmit_super",
user_id=record.company_id.l10n_au_hr_super_responsible_id.user_id.id,
note=_("Some of the fund payments may have failed. Please check the report and resubmit if needed.")
)
def action_resubmit_failed(self):
failed_lines = self._get_failed_lines()
if not failed_lines:
raise ValidationError(_("No failed transactions to resubmit."))
for failed_line in failed_lines:
failed_line.replaced_line_id = failed_line.copy().id
duplicated = self.copy({"l10n_au_super_stream_lines": failed_lines.replaced_line_id})
# Cancel the orignal payments
if self.source_payment_status == "SOURCE_FAILED":
self.action_cancel(force_cancel=True, request_cancellation=False)
# TODO: Partial refund amount for fund failure
self.message_post(body=_("Failed transactions are being resubmitted in a new SuperStream report: %s", duplicated._get_html_link()))
return duplicated.get_formview_action()
class L10n_auSuperStreamLine(models.Model):
_inherit = "l10n_au.super.stream.line"
dest_payment_ref = fields.Char("Fund Payment Ref.", readonly=True, copy=False)
dest_payment_status = fields.Selection(
selection=[
("PENDING", "Pending"),
("COMPLETED", "Success"),
("DESTINATION_FAILED", "Destination Failed"),
("DESTINATION_CANCELLED", "Destination Cancelled"),
],
string="Fund Payment Status",
required=True,
default="PENDING",
readonly=True,
copy=False,
help="Payment status for the Clearing House's payment to the Super Fund"
)
replaced_line_id = fields.Many2one("l10n_au.super.stream.line", "Replaced Line", readonly=True, copy=False)