Files
Odoo-Modules/Fusion Accounting/models/batch_payment.py
2026-02-22 01:22:18 -05:00

258 lines
9.0 KiB
Python

"""
Fusion Accounting - Batch Payment Processing
Provides the ``fusion.batch.payment`` model which allows grouping
multiple vendor or customer payments into a single batch for
streamlined bank submission and reconciliation.
"""
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
class FusionBatchPayment(models.Model):
"""Groups individual payments into batches for bulk processing.
A batch payment collects payments that share the same journal and
payment method so they can be sent to the bank as a single file
or printed on a single check run.
"""
_name = 'fusion.batch.payment'
_description = 'Batch Payment'
_order = 'date desc, id desc'
_inherit = ['mail.thread', 'mail.activity.mixin']
# ------------------------------------------------------------------
# Fields
# ------------------------------------------------------------------
name = fields.Char(
string='Reference',
required=True,
copy=False,
readonly=True,
default='/',
tracking=True,
help="Unique reference for this batch payment.",
)
journal_id = fields.Many2one(
comodel_name='account.journal',
string='Bank Journal',
required=True,
domain="[('type', '=', 'bank')]",
tracking=True,
help="The bank journal used for all payments in this batch.",
)
payment_method_id = fields.Many2one(
comodel_name='account.payment.method',
string='Payment Method',
required=True,
tracking=True,
help="Payment method shared by every payment in the batch.",
)
payment_ids = fields.Many2many(
comodel_name='account.payment',
relation='fusion_batch_payment_rel',
column1='batch_id',
column2='payment_id',
string='Payments',
copy=False,
help="Individual payments included in this batch.",
)
state = fields.Selection(
selection=[
('draft', 'Draft'),
('sent', 'Sent'),
('reconciled', 'Reconciled'),
],
string='Status',
default='draft',
required=True,
readonly=True,
copy=False,
tracking=True,
help="Draft: batch is being assembled.\n"
"Sent: batch has been transmitted to the bank.\n"
"Reconciled: all payments in the batch are reconciled.",
)
date = fields.Date(
string='Date',
required=True,
default=fields.Date.context_today,
tracking=True,
help="Effective date of the batch payment.",
)
amount_total = fields.Monetary(
string='Total Amount',
compute='_compute_amount_total',
store=True,
currency_field='currency_id',
help="Sum of all payment amounts in this batch.",
)
currency_id = fields.Many2one(
comodel_name='res.currency',
string='Currency',
related='journal_id.currency_id',
readonly=True,
store=True,
help="Currency of the bank journal.",
)
company_id = fields.Many2one(
comodel_name='res.company',
string='Company',
related='journal_id.company_id',
store=True,
readonly=True,
)
payment_count = fields.Integer(
string='Payment Count',
compute='_compute_amount_total',
store=True,
)
# ------------------------------------------------------------------
# Computed fields
# ------------------------------------------------------------------
@api.depends('payment_ids', 'payment_ids.amount')
def _compute_amount_total(self):
"""Compute the total batch amount and payment count."""
for batch in self:
batch.amount_total = sum(batch.payment_ids.mapped('amount'))
batch.payment_count = len(batch.payment_ids)
# ------------------------------------------------------------------
# Constraints
# ------------------------------------------------------------------
@api.constrains('payment_ids')
def _check_payments_journal(self):
"""Ensure every payment belongs to the same journal and uses the
same payment method as the batch."""
for batch in self:
for payment in batch.payment_ids:
if payment.journal_id != batch.journal_id:
raise ValidationError(_(
"Payment '%(payment)s' uses journal '%(pj)s' but "
"the batch requires journal '%(bj)s'.",
payment=payment.display_name,
pj=payment.journal_id.display_name,
bj=batch.journal_id.display_name,
))
if payment.payment_method_id != batch.payment_method_id:
raise ValidationError(_(
"Payment '%(payment)s' uses payment method '%(pm)s' "
"which differs from the batch method '%(bm)s'.",
payment=payment.display_name,
pm=payment.payment_method_id.display_name,
bm=batch.payment_method_id.display_name,
))
# ------------------------------------------------------------------
# CRUD overrides
# ------------------------------------------------------------------
@api.model_create_multi
def create(self, vals_list):
"""Assign a sequence number when creating a new batch."""
for vals in vals_list:
if vals.get('name', '/') == '/':
vals['name'] = self.env['ir.sequence'].next_by_code(
'fusion.batch.payment'
) or _('New')
return super().create(vals_list)
# ------------------------------------------------------------------
# Actions / Business Logic
# ------------------------------------------------------------------
def validate_batch(self):
"""Validate the batch and mark it as *Sent*.
All payments in the batch must be in the *posted* state before
the batch can be validated.
:raises UserError: if the batch contains no payments or if any
payment is not posted.
"""
self.ensure_one()
if self.state != 'draft':
raise UserError(_("Only draft batches can be validated."))
if not self.payment_ids:
raise UserError(_(
"Cannot validate an empty batch. Please add payments first."
))
non_posted = self.payment_ids.filtered(lambda p: p.state != 'posted')
if non_posted:
raise UserError(_(
"The following payments are not posted and must be confirmed "
"before the batch can be validated:\n%(payments)s",
payments=', '.join(non_posted.mapped('name')),
))
self.write({'state': 'sent'})
def action_draft(self):
"""Reset a sent batch back to draft state."""
self.ensure_one()
if self.state != 'sent':
raise UserError(_("Only sent batches can be reset to draft."))
self.write({'state': 'draft'})
def action_reconcile(self):
"""Mark the batch as reconciled once bank confirms all payments."""
self.ensure_one()
if self.state != 'sent':
raise UserError(_(
"Only sent batches can be marked as reconciled."
))
self.write({'state': 'reconciled'})
def print_batch(self):
"""Generate a printable report for this batch payment.
:return: Action dictionary triggering the report download.
:rtype: dict
"""
self.ensure_one()
return self.env.ref(
'fusion_accounting.action_report_batch_payment'
).report_action(self)
@api.model
def create_batch_from_payments(self, payment_ids):
"""Create a new batch payment from an existing set of payments.
All supplied payments must share the same journal and payment
method.
:param payment_ids: recordset or list of ``account.payment`` ids
:return: newly created ``fusion.batch.payment`` record
:raises UserError: when payments do not share journal / method
"""
if isinstance(payment_ids, (list, tuple)):
payments = self.env['account.payment'].browse(payment_ids)
else:
payments = payment_ids
if not payments:
raise UserError(_("No payments were provided."))
journals = payments.mapped('journal_id')
methods = payments.mapped('payment_method_id')
if len(journals) > 1:
raise UserError(_(
"All payments must belong to the same bank journal to "
"be batched together."
))
if len(methods) > 1:
raise UserError(_(
"All payments must use the same payment method."
))
return self.create({
'journal_id': journals.id,
'payment_method_id': methods.id,
'payment_ids': [(6, 0, payments.ids)],
})