19.0 vanilla

This commit is contained in:
Ernad Husremovic 2026-03-09 09:30:07 +01:00
parent ba20ce7443
commit 768b70e05e
2357 changed files with 1057103 additions and 712486 deletions

View file

@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
from odoo import models, fields, api
from odoo import Command, models, fields, api
from odoo.tools.translate import _
from odoo.exceptions import UserError
@ -14,21 +14,11 @@ class AccountMoveReversal(models.TransientModel):
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.')
reason = fields.Char(string='Reason displayed on Credit Note')
journal_id = fields.Many2one(
comodel_name='account.journal',
string='Use Specific Journal',
string='Journal',
required=True,
compute='_compute_journal_id',
readonly=False,
@ -59,11 +49,13 @@ class AccountMoveReversal(models.TransientModel):
for record in self:
if record.move_ids:
record.available_journal_ids = self.env['account.journal'].search([
('company_id', '=', record.company_id.id),
*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([('company_id', '=', record.company_id.id)])
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):
@ -76,14 +68,17 @@ class AccountMoveReversal(models.TransientModel):
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.'))
raise UserError(_(
'To reverse a journal entry, it has to be posted first.'
))
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')
@ -95,41 +90,32 @@ class AccountMoveReversal(models.TransientModel):
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
mixed_payment_term = move.invoice_payment_term_id.id if move.invoice_payment_term_id and move.company_id.early_pay_discount_computation == 'mixed' else None
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
lang = move.partner_id.lang or self.env.lang
return {
'ref': _('Reversal of: %(move_name)s, %(reason)s', move_name=move.name, reason=self.reason)
'ref': self.with_context(lang=lang).env._('Reversal of: %(move_name)s, %(reason)s', move_name=move.name, reason=self.reason)
if self.reason
else _('Reversal of: %s', move.name),
else self.with_context(lang=lang).env._('Reversal of: %s', move.name),
'date': reverse_date,
'invoice_date_due': reverse_date,
'invoice_date': move.is_invoice(include_receipts=True) and reverse_date or False,
'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',
'invoice_origin': move.invoice_origin,
}
def reverse_moves(self):
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,
'partner_bank_id': False, # Resets the partner_bank_id as we'll force its recomputation
**self._prepare_default_reversal(move),
})
@ -139,7 +125,7 @@ class AccountMoveReversal(models.TransientModel):
]
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 self.refund_method in ('cancel', 'modify')
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)
@ -148,12 +134,19 @@ class AccountMoveReversal(models.TransientModel):
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)
new_moves._compute_partner_bank_id()
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 self.refund_method == 'modify':
if is_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])
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_subsection', 'line_note')]
moves_vals_list.append(data)
new_moves = self.env['account.move'].create(moves_vals_list)
new_moves._compute_partner_bank_id()
moves_to_redirect |= new_moves
@ -173,9 +166,30 @@ class AccountMoveReversal(models.TransientModel):
})
else:
action.update({
'view_mode': 'tree,form',
'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):
data = {
'date': self.date
}
# if has vendor attachment, keep it
if origin_move.move_type.startswith('in_') and origin_move.message_main_attachment_id:
new_main_attachment_id = origin_move.message_main_attachment_id.copy({'res_id': False}).id
data.update({
'message_main_attachment_id': new_main_attachment_id,
'attachment_ids': [Command.link(new_main_attachment_id)],
})
return data