172 lines
7.6 KiB
Python
172 lines
7.6 KiB
Python
# -*- 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.
|
|
default_values_list = []
|
|
for move in moves:
|
|
default_values_list.append(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
|
|
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: _('This entry has been %s', reverse._get_html_link(title=_("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({'date': self.date})[0]
|
|
data['line_ids'] = [line for line in data['line_ids'] if line[2]['display_type'] == 'product']
|
|
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': 'tree,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)
|