Initial commit
This commit is contained in:
257
Fusion Accounting/models/batch_payment.py
Normal file
257
Fusion Accounting/models/batch_payment.py
Normal file
@@ -0,0 +1,257 @@
|
||||
"""
|
||||
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)],
|
||||
})
|
||||
Reference in New Issue
Block a user