[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>
This commit is contained in:
wan
2020-02-05 13:59:43 +00:00
parent a6d7c7b04a
commit dfd01b8c5c
40 changed files with 793 additions and 606 deletions
+2
View File
@@ -27,6 +27,7 @@ You could use this simplified accounting in case you work with an (external) acc
'wizard/account_accrual_accounting_view.xml',
'wizard/account_unreconcile_view.xml',
'wizard/account_move_reversal_view.xml',
'wizard/account_resequence_views.xml',
'views/account_move_views.xml',
'wizard/setup_wizards_view.xml',
'wizard/pos_box.xml',
@@ -70,6 +71,7 @@ You could use this simplified accounting in case you work with an (external) acc
],
'qweb': [
"static/src/xml/account_payment.xml",
'static/src/xml/account_resequence.xml',
"static/src/xml/account_report_backend.xml",
"static/src/xml/bills_tree_upload_views.xml",
'static/src/xml/account_journal_activity.xml',
+1
View File
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from . import sequence_mixin
from . import partner
from . import account
from . import account_reconcile_model
+8 -109
View File
@@ -921,7 +921,7 @@ class AccountJournal(models.Model):
return self.__get_bank_statements_available_sources()
name = fields.Char(string='Journal Name', required=True)
code = fields.Char(string='Short Code', size=5, required=True, help="The journal entries of this journal will be named using this prefix.")
code = fields.Char(string='Short Code', size=5, required=True, help="Shorter name used for display. The journal entries of this journal will also be named using this prefix by default.")
active = fields.Boolean(default=True, help="Set active to false to hide the Journal without removing it.")
type = fields.Selection([
('sale', 'Sales'),
@@ -944,19 +944,7 @@ class AccountJournal(models.Model):
domain="[('deprecated', '=', False), ('company_id', '=', company_id)]", help="It acts as a default account for debit amount", ondelete='restrict')
restrict_mode_hash_table = fields.Boolean(string="Lock Posted Entries with Hash",
help="If ticked, the accounting entry or invoice receives a hash as soon as it is posted and cannot be modified anymore.")
sequence_id = fields.Many2one('ir.sequence', string='Entry Sequence',
help="This field contains the information related to the numbering of the journal entries of this journal.", required=True, copy=False)
refund_sequence_id = fields.Many2one('ir.sequence', string='Credit Note Entry Sequence',
help="This field contains the information related to the numbering of the credit note entries of this journal.", copy=False)
sequence = fields.Integer(help='Used to order Journals in the dashboard view', default=10)
sequence_number_next = fields.Integer(string='Next Number',
help='The next sequence number will be used for the next invoice.',
compute='_compute_seq_number_next',
inverse='_inverse_seq_number_next')
refund_sequence_number_next = fields.Integer(string='Credit Notes Next Number',
help='The next sequence number will be used for the next credit note.',
compute='_compute_refund_seq_number_next',
inverse='_inverse_refund_seq_number_next')
invoice_reference_type = fields.Selection(string='Communication Type', required=True, selection=[('none', 'Free'), ('partner', 'Based on Customer'), ('invoice', 'Based on Invoice')], default='invoice', help='You can set here the default communication that will appear on customer invoices, once validated, to help the customer to refer to that particular invoice when making the payment.')
invoice_reference_model = fields.Selection(string='Communication Standard', required=True, selection=[('odoo', 'Odoo'),('euro', 'European')], default='odoo', help="You can choose different models for each type of reference. The default one is the Odoo reference.")
@@ -967,6 +955,11 @@ class AccountJournal(models.Model):
help="Company related to this journal")
refund_sequence = fields.Boolean(string='Dedicated Credit Note Sequence', help="Check this box if you don't want to share the same sequence for invoices and credit notes made from this journal", default=False)
sequence_override_regex = fields.Text(help="Technical field used to enforce complex sequence composition that the system would normally misunderstand.\n"\
"This is a regex that can include all the following capture groups: prefix1, year, prefix2, month, prefix3, seq, suffix.\n"\
"The prefix* groups are the separators between the year, month and the actual increasing sequence number (seq).\n"\
"e.g: ^(?P<prefix1>.*?)(?P<year>\d{4})(?P<prefix2>\D*?)(?P<month>\d{2})(?P<prefix3>\D+?)(?P<seq>\d+)(?P<suffix>\D*?)$")
inbound_payment_method_ids = fields.Many2many('account.payment.method', 'account_journal_inbound_payment_method_rel', 'journal_id', 'inbound_payment_method',
domain=[('payment_type', '=', 'inbound')], string='For Incoming Payments', default=lambda self: self._default_inbound_payment_methods(),
@@ -1009,50 +1002,6 @@ class AccountJournal(models.Model):
for record in self:
record.alias_domain = alias_domain
# do not depend on 'sequence_id.date_range_ids', because
# sequence_id._get_current_sequence() may invalidate it!
@api.depends('sequence_id.use_date_range', 'sequence_id.number_next_actual')
def _compute_seq_number_next(self):
'''Compute 'sequence_number_next' according to the current sequence in use,
an ir.sequence or an ir.sequence.date_range.
'''
for journal in self:
if journal.sequence_id:
sequence = journal.sequence_id._get_current_sequence()
journal.sequence_number_next = sequence.number_next_actual
else:
journal.sequence_number_next = 1
def _inverse_seq_number_next(self):
'''Inverse 'sequence_number_next' to edit the current sequence next number.
'''
for journal in self:
if journal.sequence_id and journal.sequence_number_next:
sequence = journal.sequence_id._get_current_sequence()
sequence.sudo().number_next = journal.sequence_number_next
# do not depend on 'refund_sequence_id.date_range_ids', because
# refund_sequence_id._get_current_sequence() may invalidate it!
@api.depends('refund_sequence_id.use_date_range', 'refund_sequence_id.number_next_actual')
def _compute_refund_seq_number_next(self):
'''Compute 'sequence_number_next' according to the current sequence in use,
an ir.sequence or an ir.sequence.date_range.
'''
for journal in self:
if journal.refund_sequence_id and journal.refund_sequence:
sequence = journal.refund_sequence_id._get_current_sequence()
journal.refund_sequence_number_next = sequence.number_next_actual
else:
journal.refund_sequence_number_next = 1
def _inverse_refund_seq_number_next(self):
'''Inverse 'refund_sequence_number_next' to edit the current sequence next number.
'''
for journal in self:
if journal.refund_sequence_id and journal.refund_sequence and journal.refund_sequence_number_next:
sequence = journal.refund_sequence_id._get_current_sequence()
sequence.sudo().number_next = journal.refund_sequence_number_next
@api.constrains('type_control_ids')
def _constrains_type_control_ids(self):
self.env['account.move.line'].flush(['account_id', 'journal_id'])
@@ -1195,14 +1144,6 @@ class AccountJournal(models.Model):
'company_id': company.id,
'partner_id': company.partner_id.id,
})
if ('code' in vals and journal.code != vals['code']):
if self.env['account.move'].search([('journal_id', 'in', self.ids)], limit=1):
raise UserError(_('This journal already contains items, therefore you cannot modify its short name.'))
new_prefix = self._get_sequence_prefix(vals['code'], refund=False)
journal.sequence_id.write({'prefix': new_prefix})
if journal.refund_sequence_id:
new_prefix = self._get_sequence_prefix(vals['code'], refund=True)
journal.refund_sequence_id.write({'prefix': new_prefix})
if 'currency_id' in vals:
if not 'default_debit_account_id' in vals and journal.default_debit_account_id:
journal.default_debit_account_id.currency_id = vals['currency_id']
@@ -1230,16 +1171,6 @@ class AccountJournal(models.Model):
if 'bank_acc_number' in vals:
for journal in self.filtered(lambda r: r.type == 'bank' and not r.bank_account_id):
journal.set_bank_account(vals.get('bank_acc_number'), vals.get('bank_id'))
# create the relevant refund sequence
if vals.get('refund_sequence'):
for journal in self.filtered(lambda j: j.type in ('sale', 'purchase') and not j.refund_sequence_id):
journal_vals = {
'name': journal.name,
'company_id': journal.company_id.id,
'code': journal.code,
'refund_sequence_number_next': vals.get('refund_sequence_number_next', journal.refund_sequence_number_next),
}
journal.refund_sequence_id = self.sudo()._create_sequence(journal_vals, refund=True).id
# Changing the 'post_at' option will post the draft payment moves and change the related invoices' state.
if 'post_at' in vals and vals['post_at'] != 'bank_rec':
draft_moves = self.env['account.move'].search([('journal_id', 'in', self.ids), ('state', '=', 'draft')])
@@ -1252,33 +1183,6 @@ class AccountJournal(models.Model):
return result
@api.model
def _get_sequence_prefix(self, code, refund=False):
prefix = code.upper()
if refund:
prefix = 'R' + prefix
return prefix + '/%(range_year)s/'
@api.model
def _create_sequence(self, vals, refund=False):
""" Create new no_gap entry sequence for every new Journal"""
prefix = self._get_sequence_prefix(vals['code'], refund)
seq_name = refund and vals['code'] + _(': Refund') or vals['code']
seq = {
'name': _('%s Sequence') % seq_name,
'implementation': 'no_gap',
'prefix': prefix,
'padding': 4,
'number_increment': 1,
'use_date_range': True,
}
if 'company_id' in vals:
seq['company_id'] = vals['company_id']
seq = self.env['ir.sequence'].create(seq)
seq_date_range = seq._get_current_sequence()
seq_date_range.number_next = refund and vals.get('refund_sequence_number_next', 1) or vals.get('sequence_number_next', 1)
return seq
@api.model
def _prepare_liquidity_account(self, name, company, currency_id, type):
'''
@@ -1353,11 +1257,6 @@ class AccountJournal(models.Model):
if 'refund_sequence' not in vals:
vals['refund_sequence'] = vals['type'] in ('sale', 'purchase')
# We just need to create the relevant sequences according to the chosen options
if not vals.get('sequence_id'):
vals.update({'sequence_id': self.sudo()._create_sequence(vals).id})
if vals.get('type') in ('sale', 'purchase') and vals.get('refund_sequence') and not vals.get('refund_sequence_id'):
vals.update({'refund_sequence_id': self.sudo()._create_sequence(vals, refund=True).id})
journal = super(AccountJournal, self.with_context(mail_create_nolog=True)).create(vals)
if 'alias_name' in vals:
journal._update_mail_alias(vals)
@@ -1622,9 +1521,9 @@ class AccountTax(models.Model):
JOIN account_tax tax ON tax.id = line.tax_line_id
WHERE line.tax_line_id IN %s
AND line.company_id != tax.company_id
UNION ALL
SELECT line.id
FROM account_move_line_account_tax_rel tax_rel
JOIN account_tax tax ON tax.id = tax_rel.account_tax_id
@@ -10,6 +10,7 @@ from odoo.exceptions import UserError, ValidationError
import time
import math
import base64
import re
class AccountCashboxLine(models.Model):
@@ -220,7 +221,7 @@ class AccountBankStatement(models.Model):
_name = "account.bank.statement"
_description = "Bank Statement"
_order = "date desc, name desc, id desc"
_inherit = ['mail.thread']
_inherit = ['mail.thread', 'sequence.mixin']
name = fields.Char(string='Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True)
reference = fields.Char(string='External Reference', states={'open': [('readonly', False)]}, copy=False, readonly=True, help="Used to hold the reference of the external mean that created this statement (name of imported file, reference of online synchronization...)")
@@ -410,18 +411,44 @@ class AccountBankStatement(models.Model):
""" Changes statement state to Running."""
for statement in self:
if not statement.name:
context = {'ir_sequence_date': statement.date}
if statement.journal_id.sequence_id:
st_number = statement.journal_id.sequence_id.with_context(**context).next_by_id()
else:
SequenceObj = self.env['ir.sequence']
st_number = SequenceObj.with_context(**context).next_by_code('account.bank.statement')
statement.name = st_number
statement._set_next_sequence()
statement.state = 'open'
def button_reopen(self):
self.state = 'open'
def _get_last_sequence_domain(self, relaxed=False):
self.ensure_one()
where_string = "WHERE journal_id = %(journal_id)s AND name != '/'"
param = {'journal_id': self.journal_id.id}
sequence_number_reset = self._deduce_sequence_number_reset(self.search([('date', '<', self.date)], order='date desc', limit=1).name)
if not relaxed:
if sequence_number_reset == 'year':
where_string += " AND date_trunc('year', date) = date_trunc('year', %(date)s) "
param['date'] = self.date
elif sequence_number_reset == 'month':
where_string += " AND date_trunc('month', date) = date_trunc('month', %(date)s) "
param['date'] = self.date
return where_string, param
def _get_starting_sequence(self):
self.ensure_one()
last_sequence = self._get_last_sequence(relaxed=True)
if last_sequence:
sequence_number_reset = self._deduce_sequence_number_reset(self.search([('date', '<', self.date)], order='date desc', limit=1).name)
if sequence_number_reset == 'year':
sequence = re.match(self._sequence_yearly_regex, last_sequence)
if sequence:
return '%s%04d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
elif sequence_number_reset == 'month':
sequence = re.match(self._sequence_monthly_regex, last_sequence)
if sequence:
return '%s%04d%s%02d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), self.date.month, sequence.group('prefix3'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
# There was no pattern found, propose one
return "%s/%04d/%02d/0000" % (self.journal_id.code, self.date.year, self.date.month)
class AccountBankStatementLine(models.Model):
_name = "account.bank.statement.line"
+111 -109
View File
@@ -31,10 +31,22 @@ def calc_check_digits(number):
class AccountMove(models.Model):
_name = "account.move"
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin']
_inherit = ['portal.mixin', 'mail.thread', 'mail.activity.mixin', 'sequence.mixin']
_description = "Journal Entries"
_order = 'date desc, name desc, id desc'
@property
def _sequence_monthly_regex(self):
return self.journal_id.sequence_override_regex or super()._sequence_monthly_regex
@property
def _sequence_yearly_regex(self):
return self.journal_id.sequence_override_regex or super()._sequence_yearly_regex
@property
def _sequence_fixed_regex(self):
return self.journal_id.sequence_override_regex or super()._sequence_fixed_regex
@api.model
def _get_default_journal(self):
''' Get the default journal.
@@ -90,7 +102,9 @@ class AccountMove(models.Model):
return self.env.company.incoterm_id
# ==== Business fields ====
name = fields.Char(string='Number', required=True, readonly=True, copy=False, default='/')
name = fields.Char(string='Number', copy=False, compute='_compute_name', readonly=False, store=True, index=True, tracking=True)
highest_name = fields.Char(compute='_compute_highest_name')
show_name_warning = fields.Boolean(store=False)
date = fields.Date(string='Date', required=True, index=True, readonly=True,
states={'draft': [('readonly', False)]},
default=fields.Date.context_today)
@@ -102,6 +116,7 @@ class AccountMove(models.Model):
('cancel', 'Cancelled'),
], string='Status', required=True, readonly=True, copy=False, tracking=True,
default='draft')
posted_before = fields.Boolean(help="Technical field for knowing if the move has been posted before", copy=False)
type = fields.Selection(selection=[
('entry', 'Journal Entry'),
('out_invoice', 'Customer Invoice'),
@@ -243,13 +258,6 @@ class AccountMove(models.Model):
readonly=True, states={'draft': [('readonly', False)]},
help='Defines the smallest coinage of the currency that can be used to pay by cash.')
# ==== Fields to set the sequence, on the first invoice of the journal ====
invoice_sequence_number_next = fields.Char(string='Next Number',
compute='_compute_invoice_sequence_number_next',
inverse='_inverse_invoice_sequence_number_next')
invoice_sequence_number_next_prefix = fields.Char(string='Next Number Prefix',
compute="_compute_invoice_sequence_number_next")
# ==== Display purpose fields ====
invoice_filter_type_domain = fields.Char(compute='_compute_invoice_filter_type_domain',
help="Technical field used to have a dynamic domain on journal / taxes in the form view.")
@@ -909,6 +917,79 @@ class AccountMove(models.Model):
# COMPUTE METHODS
# -------------------------------------------------------------------------
@api.depends('journal_id', 'date', 'state', 'highest_name')
def _compute_name(self):
for record in self.sorted(lambda m: (m.date, m.ref or '', m.id)):
if not record.name or record.name == '/':
if record.state == 'draft' and not record.posted_before and not record.highest_name:
# First name of the period for the journal, no name yet
record._set_next_sequence()
elif record.state == 'posted':
# No name yet but has been posted
record._set_next_sequence()
if record.name and record.state == 'draft' and not record.posted_before and record.highest_name:
# Not the first name of the period for the journal, but had a name set
record.name = '/'
record.name = record.name or '/'
@api.depends('journal_id', 'date', 'state')
def _compute_highest_name(self):
for record in self:
record.highest_name = record._get_last_sequence()
@api.onchange('name', 'highest_name')
def _onchange_name_warning(self):
if self.name and self.name != '/' and self.name <= (self.highest_name or ''):
self.show_name_warning = True
else:
self.show_name_warning = False
def _get_last_sequence_domain(self, relaxed=False):
self.ensure_one()
if not self.date or not self.journal_id:
return "WHERE FALSE", {}
where_string = "WHERE journal_id = %(journal_id)s AND name != '/'"
param = {'journal_id': self.journal_id.id}
if not relaxed:
reference_move = self.search([('journal_id', '=', self.journal_id.id), ('date', '<=', self.date), ('id', '!=', self.id or self._origin.id)], order='date desc', limit=1) or self.search([('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id)], order='date asc', limit=1)
sequence_number_reset = self._deduce_sequence_number_reset(reference_move.name)
if sequence_number_reset == 'year':
where_string += " AND date_trunc('year', date) = date_trunc('year', %(date)s) "
param['date'] = self.date
elif sequence_number_reset == 'month':
where_string += " AND date_trunc('month', date) = date_trunc('month', %(date)s) "
param['date'] = self.date
if self.journal_id.refund_sequence:
if self.type in ('out_refund', 'in_refund'):
where_string += " AND type IN ('out_refund', 'in_refund') "
else:
where_string += " AND type NOT IN ('out_refund', 'in_refund') "
return where_string, param
def _get_starting_sequence(self):
self.ensure_one()
# Try to find a pattern already used by relaxing a domain. If we are here, the domain non relaxed should return nothing.
last_sequence = self._get_last_sequence(relaxed=True)
if last_sequence:
reference_move = self.search([('journal_id', '=', self.journal_id.id), ('date', '<=', self.date), ('id', '!=', self.id or self._origin.id)], order='date asc', limit=1) or self.search([('journal_id', '=', self.journal_id.id), ('id', '!=', self.id or self._origin.id)], order='date desc', limit=1)
sequence_number_reset = self._deduce_sequence_number_reset(reference_move.name)
if sequence_number_reset == 'year':
sequence = re.match(self._sequence_yearly_regex, last_sequence)
if sequence:
return '%s%04d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
elif sequence_number_reset == 'month':
sequence = re.match(self._sequence_monthly_regex, last_sequence)
if sequence:
return '%s%04d%s%02d%s%s%s' % (sequence.group('prefix1'), self.date.year, sequence.group('prefix2'), self.date.month, sequence.group('prefix3'), "0" * len(sequence.group('seq')), sequence.group('suffix'))
starting_sequence = "%s/%04d/%02d/0000" % (self.journal_id.code, self.date.year, self.date.month)
if self.journal_id.refund_sequence and self.type in ('out_refund', 'in_refund'):
starting_sequence = "R" + starting_sequence
return starting_sequence
@api.depends('type')
def _compute_type_name(self):
type_name_mapping = {k: v for k, v in
@@ -1128,69 +1209,6 @@ class AccountMove(models.Model):
vendor_display_name = _('#Created by: %s') % (move.sudo().create_uid.name or self.env.user.name)
move.invoice_partner_display_name = vendor_display_name
@api.depends('state', 'journal_id', 'date', 'invoice_date')
def _compute_invoice_sequence_number_next(self):
""" computes the prefix of the number that will be assigned to the first invoice/bill/refund of a journal, in order to
let the user manually change it.
"""
# Check user group.
system_user = self.env.is_system()
if not system_user:
self.invoice_sequence_number_next_prefix = False
self.invoice_sequence_number_next = False
return
# Check moves being candidates to set a custom number next.
moves = self.filtered(lambda move: move.is_invoice() and move.name == '/')
if not moves:
self.invoice_sequence_number_next_prefix = False
self.invoice_sequence_number_next = False
return
treated = self.browse()
for key, group in groupby(moves, key=lambda move: (move.journal_id, move._get_sequence())):
journal, sequence = key
domain = [('journal_id', '=', journal.id), ('state', '=', 'posted')]
if self.ids:
domain.append(('id', 'not in', self.ids))
if journal.type == 'sale':
domain.append(('type', 'in', ('out_invoice', 'out_refund')))
elif journal.type == 'purchase':
domain.append(('type', 'in', ('in_invoice', 'in_refund')))
else:
continue
if self.search_count(domain):
continue
for move in group:
sequence_date = move.date or move.invoice_date
prefix, dummy = sequence._get_prefix_suffix(date=sequence_date, date_range=sequence_date)
number_next = sequence._get_current_sequence(sequence_date=sequence_date).number_next_actual
move.invoice_sequence_number_next_prefix = prefix
move.invoice_sequence_number_next = '%%0%sd' % sequence.padding % number_next
treated |= move
remaining = (self - treated)
remaining.invoice_sequence_number_next_prefix = False
remaining.invoice_sequence_number_next = False
def _inverse_invoice_sequence_number_next(self):
''' Set the number_next on the sequence related to the invoice/bill/refund'''
# Check user group.
if not self.env.is_admin():
return
# Set the next number in the sequence.
for move in self:
if not move.invoice_sequence_number_next:
continue
sequence = move._get_sequence()
nxt = re.sub("[^0-9]", '', move.invoice_sequence_number_next)
result = re.match("(0*)([0-9]+)", nxt)
if result and sequence:
sequence_date = move.date or move.invoice_date
date_sequence = sequence._get_current_sequence(sequence_date=sequence_date)
date_sequence.number_next_actual = int(result.group(2))
def _compute_payments_widget_to_reconcile_info(self):
for move in self:
move.invoice_outstanding_credits_debits_widget = json.dumps(False)
@@ -1367,7 +1385,7 @@ class AccountMove(models.Model):
# /!\ Computed stored fields are not yet inside the database.
self._cr.execute('''
SELECT move2.id
SELECT move2.id, move2.name
FROM account_move move
INNER JOIN account_move move2 ON
move2.name = move.name
@@ -1376,9 +1394,10 @@ class AccountMove(models.Model):
AND move2.id != move.id
WHERE move.id IN %s AND move2.state = 'posted'
''', [tuple(moves.ids)])
res = self._cr.fetchone()
res = self._cr.fetchall()
if res:
raise ValidationError(_('Posted journal entry must have an unique sequence number per company.'))
raise ValidationError(_('Posted journal entry must have an unique sequence number per company.\n'
'Problematic numbers: %s\n') % ', '.join(r[1] for r in res))
@api.constrains('ref', 'type', 'partner_id', 'journal_id', 'invoice_date')
def _check_duplicate_supplier_reference(self):
@@ -1587,7 +1606,7 @@ class AccountMove(models.Model):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_MOVE_FIELDS))
if (move.restrict_mode_hash_table and move.inalterable_hash and 'inalterable_hash' in vals) or (move.secure_sequence_number and 'secure_sequence_number' in vals):
raise UserError(_('You cannot overwrite the values ensuring the inalterability of the accounting.'))
if (move.name != '/' and 'journal_id' in vals and move.journal_id.id != vals['journal_id']):
if (move.posted_before and 'journal_id' in vals and move.journal_id.id != vals['journal_id']):
raise UserError(_('You cannot edit the journal of an account move if it has been posted once.'))
# You can't change the date of a move being inside a locked period.
@@ -1600,6 +1619,11 @@ class AccountMove(models.Model):
move._check_fiscalyear_lock_date()
move.line_ids._check_tax_lock_date()
if move.journal_id.sequence_override_regex and vals.get('name') and vals['name'] != '/' and not re.match(move.journal_id.sequence_override_regex, vals['name']):
if not self.env.user.has_group('account.group_account_manager'):
raise UserError(_('The Journal Entry sequence is not conform to the current format. Only the Advisor can change it.'))
move.journal_id.sequence_override_regex = False
if self._move_autocomplete_invoice_lines_write(vals):
res = True
else:
@@ -1612,8 +1636,8 @@ class AccountMove(models.Model):
self._check_fiscalyear_lock_date()
self.mapped('line_ids')._check_tax_lock_date()
if ('state' in vals and vals.get('state') == 'posted') and self.restrict_mode_hash_table:
for move in self.filtered(lambda m: not(m.secure_sequence_number or m.inalterable_hash)):
if ('state' in vals and vals.get('state') == 'posted'):
for move in self.filtered(lambda m: m.restrict_mode_hash_table and not(m.secure_sequence_number or m.inalterable_hash)):
new_number = move.journal_id.secure_sequence_id.next_by_id()
vals_hashing = {'secure_sequence_number': new_number,
'inalterable_hash': move._get_new_hash(new_number)}
@@ -1627,7 +1651,7 @@ class AccountMove(models.Model):
def unlink(self):
for move in self:
if move.name != '/' and not self._context.get('force_delete'):
if move.posted_before and not self._context.get('force_delete'):
raise UserError(_("You cannot delete an entry which has been posted once."))
move.line_ids.unlink()
return super(AccountMove, self).unlink()
@@ -1782,19 +1806,6 @@ class AccountMove(models.Model):
else:
raise UserError(_('The combination of reference model and reference type on the journal is not implemented'))
def _get_sequence(self):
''' Return the sequence to be used during the post of the current move.
:return: An ir.sequence record or False.
'''
self.ensure_one()
journal = self.journal_id
if self.type in ('entry', 'out_invoice', 'in_invoice', 'out_receipt', 'in_receipt') or not journal.refund_sequence:
return journal.sequence_id
if not journal.refund_sequence_id:
return
return journal.refund_sequence_id
def _get_move_display_name(self, show_ref=False):
''' Helper to get the display name of an invoice depending of its type.
:param show_ref: A flag indicating of the display name must include or not the journal entry reference.
@@ -2082,7 +2093,7 @@ class AccountMove(models.Model):
raise UserError(_('You need to add a line before posting.'))
if move.auto_post and move.date > fields.Date.today():
date_msg = move.date.strftime(get_lang(self.env).date_format)
raise UserError(_("This move is configured to be auto-posted on %s" % date_msg))
raise UserError(_("This move is configured to be auto-posted on %s") % date_msg)
if not move.partner_id:
if move.is_sale_document():
@@ -2110,25 +2121,14 @@ class AccountMove(models.Model):
# Create the analytic lines in batch is faster as it leads to less cache invalidation.
self.mapped('line_ids').create_analytic_lines()
self.state = 'posted'
self.posted_before = True
for move in self:
if move.auto_post and move.date > fields.Date.today():
raise UserError(_("This move is configured to be auto-posted on {}".format(move.date.strftime(get_lang(self.env).date_format))))
move.message_subscribe([p.id for p in [move.partner_id] if p not in move.sudo().message_partner_ids])
to_write = {'state': 'posted'}
if move.name == '/':
# Get the journal's sequence.
sequence = move._get_sequence()
if not sequence:
raise UserError(_('Please define a sequence on your journal.'))
# Consume a new number.
to_write['name'] = sequence.next_by_id(sequence_date=move.date)
move.write(to_write)
# Compute 'ref' for 'out_invoice'.
if move._auto_compute_invoice_reference():
to_write = {
@@ -2147,7 +2147,8 @@ class AccountMove(models.Model):
move.company_id.account_bank_reconciliation_start = move.date
for move in self:
if not move.partner_id: continue
if not move.partner_id:
continue
if move.type.startswith('out_'):
move.partner_id._increase_rank('customer_rank')
elif move.type.startswith('in_'):
@@ -3321,13 +3322,13 @@ class AccountMoveLine(models.Model):
if account_to_write and account_to_write.deprecated:
raise UserError(_('You cannot use a deprecated account.'))
# when making a reconciliation on an existing liquidity journal item, mark the payment as reconciled
for line in self:
if line.parent_state == 'posted':
if line.move_id.restrict_mode_hash_table and set(vals).intersection(INTEGRITY_HASH_LINE_FIELDS):
raise UserError(_("You cannot edit the following fields due to restrict mode being activated on the journal: %s.") % ', '.join(INTEGRITY_HASH_LINE_FIELDS))
if any(key in vals for key in ('tax_ids', 'tax_line_ids')):
raise UserError(_('You cannot modify the taxes related to a posted journal item, you should reset the journal entry to draft to do so.'))
# When making a reconciliation on an existing liquidity journal item, mark the payment as reconciled
if 'statement_line_id' in vals and line.payment_id:
# In case of an internal transfer, there are 2 liquidity move lines to match with a bank statement
if all(line.statement_id for line in line.payment_id.move_line_ids.filtered(
@@ -3370,7 +3371,7 @@ class AccountMoveLine(models.Model):
# Get initial values for each line
move_initial_values = {}
for line in self.filtered(lambda l: l.move_id.name != '/'): # Only lines with posted once move.
for line in self.filtered(lambda l: l.move_id.posted_before): # Only lines with posted once move.
for field in tracking_fields:
# Group initial values by move_id
if line.move_id.id not in move_initial_values:
@@ -4398,6 +4399,7 @@ class AccountPartialReconcile(models.Model):
# recorded before the period lock date as the tax statement for this period is
# probably already sent to the estate.
newly_created_move.write({'date': move_date})
newly_created_move.recompute(['name'])
# post move
newly_created_move.post()
+125
View File
@@ -0,0 +1,125 @@
# -*- coding: utf-8 -*-
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
import re
class SequenceMixin(models.AbstractModel):
"""Mechanism used to have an editable sequence number.
Be careful of how you use this regarding the prefixes. More info in the
docstring of _get_last_sequence.
"""
_name = 'sequence.mixin'
_description = "Automatic sequence"
_sequence_field = "name"
_sequence_monthly_regex = r'^(?P<prefix1>.*?)(?P<year>\d{4})(?P<prefix2>\D*?)(?P<month>\d{2})(?P<prefix3>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
_sequence_yearly_regex = r'^(?P<prefix1>.*?)(?P<year>\d{4})(?P<prefix2>\D+?)(?P<seq>\d*)(?P<suffix>\D*?)$'
_sequence_fixed_regex = r'^(?P<prefix1>.*?)(?P<seq>\d*)(?P<suffix>\D*?)$'
@api.model
def _deduce_sequence_number_reset(self, name):
"""Detect if the used sequence resets yearly, montly or never.
:param name: the sequence that is used as a reference to detect the resetting
periodicity. Typically, it is the last before the one you want to give a
sequence.
"""
def _check_grouping(grouping, optional=None, required=None):
sequence_dict = sequence.groupdict()
return all(key in sequence_dict for key in (optional or [])) and all(sequence_dict.get(key) for key in (required or []))
if not name:
return False
sequence = re.match(self._sequence_monthly_regex, name)
if sequence and _check_grouping(sequence, ['prefix1', 'prefix2', 'prefix3', 'seq', 'suffix'], ['year', 'month']) and 2000 <= int(sequence.group('year')) <= 2100 and 0 < int(sequence.group('month')) <= 12:
return 'month'
sequence = re.match(self._sequence_yearly_regex, name)
if sequence and _check_grouping(sequence, ['prefix1', 'prefix2', 'seq', 'suffix'], ['year']) and 2000 <= int(sequence.group('year')) <= 2100:
return 'year'
sequence = re.match(self._sequence_fixed_regex, name)
if sequence and _check_grouping(sequence, ['prefix1', 'seq', 'suffix']):
return 'never'
raise ValidationError(_('The sequence regex should at least contain the prefix1, seq and suffix grouping keys. For instance:\n^(?P<prefix1>.*?)(?P<seq>\d*)(?P<suffix>\D*?)$'))
def _get_last_sequence_domain(self, relaxed=False):
"""Get the sql domain to retreive the previous sequence number.
This function should be overriden by models heriting from this mixin.
:param relaxed: see _get_last_sequence.
:returns: tuple(where_string, where_params): with
where_string: the entire SQL WHERE clause as a string.
where_params: a dictionary containing the parameters to substitute
at the execution of the query.
"""
self.ensure_one()
return "", {}
def _get_starting_sequence(self):
"""Get a default sequence number.
This function should be overriden by models heriting from this mixin
This number will be incremented so you probably want to start the sequence at 0.
:return: string to use as the default sequence to increment
"""
self.ensure_one()
return "00000000"
def _get_last_sequence(self, relaxed=False):
"""Retrieve the previous sequence.
This is done by taking the number with the greatest alphabetical value within
the domain of _get_last_sequence_domain. This means that the prefix has a
huge importance.
For instance, if you have INV/2019/0001 and INV/2019/0002, when you rename the
last one to FACT/2019/0001, one might expect the next number to be
FACT/2019/0002 but it will be INV/2019/0002 (again) because INV > FACT.
Therefore, changing the prefix might not be convenient during a period, and
would only work when the numbering makes a new start (domain returns by
_get_last_sequence_domain is [], i.e: a new year).
:param field_name: the field that contains the sequence.
:param relaxed: this should be set to True when a previous request didn't find
something without. This allows to find a pattern from a previous period, and
try to adapt it for the new period.
:return: the string of the previous sequence or None if there wasn't any.
"""
self.ensure_one()
if self._sequence_field not in self._fields or not self._fields[self._sequence_field].store:
raise ValidationError(_('%s is not a stored field') % self._sequence_field)
where_string, param = self._get_last_sequence_domain(relaxed)
if self.id or self.id.origin:
where_string += " AND id != %(id)s "
param['id'] = self.id or self.id.origin
query = "SELECT {field} FROM {table} {where_string} ORDER BY {field} DESC LIMIT 1 FOR UPDATE".format(table=self._table, where_string=where_string, field=self._sequence_field)
self.flush([self._sequence_field])
self.env.cr.execute(query, param)
return (self.env.cr.fetchone() or [None])[0]
def _set_next_sequence(self):
"""Set the next sequence.
This method ensures that the field is set both in the ORM and in the database.
This is necessary because we use a database query to get the previous sequence,
and we need that query to always be executed on the latest data.
:param field_name: the field that contains the sequence.
"""
self.ensure_one()
last_sequence = self._get_last_sequence() or self._get_starting_sequence()
sequence = re.match(self._sequence_fixed_regex, last_sequence)
value = ("{prefix}{seq:0%sd}{suffix}" % len(sequence.group('seq'))).format(
prefix=sequence.group('prefix1'),
seq=int(sequence.group('seq') or 0) + 1,
suffix=sequence.group('suffix'),
)
self[self._sequence_field] = value
self.flush([self._sequence_field])
@@ -128,6 +128,7 @@ access_account_payment_register,access.account.payment.register,model_account_pa
access_account_bank_statement_closebalance,access.account.bank.statement.closebalance,model_account_bank_statement_closebalance,account.group_account_user,1,1,1,0
access_account_accrual_accounting_wizard,access.account.accrual.accounting.wizard,model_account_accrual_accounting_wizard,account.group_account_user,1,1,1,0
access_account_unreconcile,access.account.unreconcile,model_account_unreconcile,account.group_account_user,1,1,1,0
access_account_resequence,access.account.resequence.wizard,model_account_resequence_wizard,account.group_account_user,1,1,1,0
access_validate_account_move,access.validate.account.move,model_validate_account_move,account.group_account_invoice,1,1,1,0
access_cash_box_out,access.cash.box.out,model_cash_box_out,account.group_account_user,1,1,1,0
access_account_move_reversal,access.account.move.reversal,model_account_move_reversal,account.group_account_invoice,1,1,1,0
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
128
129
130
131
132
133
134
@@ -0,0 +1,32 @@
odoo.define('account.ShowResequenceRenderer', function (require) {
"use strict";
const { Component } = owl;
const { useState } = owl.hooks;
const AbstractFieldOwl = require('web.AbstractFieldOwl');
const field_registry = require('web.field_registry');
class ChangeLine extends Component { }
ChangeLine.template = 'account.ResequenceChangeLine';
ChangeLine.props = ["changeLine", 'ordering'];
class ShowResequenceRenderer extends AbstractFieldOwl {
constructor(...args) {
super(...args);
this.data = this.value ? JSON.parse(this.value) : {
changeLines: [],
ordering: 'date',
};
}
async willUpdateProps(nextProps) {
await super.willUpdateProps(nextProps);
Object.assign(this.data, JSON.parse(this.value));
}
}
ShowResequenceRenderer.template = 'account.ResequenceRenderer';
ShowResequenceRenderer.components = { ChangeLine }
field_registry.add('account_resequence_widget', ShowResequenceRenderer);
return ShowResequenceRenderer;
});
@@ -3,3 +3,17 @@ $o-account-main-table-borders-padding: 3px;
$o-account-light-border: 1px solid #bbb;
$o-account-initial-line-background: #f0f0f0;
$o-account-info-color: #44c;
@keyframes animate-red {
0% {
color: red;
}
100% {
color: inherit;
}
}
.animate {
animation: animate-red 1s ease;
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<templates>
<div t-name="account.ResequenceRenderer" owl="1" class="d-block">
<table t-if="data.changeLines.length" class="table table-sm">
<thead><tr>
<th>Date</th>
<th>Before</th>
<th>After</th>
</tr></thead>
<tbody t-foreach="data.changeLines" t-as="changeLine" t-key="changeLine.current_name">
<ChangeLine changeLine="changeLine" ordering="data.ordering"/>
</tbody>
</table>
</div>
<t t-name="account.ResequenceChangeLine" owl="1">
<tr>
<td t-esc="props.changeLine.date"/>
<td t-esc="props.changeLine.current_name"/>
<td t-if="props.ordering == 'keep'" t-esc="props.changeLine.new_by_name" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
<td t-else="" t-esc="props.changeLine.new_by_date" t-attf-class="{{ props.changeLine.new_by_name != props.changeLine.new_by_date ? 'animate' : ''}}"/>
</tr>
</t>
</templates>
+147 -8
View File
@@ -6,6 +6,8 @@ from odoo import fields
from odoo.exceptions import ValidationError, UserError
from dateutil.relativedelta import relativedelta
from functools import reduce
import json
@tagged('post_install', '-at_install')
@@ -320,18 +322,155 @@ class TestAccountMove(AccountTestInvoicingCommon):
# You can remove journal items if the related journal entry is still balanced.
self.test_move.line_ids.unlink()
def test_misc_unique_sequence_number(self):
''' Ensure two journal entries can't share the same name when using the same sequence. '''
def test_journal_sequence(self):
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
self.test_move.post()
self.assertEqual(self.test_move.name, 'MISC/2016/01/0001')
# Edit the sequence to force the next move to get the same name.
self.test_move.journal_id\
.sequence_id.date_range_ids\
.filtered(lambda seq: seq.date_from == fields.Date.from_string('2016-01-01')).number_next -= 1
copy1 = self.test_move.copy()
self.assertEqual(copy1.name, '/')
copy1.post()
self.assertEqual(copy1.name, 'MISC/2016/01/0002')
test_move2 = self.test_move.copy()
copy2 = self.test_move.copy()
new_journal = self.test_move.journal_id.copy()
new_journal.code = "MISC2"
copy2.journal_id = new_journal
self.assertEqual(copy2.name, 'MISC2/2016/01/0001')
with Form(copy2) as move_form: # It is editable in the form
move_form.name = 'MyMISC/2099/0001'
copy2.post()
self.assertEqual(copy2.name, 'MyMISC/2099/0001')
copy3 = copy2.copy()
self.assertEqual(copy3.name, '/')
with self.assertRaises(AssertionError):
with Form(copy2) as move_form: # It is not editable in the form
move_form.name = 'MyMISC/2099/0002'
copy3.post()
self.assertEqual(copy3.name, 'MyMISC/2099/0002')
copy3.name = 'MISC2/2016/00002'
copy4 = copy2.copy()
copy4.post()
self.assertEqual(copy4.name, 'MyMISC/2099/0002')
copy5 = copy2.copy()
copy5.date = '2021-02-02'
copy5.post()
self.assertEqual(copy5.name, 'MyMISC/2021/0001')
copy5.name = 'N\'importe quoi?'
copy6 = copy5.copy()
copy6.post()
self.assertEqual(copy6.name, '1N\'importe quoi?')
def test_journal_sequence_format(self):
"""Test different format of sequences and what it becomes on another period"""
sequences = [
('JRNL/2016/00001', 'JRNL/2016/00002', 'JRNL/2016/00003', 'JRNL/2017/00001'),
('1234567', '1234568', '1234569', '1234570'),
('20190910', '20190911', '20190912', '20190913'),
('2019-0910', '2019-0911', '2019-0912', '2017-0001'),
('201909-10', '201909-11', '201604-01', '201703-01'),
('JRNL/2016/00001suffix', 'JRNL/2016/00002suffix', 'JRNL/2016/00003suffix', 'JRNL/2017/00001suffix'),
]
other_moves = self.env['account.move'].search([('journal_id', '=', self.test_move.journal_id.id)]) - self.test_move
other_moves.unlink() # Do not interfere when trying to get the highest name for new periods
init_move = self.test_move
next_move = init_move.copy()
next_move_month = init_move.copy()
next_move_year = init_move.copy()
init_move.date = '2016-03-12'
next_move.date = '2016-03-12'
next_move_month.date = '2016-04-12'
next_move_year.date = '2017-03-12'
next_moves = (next_move + next_move_month + next_move_year)
next_moves.post()
for sequence_init, sequence_next, sequence_next_month, sequence_next_year in sequences:
init_move.name = sequence_init
next_moves.name = False
next_moves._compute_name()
self.assertEqual(next_move.name, sequence_next)
self.assertEqual(next_move_month.name, sequence_next_month)
self.assertEqual(next_move_year.name, sequence_next_year)
def test_journal_override_sequence_regex(self):
other_moves = self.env['account.move'].search([('journal_id', '=', self.test_move.journal_id.id)]) - self.test_move
other_moves.unlink() # Do not interfere when trying to get the highest name for new periods
self.test_move.name = '00000876-G 0002'
next = self.test_move.copy()
next.post()
self.assertEqual(next.name, '00000876-G 0003') # Wait, I didn't want this!
next.journal_id.sequence_override_regex = r'^(?P<prefix1>)(?P<seq>\d*)(?P<suffix>.*)$'
next.name = '/'
next._compute_name()
self.assertEqual(next.name, '00000877-G 0002') # Pfew, better!
def test_journal_sequence_ordering(self):
self.test_move.name = 'XMISC/2016/00001'
copies = reduce((lambda x, y: x+y), [self.test_move.copy() for i in range(6)])
copies[0].date = '2019-03-05'
copies[1].date = '2019-03-06'
copies[2].date = '2019-03-07'
copies[3].date = '2019-03-04'
copies[4].date = '2019-03-05'
copies[5].date = '2019-03-05'
# that entry is actualy the first one of the period, so it already has a name
# set it to '/' so that it is recomputed at post to be ordered correctly.
copies[0].name = '/'
copies.post()
# Ordered by date
self.assertEqual(copies[0].name, 'XMISC/2019/00002')
self.assertEqual(copies[1].name, 'XMISC/2019/00005')
self.assertEqual(copies[2].name, 'XMISC/2019/00006')
self.assertEqual(copies[3].name, 'XMISC/2019/00001')
self.assertEqual(copies[4].name, 'XMISC/2019/00003')
self.assertEqual(copies[5].name, 'XMISC/2019/00004')
# Can't have twice the same name
with self.assertRaises(ValidationError):
test_move2.post()
copies[0].name = 'XMISC/2019/00001'
# Lets remove the order by date
copies[0].name = 'XMISC/2019/10001'
copies[1].name = 'XMISC/2019/10002'
copies[2].name = 'XMISC/2019/10003'
copies[3].name = 'XMISC/2019/10004'
copies[4].name = 'XMISC/2019/10005'
copies[5].name = 'XMISC/2019/10006'
copies[4].with_context(force_delete=True).unlink()
copies[5].button_draft()
wizard = Form(self.env['account.resequence.wizard'].with_context(active_ids=set(copies.ids) - set(copies[4].ids), active_model='account.move'))
new_values = json.loads(wizard.new_values)
self.assertEqual(new_values[str(copies[0].id)]['new_by_date'], 'XMISC/2019/10002')
self.assertEqual(new_values[str(copies[0].id)]['new_by_name'], 'XMISC/2019/10001')
self.assertEqual(new_values[str(copies[1].id)]['new_by_date'], 'XMISC/2019/10004')
self.assertEqual(new_values[str(copies[1].id)]['new_by_name'], 'XMISC/2019/10002')
self.assertEqual(new_values[str(copies[2].id)]['new_by_date'], 'XMISC/2019/10005')
self.assertEqual(new_values[str(copies[2].id)]['new_by_name'], 'XMISC/2019/10003')
self.assertEqual(new_values[str(copies[3].id)]['new_by_date'], 'XMISC/2019/10001')
self.assertEqual(new_values[str(copies[3].id)]['new_by_name'], 'XMISC/2019/10004')
self.assertEqual(new_values[str(copies[5].id)]['new_by_date'], 'XMISC/2019/10003')
self.assertEqual(new_values[str(copies[5].id)]['new_by_name'], 'XMISC/2019/10005')
wizard.save().resequence()
self.assertEqual(copies[3].state, 'posted')
self.assertEqual(copies[5].name, 'XMISC/2019/10005')
self.assertEqual(copies[5].state, 'draft')
def test_add_followers_on_post(self):
# Add some existing partners, some from another company
@@ -1092,33 +1092,6 @@ class TestAccountMoveInInvoiceOnchanges(AccountTestInvoicingCommon):
'amount_total': 208.01,
})
def test_in_invoice_line_onchange_sequence_number_1(self):
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0001',
'invoice_sequence_number_next_prefix': 'BILL/2019/',
}])
move_form = Form(self.invoice)
move_form.invoice_sequence_number_next = '0042'
move_form.save()
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0042',
'invoice_sequence_number_next_prefix': 'BILL/2019/',
}])
self.invoice.post()
self.assertRecordValues(self.invoice, [{'name': 'BILL/2019/0042'}])
values = {
'invoice_date': self.invoice.invoice_date,
}
invoice_copy = self.invoice.copy(default=values)
invoice_copy.post()
self.assertRecordValues(invoice_copy, [{'name': 'BILL/2019/0043'}])
def test_in_invoice_onchange_past_invoice_1(self):
copy_invoice = self.invoice.copy()
@@ -804,33 +804,6 @@ class TestAccountMoveInRefundOnchanges(AccountTestInvoicingCommon):
'amount_total': 208.01,
})
def test_in_refund_line_onchange_sequence_number_1(self):
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0001',
'invoice_sequence_number_next_prefix': 'RBILL/2019/',
}])
move_form = Form(self.invoice)
move_form.invoice_sequence_number_next = '0042'
move_form.save()
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0042',
'invoice_sequence_number_next_prefix': 'RBILL/2019/',
}])
self.invoice.post()
self.assertRecordValues(self.invoice, [{'name': 'RBILL/2019/0042'}])
values = {
'invoice_date': self.invoice.invoice_date,
}
invoice_copy = self.invoice.copy(default=values)
invoice_copy.post()
self.assertRecordValues(invoice_copy, [{'name': 'RBILL/2019/0043'}])
def test_in_refund_onchange_past_invoice_1(self):
copy_invoice = self.invoice.copy()
@@ -1501,33 +1501,6 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
'amount_total': 260.01,
})
def test_out_invoice_onchange_sequence_number_1(self):
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0001',
'invoice_sequence_number_next_prefix': 'INV/2019/',
}])
move_form = Form(self.invoice)
move_form.invoice_sequence_number_next = '0042'
move_form.save()
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0042',
'invoice_sequence_number_next_prefix': 'INV/2019/',
}])
self.invoice.post()
self.assertRecordValues(self.invoice, [{'name': 'INV/2019/0042'}])
values = {
'invoice_date': self.invoice.invoice_date,
}
invoice_copy = self.invoice.copy(default=values)
invoice_copy.post()
self.assertRecordValues(invoice_copy, [{'name': 'INV/2019/0043'}])
def test_out_invoice_create_refund(self):
self.invoice.post()
@@ -1571,7 +1544,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
], {
**self.move_vals,
'invoice_payment_term_id': None,
'name': '/',
'name': 'RINV/2019/02/0001',
'date': move_reversal.date,
'state': 'draft',
'ref': 'Reversal of: %s, %s' % (self.invoice.name, move_reversal.reason),
@@ -2335,7 +2308,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
{
**self.term_line_vals_1,
'currency_id': self.currency_data['currency'].id,
'name': 'INV/2017/0001',
'name': 'INV/2017/01/0001',
'amount_currency': 1410.0,
'debit': 705.0,
'credit': 0.0,
@@ -2345,7 +2318,7 @@ class TestAccountMoveOutInvoiceOnchanges(AccountTestInvoicingCommon):
**self.move_vals,
'currency_id': self.currency_data['currency'].id,
'date': fields.Date.from_string('2017-01-01'),
'invoice_payment_ref': 'INV/2017/0001',
'invoice_payment_ref': 'INV/2017/01/0001',
})
accrual_lines = move.invoice_line_ids.mapped('matched_debit_ids.debit_move_id.move_id.line_ids').sorted('date')
@@ -804,33 +804,6 @@ class TestAccountMoveOutRefundOnchanges(AccountTestInvoicingCommon):
'amount_total': 260.01,
})
def test_out_refund_line_onchange_sequence_number_1(self):
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0001',
'invoice_sequence_number_next_prefix': 'RINV/2019/',
}])
move_form = Form(self.invoice)
move_form.invoice_sequence_number_next = '0042'
move_form.save()
self.assertRecordValues(self.invoice, [{
'invoice_sequence_number_next': '0042',
'invoice_sequence_number_next_prefix': 'RINV/2019/',
}])
self.invoice.post()
self.assertRecordValues(self.invoice, [{'name': 'RINV/2019/0042'}])
values = {
'invoice_date': self.invoice.invoice_date,
}
invoice_copy = self.invoice.copy(default=values)
invoice_copy.post()
self.assertRecordValues(invoice_copy, [{'name': 'RINV/2019/0043'}])
def test_out_refund_create_1(self):
# Test creating an account_move with the least information.
move = self.env['account.move'].create({
@@ -22,7 +22,7 @@ class TestReconciliationMatchingRules(AccountTestCommon):
cls.invoice_line_1 = cls._create_invoice_line(100, cls.partner_1, 'out_invoice')
cls.invoice_line_2 = cls._create_invoice_line(200, cls.partner_1, 'out_invoice')
cls.invoice_line_3 = cls._create_invoice_line(300, cls.partner_1, 'in_refund')
cls.invoice_line_3.move_id.name = "RBILL/2018/0013" # Without demo data, avoid to match with the first invoice
cls.invoice_line_3.move_id.name = "RBILL/2019/09/0013" # Without demo data, avoid to match with the first invoice
cls.invoice_line_4 = cls._create_invoice_line(1000, cls.partner_2, 'in_invoice')
current_assets_account = cls.env['account.account'].search([
@@ -65,7 +65,7 @@ class TestReconciliationMatchingRules(AccountTestCommon):
})
cls.bank_line_1 = cls.env['account.bank.statement.line'].create({
'statement_id': cls.bank_st.id,
'name': 'invoice %s-%s' % (invoice_number.split('/')[1], invoice_number.split('/')[2]),
'name': 'invoice %s-%s-%s' % (invoice_number.split('/')[1], invoice_number.split('/')[2], invoice_number.split('/')[3]),
'partner_id': cls.partner_1.id,
'amount': '100',
'sequence': 1,
@@ -99,8 +99,7 @@ class TestReconciliationMatchingRules(AccountTestCommon):
@classmethod
def _create_invoice_line(cls, amount, partner, type):
''' Create an invoice on the fly.'''
invoice_form = Form(cls.env['account.move'].with_context(default_type=type))
invoice_form.invoice_date = fields.Date.from_string('2019-09-01')
invoice_form = Form(cls.env['account.move'].with_context(default_type=type, default_invoice_date='2019-09-01', default_date='2019-09-01'))
invoice_form.partner_id = partner
with invoice_form.invoice_line_ids.new() as invoice_line_form:
invoice_line_form.name = 'xxxx'
+1
View File
@@ -20,6 +20,7 @@
<link rel="stylesheet" type="text/scss" href="/account/static/src/scss/account_activity.scss"/>
<script type="text/javascript" src="/account/static/src/js/account_payment_field.js"></script>
<script type="text/javascript" src="/account/static/src/js/account_resequence_field.js"></script>
<script type="text/javascript" src="/account/static/src/js/mail_activity.js"></script>
<script type="text/javascript" src="/account/static/src/js/tax_group.js"></script>
<script type="text/javascript" src="/account/static/src/js/aml_preview.js"></script>
+9 -21
View File
@@ -561,6 +561,8 @@
<!-- Invisible fields -->
<field name="id" invisible="1"/>
<field name="show_name_warning" invisible="1"/>
<field name="posted_before" invisible="1"/>
<field name="type" invisible="1"/>
<field name="payment_state" invisible="1" force_save="1"/>
<field name="invoice_filter_type_domain" invisible="1"/>
@@ -570,37 +572,23 @@
<field name="invoice_has_outstanding" invisible="1"/>
<field name="invoice_sent" invisible="1"/>
<field name="invoice_sequence_number_next_prefix" invisible="1"/>
<field name="invoice_sequence_number_next" invisible="1"/>
<field name="invoice_has_matching_suspense_amount" invisible="1"/>
<field name="has_reconciled_entries" invisible="1"/>
<field name="restrict_mode_hash_table" invisible="1"/>
<div>
<div class="oe_title">
<!-- Invoice draft header -->
<span class="o_form_label"><field name="type" attrs="{'invisible': ['|', ('type', '=', 'entry'), ('state', '=', 'draft')]}" readonly="1" nolabel="1"/></span>
<span class="o_form_label"><field name="type" attrs="{'invisible': [('type', '=', 'entry')]}" readonly="1" nolabel="1"/></span>
<h1>
<span attrs="{'invisible': ['|', '|', ('type', '!=', 'out_invoice'), ('state', '!=', 'draft'), ('name', '!=', '/')]}">Draft Invoice</span>
<span attrs="{'invisible': ['|', '|', ('type', '!=', 'out_refund'), ('state', '!=', 'draft'), ('name', '!=', '/')]}">Draft Credit Note</span>
<span attrs="{'invisible': ['|', '|', ('type', '!=', 'in_invoice'), ('state', '!=', 'draft'), ('name', '!=', '/')]}">Draft Bill</span>
<span attrs="{'invisible': ['|', '|', ('type', '!=', 'in_refund'), ('state', '!=', 'draft'), ('name', '!=', '/')]}">Draft Refund</span>
<span attrs="{'invisible': ['|', '|', ('type', '!=', 'out_receipt'), ('state', '!=', 'draft'), ('name', '!=', '/')]}">Draft Sales Receipt</span>
<span attrs="{'invisible': ['|', '|', ('type', '!=', 'in_receipt'), ('state', '!=', 'draft'), ('name', '!=', '/')]}">Draft Purchase Receipt</span>
<span attrs="{'invisible': ['|', ('state', '!=', 'draft'), ('name', '!=', '/')]}">Draft</span>
</h1>
<!-- Select next number header (only invoices) -->
<span class="o_form_label" attrs="{'invisible': [('invoice_sequence_number_next_prefix', '=', False)]}">First Number:</span>
<!-- Number -->
<h1 class="mt0">
<field name="name" readonly="True" attrs="{'invisible':[('name', '=', '/')]}"/>
<span class="text-warning" attrs="{'invisible': [('show_name_warning', '=', False)]}">The current highest number is <field name="highest_name"/>. You might want to put a higher number here.</span>
<h1 class="mt0" attrs="{'invisible':[('name', '=', '/'), ('posted_before', '=', False)]}">
<field name="name" attrs="{'readonly': [('state', '!=', 'draft')]}" placeholder="JRNL/2016/00001"/>
</h1>
<!-- Select next number header (only invoices) -->
<div attrs="{'invisible': [('invoice_sequence_number_next_prefix', '=', False)]}">
<field name="invoice_sequence_number_next_prefix" class="oe_inline"/>
<field name="invoice_sequence_number_next" class="oe_inline"/>
</div>
</div>
<group>
<group id="header_left_group">
@@ -657,7 +645,7 @@
groups="account.group_account_readonly"
options="{'no_create': True}"
domain="[('type', '=?', invoice_filter_type_domain)]"
attrs="{'readonly': [('name', '!=', '/')]}"/>
attrs="{'readonly': [('posted_before', '=', True)]}"/>
<field name="company_id" groups="base.group_multi_company"/>
+1 -16
View File
@@ -285,22 +285,7 @@
<group>
<group>
<field name="code"/>
<label for="sequence_number_next"/>
<div>
<field name="sequence_number_next" style="padding-right: 1.0em"/>
<field name="sequence_id" required="0"
attrs="{'readonly': 1}" groups="base.group_no_one"/>
</div>
<field name="refund_sequence"
attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}"
groups="base.group_no_one"/>
<label for="refund_sequence_number_next"
attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)]}"/>
<div attrs="{'invisible': ['|',('type', 'not in', ['sale', 'purchase']), ('refund_sequence', '!=', True)]}">
<field name="refund_sequence_number_next" style="padding-right: 1.0em"/>
<field name="refund_sequence_id" required="0"
attrs="{'readonly': 1}" groups="base.group_no_one"/>
</div>
<field name="refund_sequence" attrs="{'invisible': [('type', 'not in', ['sale', 'purchase'])]}"/>
</group>
<group>
<field name="default_debit_account_id" options="{'no_create': True}" domain="[('deprecated', '=', False)]" groups="account.group_account_readonly"/>
+1
View File
@@ -10,6 +10,7 @@ from . import account_move_reversal
from . import account_report_common
from . import account_report_common_journal
from . import account_report_print_journal
from . import account_resequence
from . import setup_wizards
from . import wizard_tax_adjustments
from . import account_invoice_send
+164
View File
@@ -0,0 +1,164 @@
# -*- 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']
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="account_resequence_view" model="ir.ui.view">
<field name="name">Re-sequence Journal Entries</field>
<field name="model">account.resequence.wizard</field>
<field name="arch" type="xml">
<form string="Re-Sequence">
<field name="move_ids" invisible="1"/>
<field name="new_values" invisible="1"/>
<field name="sequence_number_reset" invisible="1"/>
<group>
<group>
<field name="ordering" widget="radio"/>
</group>
<group>
<field name="first_name"/>
</group>
</group>
<label for="preview_moves" string="Preview Modifications"/>
<field name="preview_moves" widget="account_resequence_widget"/>
<footer>
<button string="Confirm" name="resequence" type="object" default_focus="1" class="btn-primary"/>
<button string="Cancel" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_account_resequence" model="ir.actions.act_window">
<field name="name">Resequence</field>
<field name="res_model">account.resequence.wizard</field>
<field name="view_mode">form</field>
<field name="view_id" ref="account_resequence_view"/>
<field name="target">new</field>
<field name="groups_id" eval="[(4, ref('base.group_no_one'))]"/>
<field name="binding_model_id" ref="account.model_account_move" />
<field name="binding_view_types">list</field>
</record>
</data>
</odoo>
@@ -9,7 +9,7 @@ class ValidateAccountMove(models.TransientModel):
def validate_move(self):
context = dict(self._context or {})
moves = self.env['account.move'].browse(context.get('active_ids'))
move_to_post = moves.filtered(lambda m: m.state == 'draft').sorted(lambda m: (m.date, m.ref or '', m.id))
move_to_post = moves.filtered(lambda m: m.state == 'draft')
if not move_to_post:
raise UserError(_('There are no journal items in the draft state to post.'))
move_to_post.post()
+3 -2
View File
@@ -49,11 +49,12 @@ class FinancialYearOpeningWizard(models.TransientModel):
wiz.company_id.write({
'fiscalyear_last_day': vals.get('fiscalyear_last_day') or wiz.company_id.fiscalyear_last_day,
'fiscalyear_last_month': vals.get('fiscalyear_last_month') or wiz.company_id.fiscalyear_last_month,
'account_opening_date': vals.get('opening_date'),
'account_opening_date': vals.get('opening_date') or wiz.company_id.account_opening_date,
})
wiz.company_id.account_opening_move_id.write({
'date': fields.Date.from_string(vals.get('opening_date')) - timedelta(days=1),
'date': fields.Date.from_string(vals.get('opening_date') or wiz.company_id.account_opening_date) - timedelta(days=1),
})
vals.pop('opening_date', None)
vals.pop('fiscalyear_last_day', None)
vals.pop('fiscalyear_last_month', None)
@@ -5,42 +5,27 @@
<record id="demo_refund_invoice_3" model="account.move.reversal">
<field name="reason">Mercadería defectuosa</field>
<field name="refund_method">refund</field>
<field name="l10n_latam_use_documents" eval="True"/>
<field name="move_ids" eval="[(4, ref('demo_invoice_3'), 0)]"/>
</record>
<function model="account.move.reversal" name="_onchange_move_id" eval="[ref('demo_refund_invoice_3')]"/>
<function model="account.move.reversal" name="_onchange_l10n_latam_document_number" eval="[ref('demo_refund_invoice_3')]"/>
<function model="account.move.reversal" name="reverse_moves" eval="[ref('demo_refund_invoice_3')]"/>
<!-- Create draft refund for invoice 4 -->
<record id="demo_refund_invoice_4" model="account.move.reversal">
<field name="reason">Venta cancelada</field>
<field name="refund_method">cancel</field>
<field name="l10n_latam_use_documents" eval="True"/>
<field name="move_ids" eval="[(4, ref('demo_invoice_4'), 0)]"/>
</record>
<function model="account.move.reversal" name="_onchange_move_id" eval="[ref('demo_refund_invoice_4')]"/>
<function model="account.move.reversal" name="_onchange_l10n_latam_document_number" eval="[ref('demo_refund_invoice_4')]"/>
<function model="account.move.reversal" name="reverse_moves" eval="[ref('demo_refund_invoice_4')]"/>
<!-- Create cancel refund for expo invoice 16 (las nc/nd expo invoice no requiere parametro permiso existennte, por eso agregamos este ejemplo) -->
<record id="demo_refund_invoice_16" model="account.move.reversal">
<field name="reason">Venta cancelada</field>
<field name="refund_method">cancel</field>
<field name="l10n_latam_use_documents" eval="True"/>
<field name="move_ids" eval="[(4, ref('demo_invoice_16'), 0)]"/>
</record>
<function model="account.move.reversal" name="_onchange_move_id" eval="[ref('demo_refund_invoice_16')]"/>
<function model="account.move.reversal" name="_onchange_l10n_latam_document_number" eval="[ref('demo_refund_invoice_16')]"/>
<function model="account.move.reversal" name="reverse_moves" eval="[ref('demo_refund_invoice_16')]"/>
</odoo>
@@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo noupdate="True">
<!-- we add l10n_latam_document_number on on a separete line because we need l10n_latam_document_type_id to be auto assigned so that account.move.name can be computed with the _inverse_l10n_latam_document_number -->
<!-- Invoice from gritti support service, auto fiscal position set VAT Not Applicable -->
<record id="demo_sup_invoice_1" model="account.move">
<field name="name">FA-C 0001-00000008</field>
<field name="partner_id" ref="res_partner_gritti_agrimensura"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -23,6 +22,7 @@
<!-- Invoice from Foreign with vat 21, 27 and 10,5 -->
<record id="demo_sup_invoice_2" model="account.move">
<field name="name">FA-A 0002-00000123</field>
<field name="partner_id" ref="res_partner_foreign"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -38,6 +38,7 @@
<!-- Invoice from Foreign with vat zero and 21 -->
<record id="demo_sup_invoice_3" model="account.move">
<field name="name">FA-A 0003-00000312</field>
<field name="partner_id" ref="res_partner_foreign"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -52,6 +53,7 @@
<!-- Invoice to Foreign with vat exempt and 21 -->
<record id="demo_sup_invoice_4" model="account.move">
<field name="name">FA-A 0001-00000200</field>
<field name="partner_id" ref="res_partner_foreign"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -66,6 +68,7 @@
<!-- Invoice to Foreign with all type of taxes -->
<record id="demo_sup_invoice_5" model="account.move">
<field name="name">FA-A 0001-00000222</field>
<field name="partner_id" ref="res_partner_foreign"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -84,6 +87,7 @@
<!-- Service Import to Odoo, fiscal position changes tax not correspond -->
<record id="demo_sup_invoice_6" model="account.move">
<field name="name">FA-I 0001-00000333</field>
<field name="partner_id" ref="res_partner_odoo"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -97,6 +101,7 @@
<!-- Similar to last one but with line that have tax not correspond with negative amount -->
<record id="demo_sup_invoice_7" model="account.move">
<field name="name">FA-I 0001-00000334</field>
<field name="partner_id" ref="res_partner_odoo"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -111,6 +116,7 @@
<!-- Import Cleareance -->
<record id="demo_despacho_1" model="account.move">
<field name="name">DI 16052IC04000605L</field>
<field name="partner_id" ref="l10n_ar.partner_afip"/>
<field name="invoice_user_id" ref="base.user_demo"/>
<field name="invoice_payment_term_id" ref="account.account_payment_term_end_following_month"/>
@@ -216,31 +222,6 @@
<field name="account_id" model="account.move.line" eval="obj().env.ref('product.product_category_all').property_account_income_categ_id.id"/>
</record>
<record id="demo_sup_invoice_1" model="account.move">
<field name="l10n_latam_document_number">0001-00000008</field>
</record>
<record id="demo_sup_invoice_2" model="account.move">
<field name="l10n_latam_document_number">0002-00000123</field>
</record>
<record id="demo_sup_invoice_3" model="account.move">
<field name="l10n_latam_document_number">0003-00000312</field>
</record>
<record id="demo_sup_invoice_4" model="account.move">
<field name="l10n_latam_document_number">0001-00000200</field>
</record>
<record id="demo_sup_invoice_5" model="account.move">
<field name="l10n_latam_document_number">0001-00000222</field>
</record>
<record id="demo_sup_invoice_6" model="account.move">
<field name="l10n_latam_document_number">0001-00000333</field>
</record>
<record id="demo_sup_invoice_7" model="account.move">
<field name="l10n_latam_document_number">0001-00000334</field>
</record>
<record id="demo_despacho_1" model="account.move">
<field name="l10n_latam_document_number">16052IC04000605L</field>
</record>
<function model="account.move" name="_onchange_partner_id" context="{'check_move_validity': False}">
<value eval="[ref('demo_sup_invoice_1')]"/>
</function>
@@ -5,30 +5,18 @@
<record id="demo_sup_refund_invoice_3" model="account.move.reversal">
<field name="reason">Mercadería defectuosa</field>
<field name="refund_method">refund</field>
<field name="l10n_latam_document_number">0001-01234567</field>
<field name="l10n_latam_use_documents" eval="True"/>
<field name="move_ids" eval="[(4, ref('demo_sup_invoice_3'), 0)]"/>
</record>
<function model="account.move.reversal" name="_onchange_move_id" eval="[ref('demo_sup_refund_invoice_3')]"/>
<function model="account.move.reversal" name="_onchange_l10n_latam_document_number" eval="[ref('demo_sup_refund_invoice_3')]"/>
<function model="account.move.reversal" name="reverse_moves" eval="[ref('demo_sup_refund_invoice_3')]"/>
<!-- Create draft refund for invoice 4 -->
<record id="demo_sup_refund_invoice_4" model="account.move.reversal">
<field name="reason">Venta cancelada</field>
<field name="l10n_latam_document_number">0001-01234566</field>
<field name="l10n_latam_use_documents" eval="True"/>
<field name="refund_method">cancel</field>
<field name="move_ids" eval="[(4, ref('demo_sup_invoice_4'), 0)]"/>
</record>
<function model="account.move.reversal" name="_onchange_move_id" eval="[ref('demo_sup_refund_invoice_4')]"/>
<function model="account.move.reversal" name="_onchange_l10n_latam_document_number" eval="[ref('demo_sup_refund_invoice_4')]"/>
<function model="account.move.reversal" name="reverse_moves" eval="[ref('demo_sup_refund_invoice_4')]"/>
</odoo>
-12
View File
@@ -111,18 +111,6 @@ class AccountMove(models.Model):
'message': _('Please configure the AFIP Responsibility for "%s" in order to continue') % (
self.partner_id.name)}}
def _get_document_type_sequence(self):
""" Return the match sequences for the given journal and invoice """
self.ensure_one()
if self.journal_id.l10n_latam_use_documents and self.l10n_latam_country_code == 'AR':
if self.journal_id.l10n_ar_share_sequences:
return self.journal_id.l10n_ar_sequence_ids.filtered(
lambda x: x.l10n_ar_letter == self.l10n_latam_document_type_id.l10n_ar_letter)
res = self.journal_id.l10n_ar_sequence_ids.filtered(
lambda x: x.l10n_latam_document_type_id == self.l10n_latam_document_type_id)
return res
return super()._get_document_type_sequence()
@api.onchange('partner_id')
def _onchange_partner_journal(self):
""" This method is used when the invoice is created from the sale or subscription """
@@ -15,15 +15,6 @@
</tree>
</field>
</xpath>
<label for="sequence_number_next" position="attributes">
<attribute name="attrs">{'invisible': [('l10n_latam_use_documents', '=', True), ('l10n_latam_country_code', '=', 'AR')]}</attribute>
</label>
<field name="refund_sequence" position="attributes">
<attribute name="attrs">{'invisible': [('l10n_latam_use_documents', '=', True), ('l10n_latam_country_code', '=', 'AR')]}</attribute>
</field>
<xpath expr="//field[@name='sequence_number_next']/.." position="attributes">
<attribute name="attrs">{'invisible': [('l10n_latam_use_documents', '=', True), ('l10n_latam_country_code', '=', 'AR')]}</attribute>
</xpath>
<field name="l10n_latam_use_documents" position="after">
<field name="company_partner" invisible="1"/>
<field name="l10n_ar_afip_pos_system" attrs="{'invisible':['|', '|', ('l10n_latam_country_code', '!=', 'AR'), ('l10n_latam_use_documents', '=', False), ('type', '!=', 'sale')], 'required':[('l10n_latam_country_code', '=', 'AR'), ('l10n_latam_use_documents', '=', True), ('type', '=', 'sale')]}"/>
-9
View File
@@ -13,15 +13,6 @@ class AccountMove(models.Model):
l10n_latam_internal_type = fields.Selection(
related='l10n_latam_document_type_id.internal_type', string='L10n Latam Internal Type')
def _get_document_type_sequence(self):
""" Return the match sequences for the given journal and invoice """
self.ensure_one()
if self.journal_id.l10n_latam_use_documents and self.l10n_latam_country_code == 'CL':
res = self.journal_id.l10n_cl_sequence_ids.filtered(
lambda x: x.l10n_latam_document_type_id == self.l10n_latam_document_type_id)
return res
return super()._get_document_type_sequence()
def _get_l10n_latam_documents_domain(self):
self.ensure_one()
domain = super()._get_l10n_latam_documents_domain()
+1 -10
View File
@@ -19,16 +19,7 @@
</tree>
</field>
</xpath>
<label for="sequence_number_next" position="attributes">
<attribute name="attrs">{'invisible': [('l10n_latam_use_documents', '=', True), ('l10n_latam_country_code', '=', 'CL')]}</attribute>
</label>
<field name="refund_sequence" position="attributes">
<attribute name="attrs">{'invisible': [('l10n_latam_use_documents', '=', True), ('l10n_latam_country_code', '=', 'CL')]}</attribute>
</field>
<xpath expr="//field[@name='sequence_number_next']/.." position="attributes">
<attribute name="attrs">{'invisible': [('l10n_latam_use_documents', '=', True), ('l10n_latam_country_code', '=', 'CL')]}</attribute>
</xpath>
</field>
</record>
</odoo>
</odoo>
@@ -21,7 +21,6 @@
<field name="arch" type="xml">
<tree decoration-info="state == 'draft'" default_order="create_date" string="Invoices and Refunds" decoration-muted="state == 'cancel'" js_class="account_tree">
<field name="l10n_latam_document_type_id_code"/>
<field name="l10n_latam_document_number" string="Folio"/>
<field name="partner_id_vat"/>
<field name="partner_id"/>
<field name="invoice_date" optional="show"/>
@@ -6,7 +6,7 @@
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
<field name="date" eval="time.strftime('%Y')+'-01-01'"/>
<field name="name" eval="'BNK/%s/0001' % time.strftime('%Y')"/>
<field name="name" eval="'BNK/%s/00001' % time.strftime('%Y')"/>
<field name="balance_end_real">8998.2</field>
<field name="balance_start">5103.0</field>
</record>
@@ -15,7 +15,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">1</field>
<field name="name" eval="'INV/%s/0002 and INV/%s/0003' % (time.strftime('%Y'), time.strftime('%Y'))"/>
<field name="name" eval="'INV/%s/00002 and INV/%s/00003' % (time.strftime('%Y'), time.strftime('%Y'))"/>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -53,7 +53,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">4</field>
<field name="name" eval="'First 2000 $ of invoice %s/0001' % time.strftime('%Y')"/>
<field name="name" eval="'First 2000 $ of invoice %s/00001' % time.strftime('%Y')"/>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -82,7 +82,7 @@
<field name="ref"></field>
<field name="statement_id" ref="l10n_generic_coa.demo_bank_statement_1"/>
<field name="sequence">1</field>
<field name="name" eval="'INV/'+time.strftime('%Y')+'/0002'"/>
<field name="name" eval="'INV/'+time.strftime('%Y')+'/00002'"/>
<field name="journal_id" model="account.journal" search="[
('type', '=', 'bank'),
('company_id', '=', obj().env.company.id)]"/>
@@ -3,6 +3,7 @@
from odoo import models, fields, api, _
from odoo.exceptions import UserError, ValidationError
from functools import partial
import re
from odoo.tools.misc import formatLang
@@ -16,48 +17,27 @@ class AccountMove(models.Model):
l10n_latam_document_type_id = fields.Many2one(
'l10n_latam.document.type', string='Document Type', copy=False, readonly=False, auto_join=True, index=True,
states={'posted': [('readonly', True)]}, compute='_compute_l10n_latam_document_type', store=True)
l10n_latam_sequence_id = fields.Many2one('ir.sequence', compute='_compute_l10n_latam_sequence')
l10n_latam_document_number = fields.Char(
compute='_compute_l10n_latam_document_number', inverse='_inverse_l10n_latam_document_number',
string='Document Number', readonly=True, states={'draft': [('readonly', False)]})
l10n_latam_use_documents = fields.Boolean(related='journal_id.l10n_latam_use_documents')
l10n_latam_country_code = fields.Char(
related='company_id.country_id.code', help='Technical field used to hide/show fields regarding the localization')
def _get_sequence_prefix(self):
""" If we use documents we update sequences only from journal """
return super(AccountMove, self.filtered(lambda x: not x.l10n_latam_use_documents))._get_sequence_prefix()
@api.model
def _deduce_sequence_number_reset(self, name):
if self.l10n_latam_use_documents:
return 'never'
return super(AccountMove, self)._deduce_sequence_number_reset(name)
@api.depends('name')
def _compute_l10n_latam_document_number(self):
recs_with_name = self.filtered(lambda x: x.name != '/')
for rec in recs_with_name:
name = rec.name
doc_code_prefix = rec.l10n_latam_document_type_id.doc_code_prefix
if doc_code_prefix and name:
name = name.split(" ", 1)[-1]
rec.l10n_latam_document_number = name
remaining = self - recs_with_name
remaining.l10n_latam_document_number = False
def _get_last_sequence_domain(self, relaxed=False):
where_string, param = super(AccountMove, self)._get_last_sequence_domain(relaxed)
if self.l10n_latam_use_documents:
where_string += " AND l10n_latam_document_type_id = %(l10n_latam_document_type_id)s "
param['l10n_latam_document_type_id'] = self.l10n_latam_document_type_id.id or 0
return where_string, param
@api.onchange('l10n_latam_document_type_id', 'l10n_latam_document_number')
def _inverse_l10n_latam_document_number(self):
for rec in self.filtered('l10n_latam_document_type_id'):
if not rec.l10n_latam_document_number:
rec.name = '/'
else:
l10n_latam_document_number = rec.l10n_latam_document_type_id._format_document_number(rec.l10n_latam_document_number)
if rec.l10n_latam_document_number != l10n_latam_document_number:
rec.l10n_latam_document_number = l10n_latam_document_number
rec.name = "%s %s" % (rec.l10n_latam_document_type_id.doc_code_prefix, l10n_latam_document_number)
@api.depends('l10n_latam_document_type_id', 'journal_id')
def _compute_l10n_latam_sequence(self):
recs_with_journal_id = self.filtered('journal_id')
for rec in recs_with_journal_id:
rec.l10n_latam_sequence_id = rec._get_document_type_sequence()
remaining = self - recs_with_journal_id
remaining.l10n_latam_sequence_id = False
def _get_starting_sequence(self):
if self.l10n_latam_use_documents:
return "%s 0001-00000000" % (self.l10n_latam_document_type_id.doc_code_prefix)
return super(AccountMove, self)._get_starting_sequence()
def _compute_l10n_latam_amount_and_taxes(self):
recs_invoice = self.filtered(lambda x: x.is_invoice())
@@ -82,20 +62,10 @@ class AccountMove(models.Model):
remaining.l10n_latam_amount_untaxed = False
remaining.l10n_latam_tax_ids = [(5, 0)]
def _compute_invoice_sequence_number_next(self):
""" If journal use documents disable the next number header"""
with_latam_document_number = self.filtered('l10n_latam_use_documents')
with_latam_document_number.invoice_sequence_number_next_prefix = False
with_latam_document_number.invoice_sequence_number_next = False
return super(AccountMove, self - with_latam_document_number)._compute_invoice_sequence_number_next()
def post(self):
for rec in self.filtered(lambda x: x.l10n_latam_use_documents and (not x.name or x.name == '/')):
if not rec.l10n_latam_sequence_id:
raise UserError(_('No sequence or document number linked to invoice id %s') % rec.id)
if rec.type in ('in_receipt', 'out_receipt'):
raise UserError(_('We do not accept the usage of document types on receipts yet. '))
rec.l10n_latam_document_number = rec.l10n_latam_sequence_id.next_by_id()
return super().post()
@api.constrains('name', 'journal_id', 'state')
@@ -122,10 +92,10 @@ class AccountMove(models.Model):
raise ValidationError(_(
'The journal require a document type but not document type has been selected on invoices %s.' % (
without_doc_type.ids)))
without_number = validated_invoices.filtered(
lambda x: not x.l10n_latam_document_number and not x.l10n_latam_sequence_id)
valid = re.compile(r'[A-Z\-]+\s*\d{1,5}\-\d{1,8}')
without_number = validated_invoices.filtered(lambda x: not valid.match(x.name))
if without_number:
raise ValidationError(_('Please set the document number on the following invoices %s.' % (
raise ValidationError(_('The document number on the following invoices is not correct %s.' % (
without_number.ids)))
@api.constrains('type', 'l10n_latam_document_type_id')
@@ -193,24 +163,20 @@ class AccountMove(models.Model):
) for group, amounts in res]
super(AccountMove, self - move_with_doc_type)._compute_invoice_taxes_by_group()
def _get_document_type_sequence(self):
""" Method to be inherited by different localizations. """
self.ensure_one()
return self.env['ir.sequence']
@api.constrains('name', 'partner_id', 'company_id')
def _check_unique_vendor_number(self):
""" The constraint _check_unique_sequence_number is valid for customer bills but not valid for us on vendor
bills because the uniqueness must be per partner and also because we want to validate on entry creation and
not on entry validation """
for rec in self.filtered(lambda x: x.is_purchase_document() and x.l10n_latam_use_documents and x.l10n_latam_document_number):
for rec in self.filtered(lambda x: x.is_purchase_document() and x.l10n_latam_use_documents):
domain = [
('type', '=', rec.type),
# by validating name we validate l10n_latam_document_number and l10n_latam_document_type_id
# by validating name we validate l10n_latam_document_type_id
('name', '=', rec.name),
('company_id', '=', rec.company_id.id),
('id', '!=', rec.id),
('commercial_partner_id', '=', rec.commercial_partner_id.id)
('commercial_partner_id', '=', rec.commercial_partner_id.id),
('posted_before', '=', True),
]
if rec.search(domain):
raise ValidationError(_('Vendor bill number must be unique per vendor and company.'))
@@ -38,15 +38,12 @@
<field name="l10n_latam_available_document_type_ids" invisible="1"/>
<field name="l10n_latam_use_documents" invisible="1"/>
<field name="l10n_latam_country_code" invisible="1"/>
<field name="l10n_latam_sequence_id" invisible="1"/>
</form>
<field name="journal_id" position="after">
<field name="l10n_latam_document_type_id"
attrs="{'invisible': [('l10n_latam_use_documents', '=', False)], 'required': [('l10n_latam_use_documents', '=', True)], 'readonly': [('state', '!=', 'draft')]}"
domain="[('id', 'in', l10n_latam_available_document_type_ids)]" options="{'no_open': True, 'no_create': True}"/>
<field name="l10n_latam_document_number"
attrs="{'invisible': ['|', ('l10n_latam_sequence_id', '!=', False), ('l10n_latam_use_documents', '=', False)], 'required': [('l10n_latam_sequence_id', '=', False), ('l10n_latam_use_documents', '=', True)], 'readonly': [('state', '!=', 'draft')]}"/>
</field>
</field>
</record>
@@ -13,7 +13,6 @@
<xpath expr="//h2" position="after">
<h2 t-if="o.l10n_latam_document_type_id.report_name">
<span t-field="o.l10n_latam_document_type_id.report_name"/>
<span t-field="o.l10n_latam_document_number"/>
</h2>
</xpath>
@@ -5,27 +5,11 @@ from odoo.exceptions import UserError
class AccountMoveReversal(models.TransientModel):
_inherit = "account.move.reversal"
l10n_latam_use_documents = fields.Boolean(readonly=True)
l10n_latam_document_type_id = fields.Many2one('l10n_latam.document.type', 'Document Type', ondelete='cascade', domain="[('id', 'in', l10n_latam_available_document_type_ids)]")
l10n_latam_available_document_type_ids = fields.Many2many('l10n_latam.document.type', store=False)
l10n_latam_sequence_id = fields.Many2one('ir.sequence', compute='_compute_l10n_latam_sequence')
l10n_latam_document_number = fields.Char(string='Document Number')
@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) > 1:
move_ids_use_document = move_ids.filtered(lambda move: move.l10n_latam_use_documents)
if move_ids_use_document:
raise UserError(_('You can only reverse documents with legal invoicing documents from Latin America one at a time.\nProblematic documents: %s') % ", ".join(move_ids_use_document.mapped('name')))
else:
res['l10n_latam_use_documents'] = move_ids.journal_id.l10n_latam_use_documents
return res
l10n_latam_use_documents = fields.Boolean(compute='_compute_document_type')
l10n_latam_document_type_id = fields.Many2one('l10n_latam.document.type', 'Document Type', ondelete='cascade', domain="[('id', 'in', l10n_latam_available_document_type_ids)]", compute='_compute_document_type', readonly=False)
l10n_latam_available_document_type_ids = fields.Many2many('l10n_latam.document.type', compute='_compute_document_type')
@api.model
def _reverse_type_map(self, move_type):
@@ -38,41 +22,29 @@ class AccountMoveReversal(models.TransientModel):
'in_receipt': 'out_receipt'}
return match.get(move_type)
@api.onchange('move_ids')
def _onchange_move_id(self):
if self.l10n_latam_use_documents:
refund = self.env['account.move'].new({
'type': self._reverse_type_map(self.move_ids.type),
'journal_id': self.move_ids.journal_id.id,
'partner_id': self.move_ids.partner_id.id,
'company_id': self.move_ids.company_id.id,
})
self.l10n_latam_document_type_id = refund.l10n_latam_document_type_id
self.l10n_latam_available_document_type_ids = refund.l10n_latam_available_document_type_ids
@api.depends('move_ids')
def _compute_document_type(self):
self.l10n_latam_available_document_type_ids = False
self.l10n_latam_document_type_id = False
self.l10n_latam_use_documents = False
for record in self:
if len(record.move_ids) > 1:
move_ids_use_document = record.move_ids._origin.filtered(lambda move: move.l10n_latam_use_documents)
if move_ids_use_document:
raise UserError(_('You can only reverse documents with legal invoicing documents from Latin America one at a time.\nProblematic documents: %s') % ", ".join(move_ids_use_document.mapped('name')))
else:
record.l10n_latam_use_documents = record.move_ids.journal_id.l10n_latam_use_documents
if record.l10n_latam_use_documents:
refund = record.env['account.move'].new({
'type': record._reverse_type_map(record.move_ids.type),
'journal_id': record.move_ids.journal_id.id,
'partner_id': record.move_ids.partner_id.id,
'company_id': record.move_ids.company_id.id,
})
record.l10n_latam_document_type_id = refund.l10n_latam_document_type_id
record.l10n_latam_available_document_type_ids = refund.l10n_latam_available_document_type_ids
def reverse_moves(self):
return super(AccountMoveReversal, self.with_context(
default_l10n_latam_document_type_id=self.l10n_latam_document_type_id.id,
default_l10n_latam_document_number=self.l10n_latam_document_number)).reverse_moves()
@api.depends('l10n_latam_document_type_id')
def _compute_l10n_latam_sequence(self):
for rec in self:
rec.l10n_latam_sequence_id = False
if len(rec.move_ids) <= 1:
refund = rec.env['account.move'].new({
'type': self._reverse_type_map(rec.move_ids.type),
'journal_id': rec.move_ids.journal_id.id,
'partner_id': rec.move_ids.partner_id.id,
'company_id': rec.move_ids.company_id.id,
'l10n_latam_document_type_id': rec.l10n_latam_document_type_id.id,
})
rec.l10n_latam_sequence_id = refund._get_document_type_sequence()
@api.onchange('l10n_latam_document_number', 'l10n_latam_document_type_id')
def _onchange_l10n_latam_document_number(self):
if self.l10n_latam_document_type_id:
l10n_latam_document_number = self.l10n_latam_document_type_id._format_document_number(
self.l10n_latam_document_number)
if self.l10n_latam_document_number != l10n_latam_document_number:
self.l10n_latam_document_number = l10n_latam_document_number
default_l10n_latam_document_type_id=self.l10n_latam_document_type_id.id)).reverse_moves()
@@ -8,12 +8,10 @@
<field name="arch" type="xml">
<form>
<field name="l10n_latam_use_documents" invisible="1"/>
<field name="l10n_latam_sequence_id" invisible="1"/>
</form>
<field name="date" position="before">
<field name="l10n_latam_available_document_type_ids" invisible="1"/>
<field name="l10n_latam_document_type_id" attrs="{'invisible': ['|', ('l10n_latam_use_documents', '=', False), ('refund_method', '=', 'refund')], 'required': [('l10n_latam_use_documents', '=', True), ('refund_method', '!=', 'refund')]}" options="{'no_open': True, 'no_create': True}"/>
<field name="l10n_latam_document_number" attrs="{'invisible': ['|', ('l10n_latam_sequence_id', '!=', False), ('refund_method', '=', 'refund')], 'required': [('l10n_latam_sequence_id', '=', True), ('refund_method', '!=', 'refund')]}"/>
</field>
</field>
</record>
+1 -1
View File
@@ -99,7 +99,7 @@ class TestSaleToInvoice(TestCommonSaleNoChart):
self.assertEqual(len(self.sale_order.invoice_ids), 2, 'Invoice should be created for the SO')
invoice = self.sale_order.invoice_ids.sorted()[0]
invoice = self.sale_order.invoice_ids.sorted()[-1]
self.assertEqual(len(invoice.invoice_line_ids), len(self.sale_order.order_line), 'All lines should be invoiced')
self.assertEqual(invoice.amount_total, self.sale_order.amount_total - downpayment_line.price_unit, 'Downpayment should be applied')
@@ -47,7 +47,9 @@ class ValuationReconciliationTestCommon(StockAccountTestCommon):
def _change_pickings_date(self, pickings, date):
pickings.mapped('move_lines').write({'date': date})
pickings.mapped('move_lines.account_move_ids').write({'name': '/', 'state': 'draft'})
pickings.mapped('move_lines.account_move_ids').write({'date': date})
pickings.move_lines.account_move_ids.post()
def _create_product_category(self):
return self.env['product.category'].create({