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

384 lines
18 KiB
Python

# -*- coding: utf-8 -*-
# Part of Odoo. See LICENSE file for full copyright and licensing details.
import base64
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError, UserError
class AccountBatchPayment(models.Model):
_name = "account.batch.payment"
_description = "Batch Payment"
_order = "date desc, id desc"
_inherit = ["mail.thread", "mail.activity.mixin"]
name = fields.Char(required=True, copy=False, string='Reference')
date = fields.Date(required=True, copy=False, default=fields.Date.context_today, tracking=True)
state = fields.Selection([
('draft', 'New'),
('sent', 'Sent'),
('reconciled', 'Reconciled'),
], store=True, compute='_compute_state', default='draft', tracking=True)
journal_id = fields.Many2one(
'account.journal',
string='Bank',
check_company=True,
domain=[('type', '=', 'bank')],
tracking=True,
)
payment_ids = fields.One2many('account.payment', 'batch_payment_id', string="Payments", required=True)
currency_id = fields.Many2one('res.currency', compute='_compute_currency', store=True, readonly=True)
company_currency_id = fields.Many2one(
string="Company Currency",
related='journal_id.company_id.currency_id',
store=True,
)
amount_residual = fields.Monetary(
currency_field='company_currency_id',
compute='_compute_from_payment_ids',
store=True,
)
amount_residual_currency = fields.Monetary(
currency_field='currency_id',
compute='_compute_from_payment_ids',
store=True,
)
amount = fields.Monetary(
currency_field='currency_id',
compute='_compute_from_payment_ids',
store=True,
)
batch_type = fields.Selection(selection=[('inbound', 'Inbound'), ('outbound', 'Outbound')], required=True, default='inbound', tracking=True)
payment_method_id = fields.Many2one(
comodel_name='account.payment.method',
string='Payment Method', store=True, readonly=False,
compute='_compute_payment_method_id',
domain="[('id', 'in', available_payment_method_ids)]",
help="The payment method used by the payments in this batch.", tracking=True)
available_payment_method_ids = fields.Many2many(
comodel_name='account.payment.method',
compute='_compute_available_payment_method_ids')
payment_method_code = fields.Char(related='payment_method_id.code', tracking=True)
export_file_create_date = fields.Date(string='Generation Date', default=fields.Date.today, readonly=True, help="Creation date of the related export file.", copy=False)
export_file = fields.Binary(string='File', readonly=True, help="Export file related to this batch", copy=False)
export_filename = fields.Char(string='File Name', help="Name of the export file generated for this batch", store=True, copy=False)
file_generation_enabled = fields.Boolean(help="Whether or not this batch payment should display the 'Generate File' button instead of 'Print' in form view.", compute='_compute_file_generation_enabled')
@api.depends('batch_type', 'journal_id', 'payment_ids')
def _compute_payment_method_id(self):
''' Compute the 'payment_method_id' field.
This field is not computed in '_compute_available_payment_method_ids' because it's a stored editable one.
'''
for batch in self:
if batch.payment_ids:
batch.payment_method_id = batch.payment_ids.payment_method_line_id[0].payment_method_id
continue
if not batch.journal_id:
batch.available_payment_method_ids = False
batch.payment_method_id = False
continue
available_payment_method_lines = batch.journal_id._get_available_payment_method_lines(batch.batch_type)
batch.available_payment_method_ids = available_payment_method_lines.mapped('payment_method_id')
# Select the first available one by default.
if batch.available_payment_method_ids:
batch.payment_method_id = batch.available_payment_method_ids[0]._origin
else:
batch.payment_method_id = False
@api.depends('batch_type', 'journal_id')
def _compute_available_payment_method_ids(self):
for batch in self:
available_payment_method_lines = batch.journal_id._get_available_payment_method_lines(batch.batch_type)
batch.available_payment_method_ids = available_payment_method_lines.mapped('payment_method_id')
@api.depends('payment_ids.move_id.is_move_sent', 'payment_ids.is_matched')
def _compute_state(self):
for batch in self:
if batch.payment_ids and all(pay.is_matched and pay.is_move_sent for pay in batch.payment_ids):
batch.state = 'reconciled'
elif batch.payment_ids and all(pay.is_move_sent for pay in batch.payment_ids):
batch.state = 'sent'
else:
batch.state = 'draft'
@api.depends('payment_method_id')
def _compute_file_generation_enabled(self):
for record in self:
record.file_generation_enabled = record.payment_method_id.code in record._get_methods_generating_files()
def _get_methods_generating_files(self):
""" Hook for extension. Any payment method whose code stands in the list
returned by this function will see the "print" button disappear on batch
payments form when it gets selected and an 'Export file' appear instead.
"""
return []
@api.depends('journal_id')
def _compute_currency(self):
for batch in self:
batch.currency_id = batch.journal_id.currency_id or batch.company_currency_id or self.env.company.currency_id
@api.depends('currency_id', 'payment_ids.amount', 'payment_ids.is_matched')
def _compute_from_payment_ids(self):
for batch in self:
amount = 0.0
amount_residual = 0.0
amount_residual_currency = 0.0
for payment in batch.payment_ids:
liquidity_lines, _counterpart_lines, _writeoff_lines = payment._seek_for_lines()
for line in liquidity_lines:
amount += line.currency_id._convert(
from_amount=line.amount_currency,
to_currency=batch.currency_id,
company=line.company_id,
date=line.date,
)
amount_residual += line.amount_residual
amount_residual_currency += line.amount_residual_currency
batch.amount_residual = amount_residual
batch.amount = amount
batch.amount_residual_currency = amount_residual_currency
@api.constrains('batch_type', 'journal_id', 'payment_ids')
def _check_payments_constrains(self):
for record in self:
all_companies = set(record.payment_ids.mapped('company_id'))
if len(all_companies) > 1:
raise ValidationError(_("All payments in the batch must belong to the same company."))
all_journals = set(record.payment_ids.mapped('journal_id'))
if len(all_journals) > 1 or (record.payment_ids and record.payment_ids[:1].journal_id != record.journal_id):
raise ValidationError(_("The journal of the batch payment and of the payments it contains must be the same."))
all_types = set(record.payment_ids.mapped('payment_type'))
if all_types and record.batch_type not in all_types:
raise ValidationError(_("The batch must have the same type as the payments it contains."))
all_payment_methods = record.payment_ids.payment_method_id
if len(all_payment_methods) > 1:
raise ValidationError(_("All payments in the batch must share the same payment method."))
if all_payment_methods and record.payment_method_id not in all_payment_methods:
raise ValidationError(_("The batch must have the same payment method as the payments it contains."))
payment_null = record.payment_ids.filtered(lambda p: p.amount == 0)
if payment_null:
raise ValidationError(_('You cannot add payments with zero amount in a Batch Payment.'))
non_posted = record.payment_ids.filtered(lambda p: p.state != 'posted')
if non_posted:
raise ValidationError(_('You cannot add payments that are not posted.'))
@api.model_create_multi
def create(self, vals_list):
today = fields.Date.context_today(self)
all_payment_ids = []
for vals in vals_list:
vals['name'] = self._get_batch_name(
vals.get('batch_type'),
vals.get('date', today),
vals)
if 'payment_ids' in vals:
payments = self.new({'payment_ids': vals['payment_ids']}).payment_ids
if payments._origin.batch_payment_id:
raise ValidationError(_('You cannot create a batch with payments that are already in another batch.'))
# Collect all payment IDs
all_payment_ids.extend(payments.ids)
if len(all_payment_ids) != len(set(all_payment_ids)):
raise ValidationError(_('You cannot create batches with overlapping payments.'))
return super().create(vals_list)
def write(self, vals):
if 'batch_type' in vals:
vals['name'] = self.with_context(default_journal_id=self.journal_id.id)._get_batch_name(vals['batch_type'], self.date, vals)
if 'payment_ids' in vals:
if len(self) > 1:
raise ValidationError(_('You cannot add the same payment to multiple batches.'))
original_payments = self.new({'payment_ids': vals['payment_ids']}).payment_ids._origin
if original_payments.batch_payment_id - self:
raise ValidationError(_('You cannot create a batch with payments that are already in another batch.'))
rslt = super(AccountBatchPayment, self).write(vals)
return rslt
@api.model
def _get_batch_name(self, batch_type, sequence_date, vals):
if not vals.get('name'):
sequence_code = 'account.inbound.batch.payment'
if batch_type == 'outbound':
sequence_code = 'account.outbound.batch.payment'
return self.env['ir.sequence'].with_context(sequence_date=sequence_date).next_by_code(sequence_code)
return vals['name']
@api.depends('state')
def _compute_display_name(self):
state_values = dict(self._fields['state'].selection)
for batch in self:
batch.display_name = f'{batch.name} ({state_values.get(batch.state)})'
def validate_batch(self):
""" Verifies the content of a batch and proceeds to its sending if possible.
If not, opens a wizard listing the errors and/or warnings encountered.
"""
self.ensure_one()
if not self.payment_ids:
raise UserError(_("Cannot validate an empty batch. Please add some payments to it first."))
errors = not self.export_file and self.check_payments_for_errors() or [] # We don't re-check for errors if we are regenerating the file (we know there aren't any)
warnings = self.check_payments_for_warnings()
if errors or warnings:
return {
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'account.batch.error.wizard',
'target': 'new',
'res_id': self.env['account.batch.error.wizard'].create_from_errors_list(self, errors, warnings).id,
}
return self._send_after_validation()
def validate_batch_button(self):
return self.validate_batch()
def _send_after_validation(self):
""" Sends the payments of a batch (possibly generating an export file)
once the batch has been validated.
"""
self.ensure_one()
if self.payment_ids:
self.payment_ids.mark_as_sent()
if self.file_generation_enabled:
return self.export_batch_payment()
def check_payments_for_warnings(self):
""" Checks the payments of this batch and returns (if relevant) some
warnings about them. These warnings are not to be confused with errors,
they are only messgaes displayed to make sure the user is aware of some
specificities in the payments he's put in the batch. He will be able to
ignore them.
:return: A list of dictionaries, each one corresponding to a distinct
warning and containing the following keys:
- 'title': A short name for the warning (mandatory)
- 'records': The recordset of payments concerned by this warning (mandatory)
- 'help': A help text to give the user further information
on the reason this warning exists (optional)
"""
return []
def check_payments_for_errors(self):
""" Goes through all the payments of the batches contained in this
record set, and returns the ones that would impeach batch validation,
in such a way that the payments impeaching validation for the same reason
are grouped under a common error message. This function is a hook for
extension for modules making a specific use of batch payments, such as SEPA
ones.
:return: A list of dictionaries, each one corresponding to a distinct
error and containing the following keys:
- 'title': A short name for the error (mandatory)
- 'records': The recordset of payments facing this error (mandatory)
- 'help': A help text to give the user further information
on how to solve the error (optional)
"""
self.ensure_one()
#We first try to post all the draft batch payments
rslt = self._check_and_post_draft_payments(self.payment_ids.filtered(lambda x: x.state == 'draft'))
wrong_state_payments = self.payment_ids.filtered(lambda x: x.state != 'posted')
if wrong_state_payments:
rslt.append({
'title': _("Payments must be posted to be added to a batch."),
'records': wrong_state_payments,
'help': _("Set payments state to \"posted\".")
})
if self.batch_type == 'outbound':
not_allowed_payments = self.payment_ids.filtered(lambda x: x.partner_bank_id and not x.partner_bank_id.allow_out_payment)
if not_allowed_payments:
rslt.append({
'code': 'out_payment_not_allowed',
'title': _("Some recipient accounts do not allow out payments."),
'records': not_allowed_payments,
'help': _("Target another recipient account or allow sending money to the current one.")
})
sent_payments = self.payment_ids.filtered(lambda x: x.is_move_sent)
if sent_payments:
rslt.append({
'title': _("Some payments have already been sent."),
'records': sent_payments,
})
if self.batch_type == 'inbound':
pmls = self.journal_id.inbound_payment_method_line_ids
default_payment_account = self.journal_id.company_id.account_journal_payment_debit_account_id
else:
pmls = self.journal_id.outbound_payment_method_line_ids
default_payment_account = self.journal_id.company_id.account_journal_payment_credit_account_id
pmls = pmls.filtered(lambda x: x.payment_method_id == self.payment_method_id)
no_statement_reconciliation = self.journal_id.default_account_id == (pmls.payment_account_id[:1] or default_payment_account)
bank_reconciled_payments = self.payment_ids.filtered(lambda x: x.is_matched)
if bank_reconciled_payments and not no_statement_reconciliation:
rslt.append({
'title': _("Some payments have already been matched with a bank statement."),
'records': bank_reconciled_payments,
})
return rslt
def _check_and_post_draft_payments(self, draft_payments):
""" Tries posting each of the draft payments contained in this batch.
If it fails and raise a UserError, it is catched and the process continues
on the following payments. All the encountered errors are then returned
withing a dictionary, in the same fashion as check_payments_for_errors.
"""
exceptions_mapping = {}
for payment in draft_payments:
try:
payment.action_post()
except UserError as e:
name = e.args[0]
if name in exceptions_mapping:
exceptions_mapping[name] += payment
else:
exceptions_mapping[name] = payment
return [{'title': error, 'records': pmts} for error, pmts in exceptions_mapping.items()]
def export_batch_payment(self):
#export and save the file for each batch payment
self.check_access_rights('write')
self.check_access_rule('write')
for record in self.sudo():
record = record.with_company(record.journal_id.company_id)
export_file_data = record._generate_export_file()
record.export_file = export_file_data['file']
record.export_filename = export_file_data['filename']
record.export_file_create_date = fields.Date.today()
record.message_post(
attachments=[
(record.export_filename, base64.decodebytes(record.export_file)),
]
)
def print_batch_payment(self):
return self.env.ref('account_batch_payment.action_print_batch_payment').report_action(self, config=False)
def _generate_export_file(self):
""" To be overridden by modules adding support for different export format.
This function returns False if no export file could be generated
for this batch. Otherwise, it returns a dictionary containing the following keys:
- file: the content of the generated export file, in base 64.
- filename: the name of the generated file
- warning: (optional) the warning message to display
"""
self.ensure_one()
return False