[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:
@@ -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,5 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from . import sequence_mixin
|
||||
from . import partner
|
||||
from . import account
|
||||
from . import account_reconcile_model
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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>
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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')]}"/>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user