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

193 lines
10 KiB
Python

# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import fields, models, _
from odoo.exceptions import ValidationError, RedirectWarning
from odoo.tools import remove_accents
class AccountBatchPayment(models.Model):
_inherit = "account.batch.payment"
l10n_ca_cpa005_file_creation_number = fields.Char(
string="File Creation Number used in Canadian EFT",
copy=False,
help="Leave blank to auto-populate with the next number in the sequence. The FCN is a 4-digit sequence from 0001 "
"to 9999. This will be used by your bank to identify the file. You can set a value here to override the FCN "
"sequence just for this payment.",
)
def _l10n_ca_cpa005_pre_validate(self):
journal = self.journal_id
journal_errors = []
payments_without_tx_code = self.env["account.payment"]
if not journal.company_id.l10n_ca_cpa005_short_name:
raise RedirectWarning(
_("Please set a Canadian EFT short company name on the %s company.", journal.company_id.display_name),
journal.company_id._get_records_action(),
_("Go to company"),
)
if not journal.l10n_ca_cpa005_originator_id:
journal_errors.append(_("- Please set an originator ID on the %(journal)s journal."))
if not journal.l10n_ca_cpa005_destination_data_center:
journal_errors.append(_("- Please set a destination data center on the %(journal)s journal."))
if not journal.bank_account_id:
journal_errors.append(_("- Please set an account number on the %(journal)s journal."))
elif not journal.bank_account_id.l10n_ca_financial_institution_number:
raise RedirectWarning(
_(
"Please set a Financial Institution ID Number on the %(account)s bank account of %(partner)s.",
account=journal.bank_account_id.display_name,
partner=journal.bank_account_id.partner_id.display_name,
),
journal.bank_account_id._get_records_action(),
_("Go to bank account"),
)
if journal_errors:
raise RedirectWarning(
_(
"Please fix the following issue(s) on the %(journal)s journal:\n%(errors)s",
journal=journal.display_name,
errors="\n".join(journal_errors) % {"journal": journal.display_name},
),
journal._get_records_action(),
_("Go to journal"),
)
for payment in self.payment_ids:
if not payment.l10n_ca_cpa005_transaction_code_id:
payments_without_tx_code |= payment
if payments_without_tx_code:
raise RedirectWarning(
_(
"Please set an EFT/CPA transaction code on the following payment(s):\n%s",
"\n".join(f"- {payment.display_name}" for payment in payments_without_tx_code),
),
payments_without_tx_code._get_records_action(name=_("Payments missing EFT/CPA transaction codes")),
_("Go to payments"),
)
def _l10n_ca_cpa005_get_currency(self):
batch_currency = self.payment_ids.mapped("currency_id")
if len(batch_currency) != 1:
raise ValidationError(
_(
"A Canadian EFT file can not contain multiple currencies (%s).",
", ".join(batch_currency.mapped("display_name")),
)
)
if batch_currency not in self.env.ref("base.CAD") | self.env.ref("base.USD"):
raise ValidationError(
_(
"A Canadian EFT file can not contain %s. It can contain either exclusively payments "
"in Canadian dollars or exclusively payments in United States dollars.",
batch_currency.display_name,
)
)
return batch_currency
def _l10n_ca_cpa005_get_total_cents(self, payments):
return sum(round(payment.amount * 100) for payment in payments)
def _l10n_ca_cpa005_generate_header(self, currency, file_creation_nr):
return remove_accents(
"A" # 01 1 1 "A" Logical Record Type ID
"000000001" # 02 2-10 9 "000000001" Logical Record Count
f"{self.journal_id.l10n_ca_cpa005_originator_id:10.10}" # 03 11-20 10 Alphanumeric Originator's ID
f"{file_creation_nr}" # 04 21-24 4 Numeric File Creation No.
f"0{self.date.strftime('%y%j')}" # 05 25-30 6 Numeric Creation Date
f"{self.journal_id.l10n_ca_cpa005_destination_data_center:0>5.5}" # 06 31-35 5 Numeric Destination Data Centre
f"{' ':20}" # 07 36-55 20 Alphanumeric Reserved Customer-Direct Clearer Communication area
f"{currency.name:3.3}" # 08 56-58 3 Alphanumeric Currency Code Identifier
f"{' ':1406}" # 09 59-1464 1406 Alphanumeric Filler
)
def _l10n_ca_cpa005_outgoing_payment(self, file_creation_nr, payment, logical_record_count):
journal = self.journal_id
return remove_accents(
"C" # 01 1 1 "C" Logical Record Type ID
f"{logical_record_count:09d}" # 02 2-10 9 Numeric Logical Record Count
f"{journal.l10n_ca_cpa005_originator_id:10.10}{file_creation_nr}" # 03 11-24 14 Alphanumeric Origination Control Data (originator ID + Numeric File Creation No.)
f"{payment.l10n_ca_cpa005_transaction_code_id.code:3.3}" # 04 25-27 3 Numeric Transaction Type
f"{self._l10n_ca_cpa005_get_total_cents(payment):010d}" # 05 28-37 10 Numeric Amount
f"0{payment.date.strftime('%y%j')}" # 06 38-43 6 Numeric Date Funds to be Available
f"{payment.partner_bank_id.l10n_ca_financial_institution_number:0>9}" # 07 44-52 9 Numeric Institutional Identification No.
f"{payment.partner_bank_id.acc_number:12.12}" # 08 53-64 12 Alphanumeric Payee Account No.
f"{payment.id:022d}" # 09 65-86 22 Numeric Item Trace No.
f"{0:03d}" # 10 87-89 3 Numeric Stored Transaction Type (RBC says to zero-fill)
f"{journal.company_id.l10n_ca_cpa005_short_name:15.15}" # 11 90-104 15 Alphanumeric Originator's Short Name
f"{payment.partner_id.name:30.30}" # 12 105-134 30 Alphanumeric Payee Name
f"{journal.company_id.name:30.30}" # 13 135-164 30 Alphanumeric Originator's Long Name
f"{journal.l10n_ca_cpa005_originator_id:10.10}" # 14 165-174 10 Alphanumeric Originating Direct Clearer's User's ID
f"{(payment.payment_reference or ''):19.19}" # 15 175-193 19 Alphanumeric Originator's Cross Reference No.
f"{journal.bank_account_id.l10n_ca_financial_institution_number:0>9}" # 16 194-202 9 Numeric Institutional ID Number for Returns (RBC says to zero-fill)
f"{journal.bank_account_id.acc_number:12.12}" # 17 203-214 12 Alphanumeric Account No. for Returns (RBC says to zero-fill)
f"{' ':15}" # 18 215-229 15 Alphanumeric Originator's Sundry Information (optional)
f"{' ':22}" # 19 230-251 22 Alphanumeric Filler
f"{' ':2}" # 20 252-253 2 Alphanumeric Originator-Direct Clearer Settlement code (RBC says to zero-fill)
f"{0:011d}" # 21 254-264 11 Numeric Invalid Data Element I.D.
f"{' ':1200}" # padding for segments 2-6, in practice only one payment is provided per record
)
def _l10n_ca_cpa005_generate_footer(self, file_creation_nr, logical_record_count):
journal = self.journal_id
payments = self.payment_ids
return remove_accents(
"Z" # 01 1 1 "Z" Logical Record Type ID
f"{logical_record_count:09d}" # 02 2-10 9 Numeric Logical Record Count
f"{journal.l10n_ca_cpa005_originator_id:10.10}{file_creation_nr}" # 03 11-24 14 Alphanumeric Origination Control Data (originator ID + Numeric File Creation No.)
f"{0:014d}" # 04 25-38 14 Numeric Total Value of Debit Transactions "D" and "J"
f"{0:08d}" # 05 39-46 8 Numeric Total Number of Debit Transactions "D" and "J"
f"{self._l10n_ca_cpa005_get_total_cents(payments):014d}" # 06 47-60 14 Numeric Total Value of Credit Transactions "C" and "I"
f"{len(payments):08d}" # 07 61-68 8 Numeric Total Number of Credit Transactions "C" and "I"
f"{0:014d}" # 08 69-82 14 Numeric Total Value of Error Corrections "E"
f"{0:08d}" # 09 83-90 8 Numeric Total Number of Error Corrections "E"
f"{0:014d}" # 10 91-104 14 Numeric Total Value of Error Corrections "F"
f"{0:08d}" # 11 105-112 8 Numeric Total Number of Error Corrections "F"
f"{' ':1352}" # 12 113-1464 1352 Alphanumeric Filler
)
def _generate_cpa005_file(self):
records = []
self._l10n_ca_cpa005_pre_validate()
currency = self._l10n_ca_cpa005_get_currency()
file_creation_nr = self.l10n_ca_cpa005_file_creation_number
if not file_creation_nr:
file_creation_nr = self.journal_id._l10n_ca_cpa005_next_file_creation_nr()
self.l10n_ca_cpa005_file_creation_number = file_creation_nr
records.append(self._l10n_ca_cpa005_generate_header(currency, file_creation_nr))
for payment in self.payment_ids.sorted():
records.append(self._l10n_ca_cpa005_outgoing_payment(file_creation_nr, payment, len(records) + 1))
records.append(self._l10n_ca_cpa005_generate_footer(file_creation_nr, len(records) + 1))
return "\r\n".join(records)
def _get_methods_generating_files(self):
res = super()._get_methods_generating_files()
res.append("cpa005")
return res
def _generate_export_file(self):
if self.payment_method_code == "cpa005":
data = self._generate_cpa005_file()
date = fields.Datetime.today().strftime("%Y-%m-%d") # CA date format
return {
"file": base64.encodebytes(data.encode()),
"filename": "CPA005-%s-%s.txt" % (self.journal_id.code, date),
}
else:
return super()._generate_export_file()