""" 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)], })