from odoo import models, fields, api, _ from odoo.exceptions import UserError class BankInternalTransfer(models.Model): _name = 'bank.internal.transfer' _description = 'Bank Internal Transfer' _order = 'date desc, id desc' name = fields.Char(string='Reference', required=True, copy=False, readonly=True, default=lambda self: _('New')) source_journal_id = fields.Many2one('account.journal', string='Source Bank Journal', domain="[('type', '=', 'bank')]", required=True) destination_journal_id = fields.Many2one('account.journal', string='Destination Bank Journal', domain="[('type', '=', 'bank')]", required=True) date = fields.Date(string='Date', required=True, default=fields.Date.context_today) amount = fields.Monetary(string='Amount', required=True) currency_id = fields.Many2one('res.currency', compute='_compute_currency_id', store=True) company_id = fields.Many2one('res.company', required=True, default=lambda self: self.env.company) memo = fields.Char(string='Memo') payment_method_line_id = fields.Many2one('account.payment.method.line', string='Payment Method', domain="[('journal_id', '=', source_journal_id), ('payment_type', '=', 'outbound')]") state = fields.Selection([ ('draft', 'Draft'), ('posted', 'Posted'), ], string='Status', default='draft', required=True) source_statement_line_id = fields.Many2one('account.bank.statement.line', string='Source Statement Line', readonly=True) destination_statement_line_id = fields.Many2one('account.bank.statement.line', string='Destination Statement Line', readonly=True) # Fields for report compatibility (account.report_payment_receipt) ref = fields.Char(string='Report Reference', related='memo') partner_id = fields.Many2one('res.partner', compute='_compute_dummy_fields') partner_type = fields.Char(compute='_compute_dummy_fields') payment_method_id = fields.Many2one('account.payment.method', compute='_compute_payment_method_id') reconciled_invoice_ids = fields.Many2many('account.move', compute='_compute_dummy_fields') reconciled_bill_ids = fields.Many2many('account.move', compute='_compute_dummy_fields') journal_id = fields.Many2one('account.journal', compute='_compute_dummy_fields') partner_bank_id = fields.Many2one('res.partner.bank', compute='_compute_dummy_fields') payment_ids = fields.Many2many('bank.internal.transfer', compute='_compute_dummy_fields', string='Payments') deduction_line_ids = fields.Many2many('account.move', compute='_compute_dummy_fields') amount_total = fields.Monetary(related='amount') final_payment_amount = fields.Monetary(related='amount') def _compute_dummy_fields(self): for rec in self: # Mimic standard Odoo internal transfer behavior where the company is the partner rec.partner_id = rec.company_id.partner_id rec.partner_type = 'supplier' rec.reconciled_invoice_ids = False rec.reconciled_bill_ids = False rec.journal_id = rec.source_journal_id rec.partner_bank_id = rec.destination_journal_id.bank_account_id rec.payment_ids = rec.ids rec.deduction_line_ids = False @api.depends('payment_method_line_id') def _compute_payment_method_id(self): for rec in self: rec.payment_method_id = rec.payment_method_line_id.payment_method_id def _get_payment_receipt_report_values(self): self.ensure_one() return { 'display_payment_method': True, 'display_invoices': False, } @api.depends('source_journal_id') def _compute_currency_id(self): for record in self: record.currency_id = record.source_journal_id.currency_id or record.company_id.currency_id @api.model_create_multi def create(self, vals_list): for vals in vals_list: if vals.get('name', _('New')) == _('New'): vals['name'] = self.env['ir.sequence'].next_by_code('bank.internal.transfer') or _('New') return super().create(vals_list) def action_confirm(self): for transfer in self: if transfer.amount <= 0: raise UserError(_("Amount must be strictly positive.")) if transfer.source_journal_id == transfer.destination_journal_id: raise UserError(_("Source and destination journals must be different.")) # 1. Determine Outstanding Payments account from Source Journal outstanding_account = transfer.source_journal_id.outbound_payment_method_line_ids.mapped('payment_account_id') if outstanding_account: outstanding_account = outstanding_account[0] else: outstanding_account = transfer.company_id.account_journal_payment_credit_account_id if not outstanding_account: raise UserError(_("Please configure the Outstanding Payments Account on the source journal or company.")) # 2. Create the Bank Statement Line for the Source Journal stmt_line_vals = { 'journal_id': transfer.source_journal_id.id, 'date': transfer.date, 'payment_ref': transfer.memo or transfer.name, 'amount': -transfer.amount, # Outgoing money 'bank_internal_transfer_id': transfer.id, } stmt_line = self.env['account.bank.statement.line'].create(stmt_line_vals) # 3. Modify the underlying move's suspense line to use the Outstanding Payments account # This effectively "reconciles" the source line against the transfer suspense_account = transfer.source_journal_id.suspense_account_id if not suspense_account: raise UserError(_("Please configure the Suspense Account on the source journal.")) move = stmt_line.move_id suspense_line = move.line_ids.filtered(lambda l: l.account_id == suspense_account) if not suspense_line: suspense_line = move.line_ids.filtered(lambda l: l.debit > 0) if suspense_line: suspense_line.with_context(check_move_validity=False).write({ 'account_id': outstanding_account.id, }) # Post the move move.action_post() # Link and update state transfer.write({ 'source_statement_line_id': stmt_line.id, 'state': 'posted', }) def _automate_destination_leg(self): """ Automatically create and reconcile the destination bank statement line. This is called when the source statement line is 'Reviewed'. """ self.ensure_one() if self.destination_statement_line_id: return # 1. Determine the transit account transit_account = self.source_journal_id.outbound_payment_method_line_ids.mapped('payment_account_id') if transit_account: transit_account = transit_account[0] else: # Fallback to default transit account code 218401 transit_account = self.env['account.account'].search([ ('code', '=', '218401'), ('company_id', '=', self.company_id.id) ], limit=1) if not transit_account: transit_account = self.company_id.account_journal_payment_credit_account_id if not transit_account: raise UserError(_("Could not find a suitable transit account (218401 or Outstanding Payments).")) # 2. Create the Bank Statement Line for the Destination Journal dst_st_line_vals = { 'journal_id': self.destination_journal_id.id, 'date': self.date, 'payment_ref': self.memo or self.name, 'amount': self.amount, # Incoming money 'bank_internal_transfer_id': self.id, } dst_st_line = self.env['account.bank.statement.line'].create(dst_st_line_vals) self.destination_statement_line_id = dst_st_line # 3. Reconcile the destination line against the transit account move = dst_st_line.move_id suspense_account = self.destination_journal_id.suspense_account_id suspense_line = move.line_ids.filtered(lambda l: l.account_id == suspense_account) if not suspense_line: suspense_line = move.line_ids.filtered(lambda l: l.credit > 0) if suspense_line: suspense_line.with_context(check_move_validity=False).write({ 'account_id': transit_account.id, }) # 4. Mark as reviewed automatically move.set_moves_checked(True) def action_draft(self): for transfer in self: if transfer.source_statement_line_id: if transfer.source_statement_line_id.is_reconciled and transfer.destination_statement_line_id: raise UserError(_("You cannot reset a transfer that has already been processed on the destination side.")) transfer.source_statement_line_id.move_id.button_draft() transfer.source_statement_line_id.unlink() if transfer.destination_statement_line_id: transfer.destination_statement_line_id.move_id.button_draft() transfer.destination_statement_line_id.unlink() transfer.state = 'draft'