Files
enterprise-addons/l10n_us_direct_deposit/models/account_batch_payment.py
T

228 lines
11 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import re
from uuid import uuid4
from odoo import fields, models, modules
from odoo.exceptions import UserError
from .wise_request import Wise
class AccountBatchPayment(models.Model):
_inherit = 'account.batch.payment'
wise_batch_identifier = fields.Char(string='Wise Batch ID', readonly=True, copy=False)
wise_payments_enabled = fields.Boolean(compute='_compute_wise_payments_enabled')
wise_payment_status = fields.Selection(
string='Wise Status',
selection=[
('uninitiated', 'Uninitiated'),
('new', 'New'),
('completed', 'Completed'),
],
default='uninitiated', readonly=True, copy=False,
)
def _compute_wise_payments_enabled(self):
for batch in self:
batch.wise_payments_enabled = batch.company_id.wise_connected and batch.payment_method_code == 'wise_direct_deposit'
def open_wise_batch(self):
self.ensure_one()
if not self.wise_batch_identifier or not self.wise_payments_enabled:
raise UserError(self.env._("This batch payment is not yet created in Wise."))
url = "https://wise-sandbox.com" if self.company_id.wise_environment == 'sandbox' else "https://wise.com"
return {
'type': 'ir.actions.act_url',
'url': f"{url}/transactions/activities/by-resource/BATCH_TRANSFER/{self.wise_batch_identifier}",
}
def check_payments_for_warnings(self):
rslt = super().check_payments_for_warnings()
if not self.wise_payments_enabled:
return rslt
if len(self.payment_ids):
if payments_in_future := self.payment_ids.filtered(lambda payment: payment.date != fields.Date.context_today(self)):
rslt.append({
'title': self.env._("Wise will send the entire batch payment as soon as you fund it."),
'records': payments_in_future,
'help': self.env._("Scheduled payments are not yet supported."),
})
return rslt
def check_payments_for_errors(self):
rslt = super().check_payments_for_errors()
if not self.wise_payments_enabled:
return rslt
if len(self.payment_ids):
if non_usd_payments := self.payment_ids.filtered(lambda payment: payment.currency_id.name != 'USD'):
rslt.append({
'title': self.env._("All payments in the batch must be in USD."),
'records': non_usd_payments,
'help': self.env._("Non-USD payments are not yet supported."),
})
return rslt
def _send_after_validation(self):
if self.payment_method_code != 'wise_direct_deposit' or not self.wise_payments_enabled:
return super()._send_after_validation()
wise_api = Wise(self.company_id)
if not self.wise_batch_identifier:
batch_group = wise_api.create_batch_group(currency=self.currency_id.name, batch_name=self.name)
if wise_api.has_errors(batch_group):
raise UserError(self.env._("Failed to create a batch on Wise:\n%(wise_error)s", wise_error=wise_api.format_errors(batch_group)))
self.wise_batch_identifier = batch_group['id']
self.wise_payment_status = 'new'
if self._can_commit():
self.env.cr.commit()
if self.payment_ids.partner_bank_id.filtered(lambda bank: not bank.wise_bank_account):
all_recipients = wise_api.get_recipients()
if wise_api.has_errors(all_recipients):
raise UserError(self.env._("Failed to load vendors from Wise:\n%(wise_error)s", wise_error=wise_api.format_errors(all_recipients)))
recipients_dict = {self._generate_wise_key(recipient=recipient): recipient for recipient in all_recipients['content']}
for payment in self.payment_ids:
# Find or create a recipient for the partner
if not payment.partner_bank_id.wise_bank_account:
matched_recipient = recipients_dict.get(self._generate_wise_key(payment=payment))
if not matched_recipient:
matched_recipient = wise_api.create_recipient(self._prepare_wise_recipient_data(payment))
if wise_api.has_errors(matched_recipient):
raise UserError(self.env._("Failed to create a vendor on Wise:\n%(wise_error)s", wise_error=wise_api.format_errors(matched_recipient)))
payment.partner_bank_id.wise_bank_account = matched_recipient['id']
if self._can_commit():
self.env.cr.commit()
# Create one transfer that links quote and batch together
if not payment.wise_transfer_identifier:
# Create one quote per payment
quote = wise_api.create_quote({
"sourceCurrency": payment.currency_id.name,
"targetCurrency": payment.currency_id.name,
"targetAmount": payment.amount,
"targetAccount": payment.partner_bank_id.wise_bank_account,
"payOut": "BALANCE",
})
if wise_api.has_errors(quote):
raise UserError(self.env._("Failed to create a quote on Wise:\n%(wise_error)s", wise_error=wise_api.format_errors(quote)))
transfer = wise_api.create_transfer_in_batch(self.wise_batch_identifier, self._prepare_wise_transfer_data(payment, quote['id']))
if wise_api.has_errors(transfer):
raise UserError(self.env._("Failed to create a transfer on Wise:\n%(wise_error)s", wise_error=wise_api.format_errors(transfer)))
payment.wise_transfer_identifier = transfer['id']
if self._can_commit():
self.env.cr.commit()
# Mark the batch as complete for the one click pay on redirect.
batch_information = wise_api.get_batch_group(self.wise_batch_identifier)
if wise_api.has_errors(batch_information):
raise UserError(self.env._("Failed to retrieve batch information on Wise:\n%(wise_error)s", wise_error=wise_api.format_errors(batch_information)))
status = wise_api.complete_batch_group(self.wise_batch_identifier, batch_information.get('version'))
if wise_api.has_errors(status):
raise UserError(self.env._("Failed to mark the batch as complete on Wise:\n%(wise_error)s", wise_error=wise_api.format_errors(status)))
self.wise_payment_status = status['status'].lower()
url = "https://wise-sandbox.com" if self.company_id.sudo().wise_environment == 'sandbox' else "https://wise.com"
super()._send_after_validation()
return {
'type': 'ir.actions.act_url',
'url': f"{url}/transactions/batch/{self.wise_batch_identifier}",
}
@staticmethod
def _can_commit():
""" Helper to know if we can commit the current transaction or not.
:returns: True if commit is acceptable, False otherwise.
"""
return not modules.module.current_test
def _generate_wise_key(self, payment=None, recipient=None):
"""Build a dictionary key to match a payment's bank account to an
existing Wise recipient using only financial identifiers (routing
number, account number, account/transfer type)."""
if not payment and not recipient:
return None
if payment:
bank_account = payment.partner_bank_id
if bank_account.bank_id.country_code == 'US' or not bank_account.bank_id.country_code:
key_elements = [bank_account.wise_account_type, bank_account.l10n_us_bank_account_type,
bank_account.clearing_number, bank_account.sanitized_acc_number]
else:
# IBAN is globally unique - use it as the sole identifier for non-US accounts
# regardless of whether Wise stores them as SWIFT or IBAN type.
key_elements = ['non_us', bank_account.sanitized_acc_number]
if recipient:
recipient_type = recipient['type'].lower()
if recipient_type == 'iban':
key_elements = ['non_us', recipient['details']['iban']]
elif recipient_type in ('swift_code', 'swiftcode'):
key_elements = ['non_us', recipient['details']['accountNumber']]
else:
key_elements = [recipient_type, recipient['details']['accountType'].lower(),
recipient['details']['abartn'], recipient['details']['accountNumber']]
return tuple(key_elements)
def _prepare_wise_recipient_data(self, payment):
"""Prepare data to create a Wise recipient from a partner and payment"""
partner = payment.partner_id
bank_account = payment.partner_bank_id
if not bank_account:
raise UserError(self.env._("The journal '%s' must have a linked bank account to create a Wise recipient.", payment.journal_id.name))
bank_data = {
"profile": self.company_id.sudo().wise_profile_identifier,
"accountHolderName": partner.name,
"currency": payment.currency_id.name,
"details": {
"address": {
"city": partner.city,
"country": partner.country_id.code,
"postCode": partner.zip,
"state": partner.state_id.code,
"firstLine": partner.street,
},
"legalType": "PRIVATE" if partner.company_type == 'person' else "BUSINESS",
"accountNumber": bank_account.acc_number,
"email": partner.email,
},
}
if bank_account.bank_id.country_code and bank_account.bank_id.country_code != 'US': # Always use SWIFT outside of the US
bank_data["type"] = "swift_code" # always use SWIFT outside of the US.
bank_data['details']['swiftCode'] = bank_account.bank_bic
else:
bank_data["type"] = bank_account.wise_account_type
bank_data['details']['accountType'] = bank_account.l10n_us_bank_account_type
bank_data['details']['abartn'] = bank_account.clearing_number
return bank_data
def _prepare_wise_transfer_data(self, payment, quote_id):
bank_account = payment.partner_bank_id
if not payment.wise_unique_reference:
payment.wise_unique_reference = uuid4()
reference = payment.memo or payment.name
reference = re.sub(r'[^\w\s]', '', reference)
return {
'targetAccount': bank_account.wise_bank_account,
'quoteUuid': quote_id,
'customerTransactionId': payment.wise_unique_reference,
'details': {'reference': reference[:10]},
}