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
|
# -*- 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_mode = fields.Selection(selection=[
('custom', 'Specific'),
('entry', 'Journal Entry Date')
], required=True, default='custom')
date = fields.Date(string='Reversal date', default=fields.Date.context_today)
reason = fields.Char(string='Reason')
refund_method = fields.Selection(selection=[
('refund', 'Partial Refund'),
('cancel', 'Full Refund'),
('modify', 'Full refund and new draft invoice')
], string='Credit Method', required=True,
help='Choose how you want to credit this invoice. You cannot "modify" nor "cancel" if the invoice is already reconciled.')
journal_id = fields.Many2one('account.journal', string='Use Specific Journal', help='If empty, uses the journal of the journal entry to be reversed.', check_company=True)
company_id = fields.Many2one('res.company', required=True, readonly=True)
# 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.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 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)]
if 'refund_method' in fields:
res['refund_method'] = (len(move_ids) > 1 or move_ids.move_type == 'entry') and 'cancel' or 'refund'
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 if self.date_mode == 'custom' else move.date
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': move.is_invoice(include_receipts=True) and (self.date or move.date) or False,
'journal_id': self.journal_id and self.journal_id.id or move.journal_id.id,
'invoice_payment_term_id': None,
'invoice_user_id': move.invoice_user_id.id,
'auto_post': True if reverse_date > fields.Date.context_today(self) else False,
}
def _reverse_moves_post_hook(self, moves):
# DEPRECATED: TO REMOVE IN MASTER
return
def reverse_moves(self):
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 = bool(default_vals.get('auto_post'))
is_cancel_needed = not is_auto_post and self.refund_method in ('cancel', '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)
if self.refund_method == 'modify':
moves_vals_list = []
for move in moves.with_context(include_business_fields=True):
moves_vals_list.append(move.copy_data({'date': self.date if self.date_mode == 'custom' else move.date})[0])
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,
})
else:
action.update({
'view_mode': 'tree,form',
'domain': [('id', 'in', moves_to_redirect.ids)],
})
return action
|