193 lines
10 KiB
Python
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()
|