343 lines
17 KiB
Python
343 lines
17 KiB
Python
# Part of Odoo. See LICENSE file for full copyright and licensing details.
|
|
|
|
import ast
|
|
import base64
|
|
import re
|
|
from datetime import datetime, timedelta
|
|
|
|
from odoo import models, fields, api, _
|
|
from odoo.exceptions import RedirectWarning, ValidationError, UserError
|
|
from odoo.tools import format_date
|
|
|
|
from odoo.addons.account_sepa_direct_debit.models.sdd_mandate import SDD_MIN_PRENOT_PERIOD, SDD_FIRST_MIN_PRENOT_PERIOD
|
|
|
|
|
|
class AccountBatchPayment(models.Model):
|
|
_inherit = 'account.batch.payment'
|
|
|
|
sdd_required_collection_date = fields.Date(
|
|
string='Required Collection Date',
|
|
compute='_compute_sdd_required_collection_date', store=True,
|
|
help="Date when the company expects to receive the payments of this batch. "
|
|
"It can't be inferior to the sending day + the minimum pre-notification period.",
|
|
)
|
|
sdd_min_required_collection_date = fields.Date(compute='_compute_sdd_min_required_collection_date', export_string_translation=False)
|
|
sdd_first_time_payment_ids = fields.One2many('account.payment', compute='_compute_sdd_first_time_payment_ids', export_string_translation=False)
|
|
sdd_batch_booking = fields.Boolean(
|
|
string="SDD Batch Booking",
|
|
default=False,
|
|
help="Ask the bank to group the bank transactions corresponding to these payments into a single transaction.",
|
|
)
|
|
sdd_scheme = fields.Selection(string="SDD Scheme", selection=[('CORE', 'CORE'), ('B2B', 'B2B')],
|
|
help='The B2B scheme is an optional scheme,\noffered exclusively to business payers.\nSome banks/businesses might not accept B2B SDD.',
|
|
compute='_compute_sdd_scheme', store=True, readonly=False)
|
|
|
|
@api.depends('sdd_scheme')
|
|
def _compute_payment_ids_domain(self):
|
|
super()._compute_payment_ids_domain()
|
|
for batch in self:
|
|
domain = ast.literal_eval(batch.payment_ids_domain)
|
|
domain.extend([
|
|
'|',
|
|
('payment_method_id.code', 'not in', ('sdd', 'sepa_direct_debit')),
|
|
('sdd_mandate_id.sdd_scheme', '=', batch.sdd_scheme),
|
|
])
|
|
batch.payment_ids_domain = str(domain)
|
|
|
|
@api.depends('payment_ids')
|
|
def _compute_sdd_required_collection_date(self):
|
|
"""
|
|
The regulation requires that the payer's bank must receive the request for a first direct debit collection
|
|
the latest 5 business days prior to the due date. For subsequent direct debit collections,
|
|
the payer's bank must receive such a request the latest 2 business days prior to the due date.
|
|
"""
|
|
sepa_codes = set(self.env['account.payment.method']._get_sdd_payment_method_code())
|
|
for batch in self.filtered(lambda batch: batch.payment_method_code in sepa_codes):
|
|
if batch.sdd_required_collection_date: # Do not override when there is a time
|
|
batch.sdd_required_collection_date = batch.sdd_required_collection_date
|
|
continue
|
|
|
|
minimum_offset = SDD_FIRST_MIN_PRENOT_PERIOD
|
|
mandates = batch.payment_ids.sdd_mandate_id
|
|
all_used_mandates = batch._get_all_used_mandates()
|
|
if all(all_used_mandates.get(mandate, 0) > 0 for mandate in mandates):
|
|
# The minimum delay is 5 days in all cases, except when all the mandates involved were already used once
|
|
# before, then it can be 2 days.
|
|
minimum_offset = SDD_MIN_PRENOT_PERIOD
|
|
|
|
batch.sdd_required_collection_date = fields.Date.context_today(batch) + timedelta(
|
|
max([minimum_offset, *mandates.mapped('pre_notification_period')])
|
|
)
|
|
|
|
@api.depends('payment_ids')
|
|
def _compute_sdd_min_required_collection_date(self):
|
|
"""Compute the minimum required collection date according to SEPA if there are unused mandates."""
|
|
sepa_codes = set(self.env['account.payment.method']._get_sdd_payment_method_code())
|
|
for batch in self:
|
|
if batch.payment_method_code not in sepa_codes:
|
|
batch.sdd_min_required_collection_date = False
|
|
continue
|
|
|
|
mandates = batch.payment_ids.sdd_mandate_id
|
|
all_used_mandates = batch._get_all_used_mandates()
|
|
if all(all_used_mandates.get(mandate, 0) > 0 for mandate in mandates):
|
|
# The minimum delay is 5 days in all cases, except when all the mandates involved were already used once
|
|
# before, then it can be 2 days.
|
|
batch.sdd_min_required_collection_date = False
|
|
else:
|
|
batch.sdd_min_required_collection_date = fields.Date.context_today(batch) + timedelta(
|
|
SDD_FIRST_MIN_PRENOT_PERIOD
|
|
)
|
|
|
|
@api.depends('payment_ids')
|
|
def _compute_sdd_first_time_payment_ids(self):
|
|
"""Compute the payments in the batch having mandates that were never used before."""
|
|
sepa_codes = set(self.env['account.payment.method']._get_sdd_payment_method_code())
|
|
for batch in self:
|
|
if batch.payment_method_code not in sepa_codes:
|
|
batch.sdd_first_time_payment_ids = False
|
|
continue
|
|
|
|
all_used_mandates = batch._get_all_used_mandates()
|
|
batch.sdd_first_time_payment_ids = batch.payment_ids.filtered(
|
|
lambda p: all_used_mandates.get(p.sdd_mandate_id, 0) == 0
|
|
)
|
|
|
|
@api.depends('payment_method_id')
|
|
def _compute_sdd_scheme(self):
|
|
sdd_payment_codes = self.payment_method_id._get_sdd_payment_method_code()
|
|
for batch in self:
|
|
if batch.payment_method_id.code not in sdd_payment_codes:
|
|
batch.sdd_scheme = False
|
|
else:
|
|
if batch.sdd_scheme:
|
|
batch.sdd_scheme = batch.sdd_scheme
|
|
else:
|
|
batch.sdd_scheme = batch.payment_ids and batch.payment_ids[0].sdd_mandate_scheme or 'CORE'
|
|
|
|
def _get_all_used_mandates(self):
|
|
"""Retrieve a dict, mapping all used mandates in this batch payment to the number of uses.
|
|
|
|
:return: Used mandates mapped to the number of uses.
|
|
:rtype: dict[:class:`odoo.addons.account_sepa_direct_debit.models.SDDMandate`, int]
|
|
"""
|
|
self.ensure_one()
|
|
mandates = self.payment_ids.sdd_mandate_id
|
|
return dict(self.env['account.payment']._read_group([
|
|
('sdd_mandate_id', 'in', mandates.ids),
|
|
('is_matched', '=', True),
|
|
],
|
|
groupby=['sdd_mandate_id'],
|
|
aggregates=['__count'],
|
|
))
|
|
|
|
def _get_methods_generating_files(self):
|
|
rslt = super()._get_methods_generating_files()
|
|
rslt += self.payment_method_id._get_sdd_payment_method_code()
|
|
return rslt
|
|
|
|
@api.constrains('sdd_required_collection_date', 'payment_method_id')
|
|
def _check_minimal_collection_date(self):
|
|
sepa_codes = self.env['account.payment.method']._get_sdd_payment_method_code()
|
|
|
|
sepa_batch_payment = self.filtered(lambda p: p.payment_method_code in sepa_codes)
|
|
if not sepa_batch_payment:
|
|
return
|
|
|
|
for batch in sepa_batch_payment:
|
|
minimum_date = fields.Date.context_today(batch) + timedelta(days=SDD_MIN_PRENOT_PERIOD)
|
|
if batch.payment_method_code in sepa_codes and batch.sdd_required_collection_date < minimum_date:
|
|
raise ValidationError(_(
|
|
"The bank needs to be informed at least %(min_days_new)s days in advance for collections related "
|
|
"to a new mandate and %(min_days)s days in advance when the mandate is already known by them.\n"
|
|
"The minimum collection date must be %(date)s.",
|
|
min_days_new=SDD_FIRST_MIN_PRENOT_PERIOD,
|
|
min_days=SDD_MIN_PRENOT_PERIOD,
|
|
date=format_date(self.env, minimum_date),
|
|
))
|
|
|
|
@api.constrains('batch_type', 'journal_id', 'payment_ids', 'payment_method_id')
|
|
def _check_payments_constrains(self):
|
|
super(AccountBatchPayment, self)._check_payments_constrains()
|
|
for record in self.filtered(lambda r: r.payment_method_code in r.payment_method_id._get_sdd_payment_method_code()):
|
|
all_sdd_schemes = set(record.payment_ids.mapped('sdd_mandate_id.sdd_scheme'))
|
|
if len(all_sdd_schemes) > 1:
|
|
raise ValidationError(_("All the payments in the batch must have the same SDD scheme."))
|
|
|
|
def validate_batch(self):
|
|
self.ensure_one()
|
|
if self.payment_method_code in self.payment_method_id._get_sdd_payment_method_code():
|
|
today = fields.Date.context_today(self)
|
|
company = self.journal_id.company_id
|
|
|
|
if not company.sdd_creditor_identifier:
|
|
action = self.env.ref('account.action_account_config')
|
|
raise RedirectWarning(_(
|
|
"Your company must have a creditor identifier in order to issue SEPA Direct Debit payments requests. "
|
|
"It can be defined in accounting module's settings."
|
|
),
|
|
action=action.id,
|
|
button_text=_("Go to settings"),
|
|
)
|
|
|
|
payments_without_mandate = self.payment_ids.filtered(lambda x: not x.sdd_mandate_id)
|
|
if payments_without_mandate:
|
|
raise RedirectWarning(
|
|
_("Some payments are not linked to any mandate."),
|
|
action={
|
|
'name': _("Payments without mandate"),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'account.payment',
|
|
'views': [(self.env.ref('account.view_account_payment_tree').id, 'list')],
|
|
'domain': [('id', 'in', payments_without_mandate.ids)]
|
|
},
|
|
button_text=_("Go to payments"),
|
|
)
|
|
|
|
invalid_mandates = self.payment_ids.sdd_mandate_id._update_and_partition_state_by_validity()['invalid']
|
|
if invalid_mandates:
|
|
raise RedirectWarning(
|
|
_("Some payments are linked to an inactive mandate."),
|
|
action={
|
|
'name': _("Problematic mandates"),
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'sdd.mandate',
|
|
'views': [(self.env.ref('account_sepa_direct_debit.account_sepa_direct_debit_mandate_tree').id, 'list')],
|
|
'domain': [('id', 'in', invalid_mandates.ids)],
|
|
},
|
|
button_text=_("Go to mandates"),
|
|
)
|
|
|
|
# Check that the pre-notification delay is good
|
|
collection_date = self.sdd_required_collection_date
|
|
min_collection_date = today + timedelta(days=SDD_MIN_PRENOT_PERIOD)
|
|
if collection_date < min_collection_date:
|
|
raise UserError(_(
|
|
"You cannot generate a SEPA Direct Debit file with a required collection date inferior to the "
|
|
"sending day + the minimum pre-notification period of %(prenot_days)s days.\n"
|
|
"The minimum required date should be %(minimum_date)s.",
|
|
prenot_days=SDD_MIN_PRENOT_PERIOD,
|
|
minimum_date=format_date(self.env, min_collection_date),
|
|
))
|
|
|
|
if self.journal_id.bank_account_id.acc_type != 'iban':
|
|
raise RedirectWarning(_(
|
|
"Only IBAN account numbers can receive SEPA Direct Debit payments. "
|
|
"Please select a journal associated to one or add an IBAN bank account to the current journal"
|
|
),
|
|
action={
|
|
'name': self.journal_id.name,
|
|
'type': 'ir.actions.act_window',
|
|
'res_model': 'account.journal',
|
|
'res_id': self.journal_id.id,
|
|
'views': [(self.env.ref('account.view_account_journal_form').id, 'form')],
|
|
},
|
|
button_text=_("Go to journal"),
|
|
)
|
|
|
|
return super().validate_batch()
|
|
|
|
def _check_and_post_draft_payments(self, draft_payments):
|
|
rslt = []
|
|
if self.payment_method_code in self.payment_method_id._get_sdd_payment_method_code():
|
|
|
|
drafts_without_mandate = draft_payments.filtered(lambda x: not x.get_usable_mandate())
|
|
if drafts_without_mandate:
|
|
rslt = [{'title': _("Some draft payments could not be posted because of the lack of any active mandate."),
|
|
'records': drafts_without_mandate,
|
|
'help': _("To solve that, you should create a mandate for each of the involved customers, valid at the moment of the payment date.")
|
|
}]
|
|
draft_payments -= drafts_without_mandate
|
|
|
|
return rslt + super()._check_and_post_draft_payments(draft_payments)
|
|
|
|
def _generate_export_file(self):
|
|
if self.payment_method_code in self.payment_method_id._get_sdd_payment_method_code():
|
|
# Constrains on models ensure all the payments can generate SDD data before
|
|
# calling this method, so we make no further check of their content here
|
|
company = self.env.company
|
|
return {
|
|
'filename': 'PAIN008' + datetime.now().strftime('%Y%m%d%H%M%S') + '.xml',
|
|
'file': base64.encodebytes(self.payment_ids.generate_xml(company, self.sdd_required_collection_date, self.sdd_batch_booking)),
|
|
}
|
|
|
|
return super()._generate_export_file()
|
|
|
|
def _send_after_validation(self):
|
|
""" Notify the customer that a debit has been made from his account.
|
|
|
|
This is required as per the SEPA Direct Debit rulebook.
|
|
The notice must include:
|
|
- the last 4 digits of the debtor's bank account
|
|
- the mandate reference
|
|
- the amount to be debited
|
|
- your SEPA creditor identifier
|
|
- your contact information
|
|
Notifications should be sent at least 14 calendar days before the payment is created unless
|
|
specified otherwise (We changed that default in Odoo by always specifying the period and defaulting it to 2).
|
|
|
|
:param recordset token: The token linked to the mandate from which the debit has been made,
|
|
as a `payment.token` record
|
|
:return: None
|
|
"""
|
|
res = super()._send_after_validation()
|
|
if self.env['ir.config_parameter'].sudo().get_param('account_sepa_direct_debit.disable_sdd_pre_notification'):
|
|
return res
|
|
|
|
template = self.env.ref('account_sepa_direct_debit.email_template_sdd_pre_notification')
|
|
sdd_codes = set(self.env['account.payment.method']._get_sdd_payment_method_code())
|
|
payments = self.payment_ids.filtered(lambda payment: payment.payment_method_code in sdd_codes and payment.sdd_mandate_id)
|
|
emails_from = template._generate_template(
|
|
payments.ids,
|
|
{'email_from'},
|
|
)
|
|
|
|
authorship_by_email = {}
|
|
for payment in payments:
|
|
mandate = payment.sdd_mandate_id
|
|
sanitized_acc_number = mandate.partner_bank_id.sanitized_acc_number
|
|
anonymized_bank_account_number = f"{re.sub(r'.', '*', sanitized_acc_number[:-4])}{sanitized_acc_number[-4:]}"
|
|
|
|
ctx = {
|
|
'iban_last_4': sanitized_acc_number[-4:],
|
|
'mandate_ref': mandate.name,
|
|
'collection_date': payment.batch_payment_id.sdd_required_collection_date,
|
|
'amount': payment.amount,
|
|
'creditor_iban': anonymized_bank_account_number,
|
|
}
|
|
email_from = emails_from[payment.id]['email_from']
|
|
|
|
if email_from not in authorship_by_email:
|
|
authorship_by_email[email_from] = (
|
|
self.env['mail.thread']._message_compute_author(
|
|
email_from=email_from,
|
|
)
|
|
)
|
|
|
|
author_id, email_from = authorship_by_email[email_from]
|
|
payment.with_context(ctx).message_post_with_source(
|
|
source_ref=template,
|
|
subtype_xmlid='mail.mt_note',
|
|
author_id=author_id,
|
|
email_from=email_from,
|
|
)
|
|
return res
|
|
|
|
def check_payments_for_errors(self):
|
|
rslt = super().check_payments_for_errors()
|
|
|
|
if self.payment_method_code not in self.payment_method_id._get_sdd_payment_method_code():
|
|
return rslt
|
|
|
|
if len(self.payment_ids):
|
|
sdd_scheme = self.payment_ids[0].sdd_mandate_id.sdd_scheme
|
|
dif_scheme_payements = self.payment_ids.filtered(lambda x: x.sdd_mandate_id.sdd_scheme != sdd_scheme)
|
|
if dif_scheme_payements:
|
|
rslt.append({
|
|
'title': _("All the payments in the batch must have the same SDD scheme."),
|
|
'records': dif_scheme_payements,
|
|
'help': _("SDD scheme is set on the customer mandate.")
|
|
})
|
|
|
|
return rslt
|