258 lines
9.0 KiB
Python
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)],
|
|
})
|