Files
odoo_source/addons/account/wizard/account_resequence.py
T
wan dfd01b8c5c [IMP] account: rework name sequences
Task 2146469

Rework the name computation:
* It doesn't use ir.sequence anymore
* It is an editable computed field
* Add a renaming tool

This allows the user to tweak the sequences more easily than having to
get through the ir.sequence settings.

The sequence depends on a parameter of the journal: continuous, montly
and yearly restart of the sequence. Everytime a new period is started,
try to make a pattern form another period.

The incrementing is done by taking the previous name, ordered
lexicographically, splitting it by taking the digits at the end, adding
1 and re contstruct with the prefix.

** This means there could be cases where the prefix has a big importance
on the next number. For instance, if you have
INVOICE/2019/0001
INVOICE/2019/0002
and then rename the last one to INV/2019/0002, don't expect the next
number to be INV/2019/0003. It will be INVOICE/2019/0002 (again) because
the highest number was INVOICE/2019/0001.
You will then end up with
INVOICE/2019/0001
INV/2019/0002
INVOICE/2019/0002

closes odoo/odoo#41485

Related: odoo/enterprise#7189
Signed-off-by: Quentin De Paoli (qdp) <qdp@openerp.com>
2020-02-05 13:59:43 +00:00

165 lines
8.6 KiB
Python

# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import UserError
from odoo.tools.date_utils import get_month, get_fiscal_year
from odoo.tools.misc import format_date
import re
from collections import defaultdict
import json
class ReSequenceWizard(models.TransientModel):
_name = 'account.resequence.wizard'
_description = 'Remake the sequence of Journal Entries.'
sequence_number_reset = fields.Char(compute='_compute_sequence_number_reset')
first_date = fields.Date(help="Date (inclusive) from which the numbers are resequenced.")
end_date = fields.Date(help="Date (inclusive) to which the numbers are resequenced. If not set, all Journal Entries up to the end of the period are resequenced.")
first_name = fields.Char(compute="_compute_first_name", readonly=False, store=True, required=True, string="First New Sequence")
ordering = fields.Selection([('keep', 'Keep current order'), ('date', 'Reorder by accounting date')], required=True, default='keep')
move_ids = fields.Many2many('account.move')
new_values = fields.Text(compute='_compute_new_values')
preview_moves = fields.Text(compute='_compute_preview_moves')
@api.model
def default_get(self, fields_list):
values = super(ReSequenceWizard, self).default_get(fields_list)
active_move_ids = self.env['account.move']
if self.env.context.get('select_all') and self.env.context.get('active_model') == 'account.move' and 'active_domain' in self.env.context:
active_move_ids = self.env['account.move'].search(self.env.context['active_domain'])
elif self.env.context['active_model'] == 'account.move' and 'active_ids' in self.env.context:
active_move_ids = self.env['account.move'].browse(self.env.context['active_ids'])
if len(active_move_ids.journal_id) > 1:
raise UserError(_('You can only resequence items from the same journal'))
if active_move_ids.journal_id.refund_sequence and len(set(active_move_ids.mapped('type')) - {'out_receipt', 'in_receipt'}) > 1:
raise UserError(_('The sequences of this journal are different for Invoices and Refunds but you selected some of both types.'))
values['move_ids'] = [(6, 0, active_move_ids.ids)]
return values
@api.depends('first_name')
def _compute_sequence_number_reset(self):
for record in self:
record.sequence_number_reset = self.move_ids[0]._deduce_sequence_number_reset(record.first_name)
@api.depends('move_ids')
def _compute_first_name(self):
self.first_name = ""
for record in self:
if record.move_ids:
record.first_name = min(record.move_ids._origin.mapped('name'))
@api.depends('new_values', 'ordering')
def _compute_preview_moves(self):
"""Reduce the computed new_values to a smaller set to display in the preview."""
for record in self:
new_values = sorted(json.loads(record.new_values).values(), key=lambda x: x['server-date'], reverse=True)
changeLines = []
in_elipsis = 0
previous_line = None
for i, line in enumerate(new_values):
if i < 3 or i == len(new_values) - 1 or line['new_by_name'] != line['new_by_date'] \
or (self.sequence_number_reset == 'year' and line['server-date'][0:4] != previous_line['server-date'][0:4])\
or (self.sequence_number_reset == 'month' and line['server-date'][0:7] != previous_line['server-date'][0:7]):
if in_elipsis:
changeLines.append({'current_name': '... (%s other)' % str(in_elipsis), 'new_by_name': '...', 'new_by_date': '...', 'date': '...'})
in_elipsis = 0
changeLines.append(line)
else:
in_elipsis += 1
previous_line = line
record.preview_moves = json.dumps({
'ordering': record.ordering,
'changeLines': changeLines,
})
@api.depends('first_name', 'move_ids', 'sequence_number_reset')
def _compute_new_values(self):
"""Compute the proposed new values.
Sets a json string on new_values representing a dictionary thats maps account.move
ids to a disctionay containing the name if we execute the action, and information
relative to the preview widget.
"""
def _get_move_key(move_id):
if self.sequence_number_reset == 'year':
return move_id.date.year
elif self.sequence_number_reset == 'month':
return (move_id.date.year, move_id.date.month)
return 'default'
def _sort_by_name_key(name):
match = re.match(self.move_ids[0]._sequence_fixed_regex, name)
return (match.group('prefix1'), int(match.group('seq') or '0'), match.group('suffix'))
self.new_values = "{}"
for record in self.filtered('first_name'):
moves_by_period = defaultdict(lambda: record.env['account.move'])
for move in record.move_ids._origin: # Sort the moves by period depending on the sequence number reset
moves_by_period[_get_move_key(move)] += move
if record.sequence_number_reset == 'month':
sequence = re.match(self.move_ids[0]._sequence_monthly_regex, record.first_name)
format = '{prefix1}%(year)04d{prefix2}%(month)02d{prefix3}%(seq)0{len}d{suffix}'.format(
prefix1=sequence.group('prefix1'),
prefix2=sequence.group('prefix2'),
prefix3=sequence.group('prefix3'),
len=len(sequence.group('seq')),
suffix=sequence.group('suffix'),
)
elif record.sequence_number_reset == 'year':
sequence = re.match(self.move_ids[0]._sequence_yearly_regex, record.first_name)
format = '{prefix1}%(year)04d{prefix2}%(seq)0{len}d{suffix}'.format(
prefix1=sequence.group('prefix1'),
prefix2=sequence.group('prefix2'),
len=len(sequence.group('seq')),
suffix=sequence.group('suffix'),
)
else:
sequence = re.match(self.move_ids[0]._sequence_fixed_regex, record.first_name)
format = '{prefix}%(seq)0{len}d{suffix}'.format(
prefix=sequence.group('prefix1'),
len=len(sequence.group('seq')),
suffix=sequence.group('suffix'),
)
new_values = {}
for j, period_recs in enumerate(moves_by_period.values()):
# compute the new values period by period
for move in period_recs:
new_values[move.id] = {
'current_name': move.name,
'state': move.state,
'date': format_date(self.env, move.date),
'server-date': str(move.date),
}
new_name_list = [format % {
'year': period_recs[0].date.year,
'month': period_recs[0].date.month,
'seq': i + (int(sequence.group('seq') or '1') if j == (len(moves_by_period)-1) else 1),
} for i in range(len(period_recs))]
# For all the moves of this period, assign the name by increasing initial name
for move, new_name in zip(period_recs.sorted(lambda m: _sort_by_name_key(m.name)), new_name_list):
new_values[move.id]['new_by_name'] = new_name
# For all the moves of this period, assign the name by increasing date
for move, new_name in zip(period_recs.sorted(lambda m: (m.date, m.name, m.id)), new_name_list):
new_values[move.id]['new_by_date'] = new_name
record.new_values = json.dumps(new_values)
def resequence(self):
new_values = json.loads(self.new_values)
# Can't change the name of a posted invoice, but we do not want to have the chatter
# logging 3 separate changes with [state to draft], [change of name], [state to posted]
self.with_context(tracking_disable=True).move_ids.state = 'draft'
for move_id in self.move_ids:
if str(move_id.id) in new_values:
if self.ordering == 'keep':
move_id.name = new_values[str(move_id.id)]['new_by_name']
else:
move_id.name = new_values[str(move_id.id)]['new_by_date']
move_id.with_context(tracking_disable=True).state = new_values[str(move_id.id)]['state']