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

293 lines
15 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
from odoo import models, _
from odoo.exceptions import UserError
from odoo.tools.misc import remove_accents
import itertools
import re
PAYMENT_CODE_MAPPINGS = {
'dd_sub_init': '01',
'dd_regular': '17',
'dd_sub_rep': '18',
'dd_sub_fin': '19',
}
def format_communication(communication):
""" Returns a formatted version of the communication given in parameter,
so that it contains only uppercase characters, numeric characters,
spaces, and the following punctuation characters: '/' '-' '.' '&'
(these are the Bacs compliance criteria)
"""
communication = remove_accents(communication)
formatted_communication = ''
for char in communication:
if char.isalnum() or char in ' /-.,&':
formatted_communication += char.upper()
elif char == '_':
formatted_communication += ' '
return formatted_communication
class AccountJournal(models.Model):
_inherit = "account.journal"
def _default_outbound_payment_methods(self):
res = super()._default_outbound_payment_methods()
if self._is_payment_method_available('bacs_dc'):
res |= self.env.ref('account_bacs.payment_method_bacs_dc')
return res
def _default_inbound_payment_methods(self):
res = super()._default_inbound_payment_methods()
if self._is_payment_method_available('bacs_dd'):
res |= self.env.ref('account_bacs.payment_method_bacs_dd')
return res
def create_bacs_file(self, payments, payment_method_code, serial_number, bacs_multi_mode, processing_date, expiry_date, batch_ref, creation_date):
""" Create a Bacs file for the given payments, and returns it as a string.
The file is created according to the Bacs format specification.
"""
company = self.company_id
bacs_sun = company.bacs_sun
bacs_file = ''
bacs_file += self._create_header(serial_number, bacs_sun, bacs_multi_mode, processing_date, expiry_date, creation_date)
company_bank = self.bank_account_id
# format company and batch details
company_account_number = company_bank.sanitized_acc_number[8:]
company_name = format_communication(self.company_id.name)[:18].ljust(18)
ref = format_communication(batch_ref)[:18].ljust(18)
if re.match(r'^BATCH/(IN|OUT)/\d{4}/\d+$', batch_ref):
batch_ref_split = batch_ref.split('/', 1)
if len(batch_ref_split) > 1:
ref = format_communication(batch_ref_split[1])[:18].ljust(18)
if payment_method_code == 'bacs_dc':
payments_details = self._create_dc_payments(payments, bacs_multi_mode, ref, company_name, company_account_number)
else:
payments_details = self._create_dd_payments(payments, bacs_multi_mode, ref, company_name, company_account_number)
bacs_file += payments_details['payments']
bacs_file += self._create_footer(payments_details, serial_number, bacs_sun, creation_date, expiry_date)
return bacs_file
def _create_header(self, serial_number, bacs_sun, bacs_multi_mode, processing_date, expiry_date, creation_date):
""" Create the header of the Bacs file, according to the Bacs format specification.
"""
formatted_processing_date = processing_date.strftime(" %y%j")
formatted_creation_date = creation_date.strftime(" %y%j")
formatted_expiration_date = expiry_date.strftime(" %y%j")
uhl_date = ' ' * 6 if bacs_multi_mode else formatted_processing_date
uhl_work_code = '4 MULTI ' if bacs_multi_mode else '1 DAILY '
sun_or_whitespaces = bacs_sun if bacs_sun != 'HSBC' else ' ' * 6
header = ''
header += f"VOL1{serial_number} {' ' * 20}{'HSBC ' if bacs_sun == 'HSBC' else ' ' * 6}{' ' * 4}{sun_or_whitespaces}{' ' * 4}{' ' * 28}1\n"
header += f"HDR1A{sun_or_whitespaces}S 1{sun_or_whitespaces}{serial_number}00010001{' ' * 6}{formatted_creation_date}{formatted_expiration_date}{' ' * 27}\n"
header += f"HDR2F0200000100{' ' * 65}\n"
header += f"UHL1{uhl_date}{bacs_sun if bacs_sun != 'HSBC' else '999999'} 00{'0' * 6}{uhl_work_code}{' ' * 43}\n"
return header
def _create_dc_payments(self, payments, bacs_multi_mode, batch_ref, company_name, company_account_number):
""" This method generates transaction and contra lines for BACS (Bankers' Automated Clearing Services) Direct Credit payments.
Parameters:
- payments (list): A list of dictionaries representing payment data.
- bacs_multi_mode (bool): If True, the function processes payments in 'multi' mode. If False, it processes payments in 'single' mode.
- batch_ref (str): The reference of the batch.
- company_name (str): The name of the company.
- company_account_number (str): The account number of the company.
In 'single' mode, the function processes all payments as a single batch.
In 'multi' mode, it groups payments by date and for each date, it generates transactions and a contra record.
Each payment dictionary represents a transaction. The method iterates over payments, validates the necessary fields,
and generates a formatted string line for each payment according to BACS standards.
After processing all payments of the same date in 'multi' mode (or all payments in 'single' mode),
it creates a contra record, which is a summary line of the processed transactions.
Returns:
dict: A dictionary containing the following keys:
- 'payments' (str): A string of formatted lines for BACS file, each line represents a transaction or a contra record.
- 'debit_total' (int): The total amount of debit transactions in pence.
- 'credit_total' (int): The total amount of credit transactions in pence.
- 'debit_count' (int): The total number of debit transactions.
- 'credit_count' (int): The total number of credit transactions.
"""
debit_total, credit_total = 0, 0
debit_count, credit_count = 0, 0
multi_contra_totals = {}
single_contra_total = 0
transaction_lines = []
for payment_date, payments_in_date in itertools.groupby(payments, key=lambda k: k['payment_date']):
payments_in_date = list(payments_in_date)
for payment in payments_in_date:
partner_name = format_communication(payment['partner_name'])[:18].ljust(18)
partner_bank_iban = payment['partner_bank_iban']
partner_sort_code = partner_bank_iban[8:14]
partner_account_number = partner_bank_iban[14:]
payment_reference = format_communication(payment['ref'])[:18].ljust(18)
amount = payment['amount']
amount_in_pence = int(amount * 100)
transaction_line = f"{partner_sort_code}{partner_account_number}099{company_account_number} {amount_in_pence:011}{company_name}{payment_reference}{partner_name}"
credit_total += amount_in_pence
credit_count += 1
if bacs_multi_mode:
transaction_line += payment_date.strftime(" %y%j") + '\n'
multi_contra_totals[payment_date] = multi_contra_totals.get(payment_date, 0) + amount_in_pence
else:
transaction_line += '\n'
single_contra_total += amount_in_pence
transaction_lines.append(transaction_line)
if bacs_multi_mode:
contra_total = multi_contra_totals[payment_date]
if contra_total > 99999999999:
raise UserError(_('Contra total for date %s is greater than 999,999,999.99.', payment_date))
contra = f"{company_account_number}017{company_account_number} {contra_total:011}{batch_ref}{'CONTRA'.ljust(18)}{company_name}{payment_date.strftime(' %y%j')}\n"
debit_total += contra_total
debit_count += 1
transaction_lines.append(contra)
if not bacs_multi_mode:
if single_contra_total > 99999999999:
raise UserError(_('Contra total for batch is greater than 999,999,999.99.'))
debit_total += single_contra_total
debit_count += 1
contra = f"{company_account_number}017{company_account_number} {single_contra_total:011}{batch_ref}{'CONTRA'.ljust(18)}{company_name}\n"
transaction_lines.append(contra)
return {
'payments': ''.join(transaction_lines),
'debit_total': debit_total,
'credit_total': credit_total,
'debit_count': debit_count,
'credit_count': credit_count,
}
def _create_dd_payments(self, payments, bacs_multi_mode, batch_ref, company_name, company_account_number):
""" This method generates transaction and contra lines for BACS (Bankers' Automated Clearing Services) Direct Debit payments.
Parameters:
- payments (list): A list of dictionaries representing payment data.
- bacs_multi_mode (bool): If True, the function processes payments in 'multi' mode. If False, it processes payments in 'single' mode.
- batch_ref (str): The reference of the batch.
- company_name (str): The name of the company.
- company_account_number (str): The account number of the company.
In 'single' mode, the function processes all payments as a single batch.
In 'multi' mode, it groups payments by date and for each date, it generates transactions and a contra record.
Each payment dictionary represents a transaction. The method iterates over payments, validates the necessary fields,
and generates a formatted string line for each payment according to BACS standards.
After processing all payments of the same date in 'multi' mode (or all payments in 'single' mode),
it creates a contra record, which is a summary line of the processed transactions.
Returns:
dict: A dictionary containing the following keys:
- 'payments' (str): A string of formatted lines for BACS file, each line represents a transaction or a contra record.
- 'debit_total' (int): The total amount of debit transactions in pence.
- 'credit_total' (int): The total amount of credit transactions in pence.
- 'debit_count' (int): The total number of debit transactions.
- 'credit_count' (int): The total number of credit transactions.
"""
debit_total, credit_total = 0, 0
debit_count, credit_count = 0, 0
multi_contra_totals = {}
single_contra_total = 0
transaction_lines = []
# itereate over payments grouped by date
for payment_date, payments_in_date in itertools.groupby(payments, key=lambda k: k['payment_date']):
payments_in_date = list(payments_in_date)
for payment in payments_in_date:
partner_name = payment['partner_name'][:18].ljust(18)
ddi = self.env['bacs.ddi'].browse(payment['bacs_ddi_id'])
if not ddi:
raise UserError(_("The payment must be linked to a BACS Direct Debit Instruction in order to generate a Direct Debit File."))
if ddi.state == 'revoked':
raise UserError(_("The BACS Direct Debit Instruction associated to the payment has been revoked and cannot be used anymore."))
partner_bank_iban = ddi.partner_bank_id.sanitized_acc_number
partner_sort_code = partner_bank_iban[8:14]
partner_account_number = partner_bank_iban[14:]
partner_name = format_communication(partner_name)[:18].ljust(18)
payment_reference = format_communication(payment['ref'])[:18].ljust(18)
amount = payment['amount']
amount_in_pence = int(amount * 100)
transaction_code = PAYMENT_CODE_MAPPINGS[payment['bacs_payment_type']]
transaction_line = f"{partner_sort_code}{partner_account_number}0{transaction_code}{company_account_number} {amount_in_pence:011}{company_name}{payment_reference}{partner_name}"
debit_total += amount_in_pence
debit_count += 1
if bacs_multi_mode:
transaction_line += payment_date.strftime(" %y%j") + '\n'
multi_contra_totals[payment_date] = multi_contra_totals.get(payment_date, 0) + amount_in_pence
else:
transaction_line += '\n'
single_contra_total += amount_in_pence
transaction_lines.append(transaction_line)
if bacs_multi_mode:
contra_total = multi_contra_totals[payment_date]
if contra_total > 99999999999:
raise UserError(_('Contra total for date %s is greater than 999,999,999.99.', payment_date))
contra = f"{company_account_number}099{company_account_number} {contra_total:011}{batch_ref}{'CONTRA'.ljust(18)}{company_name}{payment_date.strftime(' %y%j')}\n"
credit_total += contra_total
credit_count += 1
transaction_lines.append(contra)
if not bacs_multi_mode:
if single_contra_total > 99999999999:
raise UserError(_('Contra total for batch is greater than 999,999,999.99.'))
credit_total += single_contra_total
credit_count += 1
contra = f"{company_account_number}099{company_account_number} {single_contra_total:011}{batch_ref}{'CONTRA'.ljust(18)}{company_name}\n"
transaction_lines.append(contra)
return {
'payments': ''.join(transaction_lines),
'debit_total': debit_total,
'credit_total': credit_total,
'debit_count': debit_count,
'credit_count': credit_count,
}
def _create_footer(self, payments_details, serial_number, bacs_sun, creation_date, expiry_date):
""" Create the footer of the Bacs file, according to the Bacs format specification.
"""
formatted_creation_date = creation_date.strftime(" %y%j")
formatted_expiration_date = expiry_date.strftime(" %y%j")
debit_total = payments_details['debit_total']
if debit_total > 9999999999999:
raise UserError(_('Debit total for batch is greater than 99,999,999,999.99.'))
credit_total = payments_details['credit_total']
if credit_total > 9999999999999:
raise UserError(_('Credit total for batch is greater than 99,999,999,999.99.'))
debit_count = payments_details['debit_count']
credit_count = payments_details['credit_count']
sun_or_whitespaces = bacs_sun if bacs_sun != 'HSBC' else ' ' * 6
footer = ''
footer += f"EOF1A{sun_or_whitespaces}S 1{sun_or_whitespaces}{serial_number}00010001{' ' * 6}{formatted_creation_date}{formatted_expiration_date}{' ' * 27}\n"
footer += f"EOF2F0200000100{' ' * 65}\n"
footer += f"UTL1{debit_total:013}{credit_total:013}{debit_count:07}{credit_count:07}{' ' * 36}\n"
return footer