1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190
|
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo.tools.translate import _
from odoo.exceptions import UserError
class AccountMoveReversal(models.TransientModel):
"""
Account move reversal wizard, it cancel an account move by reversing it.
"""
_name = 'account.move.reversal'
_description = 'Account Move Reversal'
_check_company_auto = True
move_ids = fields.Many2many('account.move', 'account_move_reversal_move', 'reversal_id', 'move_id', domain=[('state', '=', 'posted')])
new_move_ids = fields.Many2many('account.move', 'account_move_reversal_new_move', 'reversal_id', 'new_move_id')
date = fields.Date(string='Reversal date', default=fields.Date.context_today)
reason = fields.Char(string='Reason displayed on Credit Note')
journal_id = fields.Many2one(
comodel_name='account.journal',
string='Journal',
required=True,
compute='_compute_journal_id',
readonly=False,
store=True,
check_company=True,
help='If empty, uses the journal of the journal entry to be reversed.',
)
company_id = fields.Many2one('res.company', required=True, readonly=True)
available_journal_ids = fields.Many2many('account.journal', compute='_compute_available_journal_ids')
country_code = fields.Char(related='company_id.country_id.code')
# computed fields
residual = fields.Monetary(compute="_compute_from_moves")
currency_id = fields.Many2one('res.currency', compute="_compute_from_moves")
move_type = fields.Char(compute="_compute_from_moves")
@api.depends('move_ids')
def _compute_journal_id(self):
for record in self:
if record.journal_id:
record.journal_id = record.journal_id
else:
journals = record.move_ids.journal_id.filtered(lambda x: x.active)
record.journal_id = journals[0] if journals else None
@api.depends('move_ids')
def _compute_available_journal_ids(self):
for record in self:
if record.move_ids:
record.available_journal_ids = self.env['account.journal'].search([
*self.env['account.journal']._check_company_domain(record.company_id),
('type', 'in', record.move_ids.journal_id.mapped('type')),
])
else:
record.available_journal_ids = self.env['account.journal'].search([
*self.env['account.journal']._check_company_domain(record.company_id),
])
@api.constrains('journal_id', 'move_ids')
def _check_journal_type(self):
for record in self:
if record.journal_id.type not in record.move_ids.journal_id.mapped('type'):
raise UserError(_('Journal should be the same type as the reversed entry.'))
@api.model
def default_get(self, fields):
res = super(AccountMoveReversal, self).default_get(fields)
move_ids = self.env['account.move'].browse(self.env.context['active_ids']) if self.env.context.get('active_model') == 'account.move' else self.env['account.move']
if len(move_ids.company_id) > 1:
raise UserError(_("All selected moves for reversal must belong to the same company."))
if any(move.state != "posted" for move in move_ids):
raise UserError(_('You can only reverse posted moves.'))
if 'company_id' in fields:
res['company_id'] = move_ids.company_id.id or self.env.company.id
if 'move_ids' in fields:
res['move_ids'] = [(6, 0, move_ids.ids)]
return res
@api.depends('move_ids')
def _compute_from_moves(self):
for record in self:
move_ids = record.move_ids._origin
record.residual = len(move_ids) == 1 and move_ids.amount_residual or 0
record.currency_id = len(move_ids.currency_id) == 1 and move_ids.currency_id or False
record.move_type = move_ids.move_type if len(move_ids) == 1 else (any(move.move_type in ('in_invoice', 'out_invoice') for move in move_ids) and 'some_invoice' or False)
def _prepare_default_reversal(self, move):
reverse_date = self.date
mixed_payment_term = move.invoice_payment_term_id.id if move.invoice_payment_term_id.early_pay_discount_computation == 'mixed' else None
return {
'ref': _('Reversal of: %(move_name)s, %(reason)s', move_name=move.name, reason=self.reason)
if self.reason
else _('Reversal of: %s', move.name),
'date': reverse_date,
'invoice_date_due': reverse_date,
'invoice_date': move.is_invoice(include_receipts=True) and (self.date or move.date) or False,
'journal_id': self.journal_id.id,
'invoice_payment_term_id': mixed_payment_term,
'invoice_user_id': move.invoice_user_id.id,
'auto_post': 'at_date' if reverse_date > fields.Date.context_today(self) else 'no',
}
def reverse_moves(self, is_modify=False):
self.ensure_one()
moves = self.move_ids
# Create default values.
partners = moves.company_id.partner_id + moves.commercial_partner_id
bank_ids = self.env['res.partner.bank'].search([
('partner_id', 'in', partners.ids),
('company_id', 'in', moves.company_id.ids + [False]),
], order='sequence DESC')
partner_to_bank = {bank.partner_id: bank for bank in bank_ids}
default_values_list = []
for move in moves:
if move.is_outbound():
partner = move.company_id.partner_id
else:
partner = move.commercial_partner_id
default_values_list.append({
'partner_bank_id': partner_to_bank.get(partner, self.env['res.partner.bank']).id,
**self._prepare_default_reversal(move),
})
batches = [
[self.env['account.move'], [], True], # Moves to be cancelled by the reverses.
[self.env['account.move'], [], False], # Others.
]
for move, default_vals in zip(moves, default_values_list):
is_auto_post = default_vals.get('auto_post') != 'no'
is_cancel_needed = not is_auto_post and (is_modify or self.move_type == 'entry')
batch_index = 0 if is_cancel_needed else 1
batches[batch_index][0] |= move
batches[batch_index][1].append(default_vals)
# Handle reverse method.
moves_to_redirect = self.env['account.move']
for moves, default_values_list, is_cancel_needed in batches:
new_moves = moves._reverse_moves(default_values_list, cancel=is_cancel_needed)
moves._message_log_batch(
bodies={move.id: move.env._('This entry has been %s', reverse._get_html_link(title=move.env._("reversed"))) for move, reverse in zip(moves, new_moves)}
)
if is_modify:
moves_vals_list = []
for move in moves.with_context(include_business_fields=True):
data = move.copy_data(self._modify_default_reverse_values(move))[0]
data['line_ids'] = [line for line in data['line_ids'] if line[2]['display_type'] in ('product', 'line_section', 'line_note')]
moves_vals_list.append(data)
new_moves = self.env['account.move'].create(moves_vals_list)
moves_to_redirect |= new_moves
self.new_move_ids = moves_to_redirect
# Create action.
action = {
'name': _('Reverse Moves'),
'type': 'ir.actions.act_window',
'res_model': 'account.move',
}
if len(moves_to_redirect) == 1:
action.update({
'view_mode': 'form',
'res_id': moves_to_redirect.id,
'context': {'default_move_type': moves_to_redirect.move_type},
})
else:
action.update({
'view_mode': 'list,form',
'domain': [('id', 'in', moves_to_redirect.ids)],
})
if len(set(moves_to_redirect.mapped('move_type'))) == 1:
action['context'] = {'default_move_type': moves_to_redirect.mapped('move_type').pop()}
return action
def refund_moves(self):
return self.reverse_moves(is_modify=False)
def modify_moves(self):
return self.reverse_moves(is_modify=True)
def _modify_default_reverse_values(self, origin_move):
return {
'date': self.date
}
|