commit
3bfe850eb1
160
account_move_name_sequence/README.rst
Normal file
160
account_move_name_sequence/README.rst
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
============================
|
||||||
|
Account Move Number Sequence
|
||||||
|
============================
|
||||||
|
|
||||||
|
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! This file is generated by oca-gen-addon-readme !!
|
||||||
|
!! changes will be overwritten. !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
|
||||||
|
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||||
|
:target: https://odoo-community.org/page/development-status
|
||||||
|
:alt: Beta
|
||||||
|
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
|
||||||
|
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
|
||||||
|
:alt: License: AGPL-3
|
||||||
|
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github
|
||||||
|
:target: https://github.com/OCA/account-financial-tools/tree/15.0/account_move_name_sequence
|
||||||
|
:alt: OCA/account-financial-tools
|
||||||
|
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||||
|
:target: https://translation.odoo-community.org/projects/account-financial-tools-15-0/account-financial-tools-15-0-account_move_name_sequence
|
||||||
|
:alt: Translate me on Weblate
|
||||||
|
.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
|
||||||
|
:target: https://runbot.odoo-community.org/runbot/92/15.0
|
||||||
|
:alt: Try me on Runbot
|
||||||
|
|
||||||
|
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||||
|
|
||||||
|
In Odoo version 13.0 and previous versions, the number of journal entries was generated from a sequence configured on the journal.
|
||||||
|
|
||||||
|
In Odoo version 14.0, the number of journal entries can be manually set by the user. Then, the number attributed for the next journal entries in the same journal is computed by a complex piece of code that guesses the format of the journal entry number from the number of the journal entry which was manually entered by the user. It has several drawbacks:
|
||||||
|
|
||||||
|
* the available options for the sequence are limited,
|
||||||
|
* it is not possible to configure the sequence in advance before the deployment in production,
|
||||||
|
* as it is error-prone, they added a *Resequence* wizard to re-generate the journal entry numbers, which can be considered as illegal in many countries,
|
||||||
|
* the `piece of code <https://github.com/odoo/odoo/blob/14.0/addons/account/models/sequence_mixin.py>`_ that handles this is not easy to understand and quite difficult to debug.
|
||||||
|
|
||||||
|
Odoo>=v14.0 raises new concurrency issues since it locks the last journal entry of the journal to get the new number causing a bottleneck
|
||||||
|
Even if you only are creating a draft journal entry it locks the last one
|
||||||
|
It applies to all accounting Journal Entries
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
- Customer Invoices
|
||||||
|
- Credit Notes
|
||||||
|
- Customer Payments
|
||||||
|
- Vendor Bills
|
||||||
|
- Vendor Refunds
|
||||||
|
- Vendor Payment
|
||||||
|
- Manual Journal Entries
|
||||||
|
|
||||||
|
Then, the following concurrency errors are being raised now frequently:
|
||||||
|
|
||||||
|
* Editing the last record used to get the new number from another process
|
||||||
|
* Creating a new draft invoice/payment (not only when posting it)
|
||||||
|
* Creating a transaction to create an invoice then payment or vice versa raises a deadlock error
|
||||||
|
* Reconciling the last record it could be a heavy process
|
||||||
|
* Creating 2 or more Invoices/Bills at the same time
|
||||||
|
* Creating 2 or more Payments at the same time (Even if your country allows to relax gaps in these kinds of documents, you are not able anymore to change the implementation to standard)
|
||||||
|
* Creating 2 or more Journal Entries at the same time
|
||||||
|
|
||||||
|
|
||||||
|
All these increases in concurrency errors bring more issues since that Odoo is not prepared:
|
||||||
|
|
||||||
|
* Using e-commerce, configured with Invoicing Policy Ordered and Automatic Invoice, the portal users will see errors in the checkout even if the payment was done, the sale order could be in state draft and request a new payment, so double charges
|
||||||
|
* Using `subscription_template.payment_mode=success_payment` you will see subscriptions with tag "payment exception"
|
||||||
|
* Using accounting creating invoice or payment, you will see errors then you will need to start the process again and again until you get the lock before another user
|
||||||
|
* The workers could be used for more time than before since that it could be waiting for release so less concurrent users supported or loading page is shown more frequently affecting the performance
|
||||||
|
|
||||||
|
The new accounting number is a significant bottleneck
|
||||||
|
|
||||||
|
.. image:: https://media.istockphoto.com/vectors/road-highways-with-many-different-vehicles-vector-id1328678690
|
||||||
|
|
||||||
|
|
||||||
|
If you do not believe all these issues are occurring, we have created the following issues and unittest to reproduce errors in v14.0 including the deadlock, but not v13.0:
|
||||||
|
|
||||||
|
- Passing unittest for `13.0 - [REF] account: Adding unittests for concurrency issues in account_move sequences <https://github.com/odoo/odoo/pull/91614>`_
|
||||||
|
- Concurrency errors for `14.0 - [REF] account: Adding unittests for concurrency issues in account_move sequences <https://github.com/odoo/odoo/pull/91525>`_
|
||||||
|
- `Stress testing and issue reported to Odoo <https://github.com/odoo/odoo/issues/90465>`_
|
||||||
|
- `[BUG] account: Concurrency errors increased considerably in account.move for Odoo>=v14.0 #91873 <https://github.com/odoo/odoo/issues/91873>`_
|
||||||
|
|
||||||
|
|
||||||
|
Using this module, you can configure what kind of documents the gap sequence may be relaxed
|
||||||
|
And even if you must use no-gap in your company or country it will reduce the concurrency issues since the module is using an extra table (ir_sequence) instead of locking the last record
|
||||||
|
|
||||||
|
For those like me who think that the implementation before Odoo v14.0 was much better, for the accountants who think it should not be possible to manually enter the sequence of a customer invoice, for the auditor who considers that resequencing journal entries is prohibited by law, this module may be a solution to get out of the nightmare.
|
||||||
|
|
||||||
|
The field names used in this module to configure the sequence on the journal are exactly the same as in Odoo version 13.0 and previous versions. That way, if you migrate to Odoo version 14.0 and you install this module immediately after the migration, you should keep the previous behavior and the same sequences will continue to be used.
|
||||||
|
|
||||||
|
The module removes access to the *Resequence* wizard on journal entries.
|
||||||
|
|
||||||
|
**Table of contents**
|
||||||
|
|
||||||
|
.. contents::
|
||||||
|
:local:
|
||||||
|
|
||||||
|
Configuration
|
||||||
|
=============
|
||||||
|
|
||||||
|
On the form view of an account journal, in the first tab, there is a many2one link to the sequence. When you create a new journal, you can keep this field empty and a new sequence will be automatically created when you save the journal.
|
||||||
|
|
||||||
|
On sale and purchase journals, you have an additional option to have another sequence dedicated to refunds.
|
||||||
|
|
||||||
|
Upon module installation, all existing journals will be updated with a journal entry sequence (and also a credit note sequence for sale and purchase journals). You should update the configuration of the sequences to fit your needs. You can uncheck the option *Dedicated Credit Note Sequence* on existing sale and purchase journals if you don't want it. For the journals which already have journal entries, you should update the sequence configuration to avoid a discontinuity in the numbering for the next journal entry.
|
||||||
|
|
||||||
|
Bug Tracker
|
||||||
|
===========
|
||||||
|
|
||||||
|
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-tools/issues>`_.
|
||||||
|
In case of trouble, please check there if your issue has already been reported.
|
||||||
|
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||||
|
`feedback <https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_move_name_sequence%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||||
|
|
||||||
|
Do not contact contributors directly about support or help with technical issues.
|
||||||
|
|
||||||
|
Credits
|
||||||
|
=======
|
||||||
|
|
||||||
|
Authors
|
||||||
|
~~~~~~~
|
||||||
|
|
||||||
|
* Akretion
|
||||||
|
* Vauxoo
|
||||||
|
|
||||||
|
Contributors
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
* Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
* Moisés López <moylop260@vauxoo.com>
|
||||||
|
* Francisco Luna <fluna@vauxoo.com>
|
||||||
|
|
||||||
|
Maintainers
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
This module is maintained by the OCA.
|
||||||
|
|
||||||
|
.. image:: https://odoo-community.org/logo.png
|
||||||
|
:alt: Odoo Community Association
|
||||||
|
:target: https://odoo-community.org
|
||||||
|
|
||||||
|
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
|
mission is to support the collaborative development of Odoo features and
|
||||||
|
promote its widespread use.
|
||||||
|
|
||||||
|
.. |maintainer-alexis-via| image:: https://github.com/alexis-via.png?size=40px
|
||||||
|
:target: https://github.com/alexis-via
|
||||||
|
:alt: alexis-via
|
||||||
|
.. |maintainer-moylop260| image:: https://github.com/moylop260.png?size=40px
|
||||||
|
:target: https://github.com/moylop260
|
||||||
|
:alt: moylop260
|
||||||
|
.. |maintainer-frahikLV| image:: https://github.com/frahikLV.png?size=40px
|
||||||
|
:target: https://github.com/frahikLV
|
||||||
|
:alt: frahikLV
|
||||||
|
|
||||||
|
Current `maintainers <https://odoo-community.org/page/maintainer-role>`__:
|
||||||
|
|
||||||
|
|maintainer-alexis-via| |maintainer-moylop260| |maintainer-frahikLV|
|
||||||
|
|
||||||
|
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/15.0/account_move_name_sequence>`_ project on GitHub.
|
||||||
|
|
||||||
|
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
|
2
account_move_name_sequence/__init__.py
Normal file
2
account_move_name_sequence/__init__.py
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
from .post_install import create_journal_sequences
|
||||||
|
from . import models
|
27
account_move_name_sequence/__manifest__.py
Normal file
27
account_move_name_sequence/__manifest__.py
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
# Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||||
|
# Copyright 2022 Vauxoo (https://www.vauxoo.com/)
|
||||||
|
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
# @author: Moisés López <moylop260@vauxoo.com>
|
||||||
|
# @author: Francisco Luna <fluna@vauxoo.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Account Move Number Sequence",
|
||||||
|
"version": "16.0.1.0.0",
|
||||||
|
"category": "Accounting",
|
||||||
|
"license": "AGPL-3",
|
||||||
|
"summary": "Generate journal entry number from sequence",
|
||||||
|
"author": "Akretion,Vauxoo,Odoo Community Association (OCA)",
|
||||||
|
"maintainers": ["alexis-via", "moylop260", "frahikLV"],
|
||||||
|
"website": "https://github.com/OCA/account-financial-tools",
|
||||||
|
"depends": [
|
||||||
|
"account",
|
||||||
|
],
|
||||||
|
"data": [
|
||||||
|
"views/account_journal.xml",
|
||||||
|
"views/account_move.xml",
|
||||||
|
"security/ir.model.access.csv",
|
||||||
|
],
|
||||||
|
"post_init_hook": "create_journal_sequences",
|
||||||
|
"installable": True,
|
||||||
|
}
|
129
account_move_name_sequence/i18n/account_move_name_sequence.pot
Normal file
129
account_move_name_sequence/i18n/account_move_name_sequence.pot
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * account_move_name_sequence
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 15.0\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.constraint,message:account_move_name_sequence.constraint_account_move_name_state_diagonal
|
||||||
|
msgid ""
|
||||||
|
"A move can not be posted with name \"/\" or empty value\n"
|
||||||
|
"Check the journal sequence, please"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,help:account_move_name_sequence.field_account_journal__refund_sequence
|
||||||
|
msgid ""
|
||||||
|
"Check this box if you don't want to share the same sequence for invoices and"
|
||||||
|
" credit notes made from this journal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__refund_sequence_id
|
||||||
|
msgid "Credit Note Entry Sequence"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__refund_sequence
|
||||||
|
msgid "Dedicated Credit Note Sequence"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__sequence_id
|
||||||
|
msgid "Entry Sequence"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__highest_name
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__highest_name
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__highest_name
|
||||||
|
msgid "Highest Name"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model,name:account_move_name_sequence.model_account_journal
|
||||||
|
msgid "Journal"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model,name:account_move_name_sequence.model_account_move
|
||||||
|
msgid "Journal Entry"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__name
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__name
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__name
|
||||||
|
msgid "Number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: code:addons/account_move_name_sequence/models/account_journal.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"On journal '%s', the same sequence is used as Entry Sequence and Credit Note"
|
||||||
|
" Entry Sequence."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: code:addons/account_move_name_sequence/models/account_journal.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid "Refund"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: code:addons/account_move_name_sequence/models/account_journal.py:0
|
||||||
|
#: model:ir.model,name:account_move_name_sequence.model_ir_sequence
|
||||||
|
#, python-format
|
||||||
|
msgid "Sequence"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__sequence_number
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__sequence_number
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__sequence_number
|
||||||
|
msgid "Sequence Number"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__sequence_prefix
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__sequence_prefix
|
||||||
|
#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_payment__sequence_prefix
|
||||||
|
msgid "Sequence Prefix"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: code:addons/account_move_name_sequence/models/account_journal.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"The company is not set on sequence '%(sequence)s' configured as credit note "
|
||||||
|
"sequence of journal '%(journal)s'."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: code:addons/account_move_name_sequence/models/account_journal.py:0
|
||||||
|
#, python-format
|
||||||
|
msgid ""
|
||||||
|
"The company is not set on sequence '%(sequence)s' configured on journal "
|
||||||
|
"'%(journal)s'."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,help:account_move_name_sequence.field_account_journal__refund_sequence_id
|
||||||
|
msgid ""
|
||||||
|
"This sequence will be used to generate the journal entry number for refunds."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#. module: account_move_name_sequence
|
||||||
|
#: model:ir.model.fields,help:account_move_name_sequence.field_account_journal__sequence_id
|
||||||
|
msgid "This sequence will be used to generate the journal entry number."
|
||||||
|
msgstr ""
|
3
account_move_name_sequence/models/__init__.py
Normal file
3
account_move_name_sequence/models/__init__.py
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
from . import account_journal
|
||||||
|
from . import account_move
|
||||||
|
from . import ir_sequence
|
246
account_move_name_sequence/models/account_journal.py
Normal file
246
account_move_name_sequence/models/account_journal.py
Normal file
@ -0,0 +1,246 @@
|
|||||||
|
# Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||||
|
# Copyright 2022 Vauxoo (https://www.vauxoo.com/)
|
||||||
|
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
# @author: Moisés López <moylop260@vauxoo.com>
|
||||||
|
# @author: Francisco Luna <fluna@vauxoo.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from odoo import _, api, fields, models
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AccountJournal(models.Model):
|
||||||
|
_inherit = "account.journal"
|
||||||
|
|
||||||
|
sequence_id = fields.Many2one(
|
||||||
|
"ir.sequence",
|
||||||
|
string="Entry Sequence",
|
||||||
|
copy=False,
|
||||||
|
check_company=True,
|
||||||
|
domain="[('company_id', '=', company_id)]",
|
||||||
|
help="This sequence will be used to generate the journal entry number.",
|
||||||
|
)
|
||||||
|
refund_sequence_id = fields.Many2one(
|
||||||
|
"ir.sequence",
|
||||||
|
string="Credit Note Entry Sequence",
|
||||||
|
copy=False,
|
||||||
|
check_company=True,
|
||||||
|
domain="[('company_id', '=', company_id)]",
|
||||||
|
help="This sequence will be used to generate the journal entry number for refunds.",
|
||||||
|
)
|
||||||
|
# Redefine the default to True as <=v13.0
|
||||||
|
refund_sequence = fields.Boolean(default=True)
|
||||||
|
|
||||||
|
@api.constrains("refund_sequence_id", "sequence_id")
|
||||||
|
def _check_journal_sequence(self):
|
||||||
|
for journal in self:
|
||||||
|
if (
|
||||||
|
journal.refund_sequence_id
|
||||||
|
and journal.sequence_id
|
||||||
|
and journal.refund_sequence_id == journal.sequence_id
|
||||||
|
):
|
||||||
|
raise ValidationError(
|
||||||
|
_(
|
||||||
|
"On journal '%s', the same sequence is used as "
|
||||||
|
"Entry Sequence and Credit Note Entry Sequence.",
|
||||||
|
journal.display_name,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if journal.sequence_id and not journal.sequence_id.company_id:
|
||||||
|
msg = _(
|
||||||
|
"The company is not set on sequence '%(sequence)s' configured on "
|
||||||
|
"journal '%(journal)s'.",
|
||||||
|
sequence=journal.sequence_id.display_name,
|
||||||
|
journal=journal.display_name,
|
||||||
|
)
|
||||||
|
raise ValidationError(msg)
|
||||||
|
if journal.refund_sequence_id and not journal.refund_sequence_id.company_id:
|
||||||
|
msg = _(
|
||||||
|
"The company is not set on sequence '%(sequence)s' configured as "
|
||||||
|
"credit note sequence of journal '%(journal)s'.",
|
||||||
|
sequence=journal.refund_sequence_id.display_name,
|
||||||
|
journal=journal.display_name,
|
||||||
|
)
|
||||||
|
raise ValidationError(msg)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
for vals in vals_list:
|
||||||
|
if not vals.get("sequence_id"):
|
||||||
|
vals["sequence_id"] = self._create_sequence(vals).id
|
||||||
|
if (
|
||||||
|
vals.get("type") in ("sale", "purchase")
|
||||||
|
and vals.get("refund_sequence", True)
|
||||||
|
and not vals.get("refund_sequence_id")
|
||||||
|
):
|
||||||
|
vals["refund_sequence_id"] = self._create_sequence(vals, refund=True).id
|
||||||
|
return super().create(vals_list)
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _prepare_sequence(self, vals, refund=False):
|
||||||
|
code = vals.get("code") and vals["code"].upper() or ""
|
||||||
|
prefix = "%s%s/%%(range_year)s/" % (refund and "R" or "", code)
|
||||||
|
seq_vals = {
|
||||||
|
"name": "%s%s"
|
||||||
|
% (vals.get("name", _("Sequence")), refund and " " + _("Refund") or ""),
|
||||||
|
"company_id": vals.get("company_id") or self.env.company.id,
|
||||||
|
"implementation": "no_gap",
|
||||||
|
"prefix": prefix,
|
||||||
|
"padding": 4,
|
||||||
|
"use_date_range": True,
|
||||||
|
}
|
||||||
|
return seq_vals
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _create_sequence(self, vals, refund=False):
|
||||||
|
seq_vals = self._prepare_sequence(vals, refund=refund)
|
||||||
|
return self.env["ir.sequence"].sudo().create(seq_vals)
|
||||||
|
|
||||||
|
def _prepare_sequence_current_moves(self, refund=False):
|
||||||
|
"""Get sequence dict values the journal based on current moves"""
|
||||||
|
self.ensure_one()
|
||||||
|
move_domain = [
|
||||||
|
("journal_id", "=", self.id),
|
||||||
|
("name", "!=", "/"),
|
||||||
|
]
|
||||||
|
if self.refund_sequence:
|
||||||
|
# Based on original Odoo behavior
|
||||||
|
if refund:
|
||||||
|
move_domain.append(("move_type", "in", ("out_refund", "in_refund")))
|
||||||
|
else:
|
||||||
|
move_domain.append(("move_type", "not in", ("out_refund", "in_refund")))
|
||||||
|
last_move = self.env["account.move"].search(
|
||||||
|
move_domain, limit=1, order="id DESC"
|
||||||
|
)
|
||||||
|
msg_err = (
|
||||||
|
"Journal %s could not get sequence %s values based on current moves. "
|
||||||
|
"Using default values." % (self.id, refund and "refund" or "")
|
||||||
|
)
|
||||||
|
if not last_move:
|
||||||
|
_logger.warning("%s %s", msg_err, "No moves found")
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
with self.env.cr.savepoint():
|
||||||
|
# get the current sequence values could be buggy to get
|
||||||
|
# But even we can use the default values
|
||||||
|
# or do manual changes instead of raising errors
|
||||||
|
last_sequence = last_move._get_last_sequence()
|
||||||
|
if not last_sequence:
|
||||||
|
last_sequence = (
|
||||||
|
last_move._get_last_sequence(relaxed=True)
|
||||||
|
or last_move._get_starting_sequence()
|
||||||
|
)
|
||||||
|
|
||||||
|
__, seq_format_values = last_move._get_sequence_format_param(
|
||||||
|
last_sequence
|
||||||
|
)
|
||||||
|
prefix1 = seq_format_values["prefix1"]
|
||||||
|
prefix = prefix1
|
||||||
|
if seq_format_values["year_length"] == 4:
|
||||||
|
prefix += "%(range_year)s"
|
||||||
|
elif seq_format_values["year_length"] == 2:
|
||||||
|
prefix += "%(range_y)s"
|
||||||
|
else:
|
||||||
|
# If there is not year so current values are valid
|
||||||
|
seq_vals = {
|
||||||
|
"padding": seq_format_values["seq_length"],
|
||||||
|
"suffix": seq_format_values["suffix"],
|
||||||
|
"prefix": prefix,
|
||||||
|
"date_range_ids": [],
|
||||||
|
"use_date_range": False,
|
||||||
|
"number_next_actual": seq_format_values["seq"] + 1,
|
||||||
|
}
|
||||||
|
return seq_vals
|
||||||
|
prefix2 = seq_format_values.get("prefix2") or ""
|
||||||
|
prefix += prefix2
|
||||||
|
month = seq_format_values.get("month") # It is 0 if only have year
|
||||||
|
if month:
|
||||||
|
prefix += "%(range_month)s"
|
||||||
|
prefix3 = seq_format_values.get("prefix3") or ""
|
||||||
|
where_name_value = "%s%s%s%s%s%%" % (
|
||||||
|
prefix1,
|
||||||
|
"_" * seq_format_values["year_length"],
|
||||||
|
prefix2,
|
||||||
|
"_" * bool(month) * 2,
|
||||||
|
prefix3,
|
||||||
|
)
|
||||||
|
prefixes = prefix1 + prefix2
|
||||||
|
select_year = (
|
||||||
|
"split_part(name, '%s', %d)" % (prefix2, prefixes.count(prefix2))
|
||||||
|
if prefix2
|
||||||
|
else "''"
|
||||||
|
)
|
||||||
|
prefixes += prefix3
|
||||||
|
select_month = (
|
||||||
|
"split_part(name, '%s', %d)" % (prefix3, prefixes.count(prefix3))
|
||||||
|
if prefix3
|
||||||
|
else "''"
|
||||||
|
)
|
||||||
|
select_max_number = (
|
||||||
|
"MAX(split_part(name, '%s', %d)::INTEGER) AS max_number"
|
||||||
|
% (
|
||||||
|
prefixes[-1],
|
||||||
|
prefixes.count(prefixes[-1]) + 1,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
query = (
|
||||||
|
"SELECT %s, %s, %s FROM account_move "
|
||||||
|
"WHERE name LIKE %%s AND journal_id=%%s GROUP BY 1,2"
|
||||||
|
) % (
|
||||||
|
select_year,
|
||||||
|
select_month,
|
||||||
|
select_max_number,
|
||||||
|
)
|
||||||
|
# It is not using user input
|
||||||
|
# pylint: disable=sql-injection
|
||||||
|
self.env.cr.execute(query, (where_name_value, self.id))
|
||||||
|
res = self.env.cr.fetchall()
|
||||||
|
prefix += prefix3
|
||||||
|
seq_vals = {
|
||||||
|
"padding": seq_format_values["seq_length"],
|
||||||
|
"suffix": seq_format_values["suffix"],
|
||||||
|
"prefix": prefix,
|
||||||
|
"date_range_ids": [],
|
||||||
|
"use_date_range": True,
|
||||||
|
}
|
||||||
|
for year, month, max_number in res:
|
||||||
|
if not year and not month:
|
||||||
|
seq_vals.update(
|
||||||
|
{
|
||||||
|
"use_date_range": False,
|
||||||
|
"number_next_actual": max_number + 1,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
if len(year) == 2:
|
||||||
|
# Year >=50 will be considered as last century 1950
|
||||||
|
# Year <=49 will be considered as current century 2049
|
||||||
|
if int(year) >= 50:
|
||||||
|
year = "19" + year
|
||||||
|
else:
|
||||||
|
year = "20" + year
|
||||||
|
if month:
|
||||||
|
date_from = fields.Date.to_date("%s-%s-1" % (year, month))
|
||||||
|
date_to = fields.Date.end_of(date_from, "month")
|
||||||
|
else:
|
||||||
|
date_from = fields.Date.to_date("%s-1-1" % year)
|
||||||
|
date_to = fields.Date.to_date("%s-12-31" % year)
|
||||||
|
seq_vals["date_range_ids"].append(
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"date_from": date_from,
|
||||||
|
"date_to": date_to,
|
||||||
|
"number_next_actual": max_number + 1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return seq_vals
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning("%s %s", msg_err, e)
|
||||||
|
return {}
|
72
account_move_name_sequence/models/account_move.py
Normal file
72
account_move_name_sequence/models/account_move.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
# Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||||
|
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import api, fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class AccountMove(models.Model):
|
||||||
|
_inherit = "account.move"
|
||||||
|
|
||||||
|
name = fields.Char(compute="_compute_name_by_sequence")
|
||||||
|
# highest_name, sequence_prefix and sequence_number are not needed any more
|
||||||
|
# -> compute=False to improve perf
|
||||||
|
highest_name = fields.Char(compute=False)
|
||||||
|
sequence_prefix = fields.Char(compute=False)
|
||||||
|
sequence_number = fields.Integer(compute=False)
|
||||||
|
|
||||||
|
_sql_constraints = [
|
||||||
|
(
|
||||||
|
"name_state_diagonal",
|
||||||
|
"CHECK(COALESCE(name, '') NOT IN ('/', '') OR state!='posted')",
|
||||||
|
'A move can not be posted with name "/" or empty value\n'
|
||||||
|
"Check the journal sequence, please",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
@api.depends("state", "journal_id", "date")
|
||||||
|
def _compute_name_by_sequence(self):
|
||||||
|
for move in self:
|
||||||
|
name = move.name or "/"
|
||||||
|
# I can't use posted_before in this IF because
|
||||||
|
# posted_before is set to True in _post() at the same
|
||||||
|
# time as state is set to "posted"
|
||||||
|
if (
|
||||||
|
move.state == "posted"
|
||||||
|
and (not move.name or move.name == "/")
|
||||||
|
and move.journal_id
|
||||||
|
and move.journal_id.sequence_id
|
||||||
|
):
|
||||||
|
if (
|
||||||
|
move.move_type in ("out_refund", "in_refund")
|
||||||
|
and move.journal_id.type in ("sale", "purchase")
|
||||||
|
and move.journal_id.refund_sequence
|
||||||
|
and move.journal_id.refund_sequence_id
|
||||||
|
):
|
||||||
|
seq = move.journal_id.refund_sequence_id
|
||||||
|
else:
|
||||||
|
seq = move.journal_id.sequence_id
|
||||||
|
# next_by_id(date) only applies on ir.sequence.date_range selection
|
||||||
|
# => we use with_context(ir_sequence_date=date).next_by_id()
|
||||||
|
# which applies on ir.sequence.date_range selection AND prefix
|
||||||
|
name = seq.with_context(ir_sequence_date=move.date).next_by_id()
|
||||||
|
move.name = name
|
||||||
|
|
||||||
|
# We must by-pass this constraint of sequence.mixin
|
||||||
|
def _constrains_date_sequence(self):
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _is_end_of_seq_chain(self):
|
||||||
|
invoices_no_gap_sequences = self.filtered(
|
||||||
|
lambda inv: inv.journal_id.sequence_id.implementation == "no_gap"
|
||||||
|
)
|
||||||
|
invoices_other_sequences = self - invoices_no_gap_sequences
|
||||||
|
if not invoices_other_sequences and invoices_no_gap_sequences:
|
||||||
|
return False
|
||||||
|
return super(AccountMove, invoices_other_sequences)._is_end_of_seq_chain()
|
||||||
|
|
||||||
|
def _fetch_duplicate_supplier_reference(self, only_posted=False):
|
||||||
|
moves = self.filtered(lambda m: m.is_purchase_document() and m.ref)
|
||||||
|
if moves:
|
||||||
|
self.flush_model(["name", "journal_id", "move_type", "state"])
|
||||||
|
return super()._fetch_duplicate_supplier_reference(only_posted=only_posted)
|
52
account_move_name_sequence/models/ir_sequence.py
Normal file
52
account_move_name_sequence/models/ir_sequence.py
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
from odoo import fields, models
|
||||||
|
|
||||||
|
|
||||||
|
class IrSequence(models.Model):
|
||||||
|
_inherit = "ir.sequence"
|
||||||
|
|
||||||
|
def _create_date_range_seq(self, date):
|
||||||
|
# Fix issue creating new date range for future dates
|
||||||
|
# It assigns more than one month
|
||||||
|
# TODO: Remove if odoo merge the following PR:
|
||||||
|
# https://github.com/odoo/odoo/pull/91019
|
||||||
|
date_obj = fields.Date.from_string(date)
|
||||||
|
sequence_range = self.env["ir.sequence.date_range"]
|
||||||
|
prefix_suffix = "%s %s" % (self.prefix, self.suffix)
|
||||||
|
if "%(range_day)s" in prefix_suffix:
|
||||||
|
date_from = date_obj
|
||||||
|
date_to = date_obj
|
||||||
|
elif "%(range_month)s" in prefix_suffix:
|
||||||
|
date_from = fields.Date.start_of(date_obj, "month")
|
||||||
|
date_to = fields.Date.end_of(date_obj, "month")
|
||||||
|
else:
|
||||||
|
date_from = fields.Date.start_of(date_obj, "year")
|
||||||
|
date_to = fields.Date.end_of(date_obj, "year")
|
||||||
|
date_range = sequence_range.search(
|
||||||
|
[
|
||||||
|
("sequence_id", "=", self.id),
|
||||||
|
("date_from", ">=", date),
|
||||||
|
("date_from", "<=", date_to),
|
||||||
|
],
|
||||||
|
order="date_from desc",
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
if date_range:
|
||||||
|
date_to = fields.Date.subtract(date_range.date_from, days=1)
|
||||||
|
date_range = sequence_range.search(
|
||||||
|
[
|
||||||
|
("sequence_id", "=", self.id),
|
||||||
|
("date_to", ">=", date_from),
|
||||||
|
("date_to", "<=", date),
|
||||||
|
],
|
||||||
|
order="date_to desc",
|
||||||
|
limit=1,
|
||||||
|
)
|
||||||
|
if date_range:
|
||||||
|
date_to = fields.Date.add(date_range.date_to, days=1)
|
||||||
|
sequence_range_vals = {
|
||||||
|
"date_from": date_from,
|
||||||
|
"date_to": date_to,
|
||||||
|
"sequence_id": self.id,
|
||||||
|
}
|
||||||
|
seq_date_range = sequence_range.sudo().create(sequence_range_vals)
|
||||||
|
return seq_date_range
|
32
account_move_name_sequence/post_install.py
Normal file
32
account_move_name_sequence/post_install.py
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
# Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||||
|
# Copyright 2022 Vauxoo (https://www.vauxoo.com/)
|
||||||
|
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
# @author: Moisés López <moylop260@vauxoo.com>
|
||||||
|
# @author: Francisco Luna <fluna@vauxoo.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
|
||||||
|
from odoo import SUPERUSER_ID, api
|
||||||
|
|
||||||
|
|
||||||
|
def create_journal_sequences(cr, registry):
|
||||||
|
env = api.Environment(cr, SUPERUSER_ID, {})
|
||||||
|
journals = (
|
||||||
|
env["account.journal"]
|
||||||
|
.with_context(active_test=False)
|
||||||
|
.search([("sequence_id", "=", False)])
|
||||||
|
)
|
||||||
|
for journal in journals:
|
||||||
|
journal_vals = {
|
||||||
|
"code": journal.code,
|
||||||
|
"name": journal.name,
|
||||||
|
"company_id": journal.company_id.id,
|
||||||
|
}
|
||||||
|
seq_vals = journal._prepare_sequence(journal_vals)
|
||||||
|
seq_vals.update(journal._prepare_sequence_current_moves())
|
||||||
|
vals = {"sequence_id": env["ir.sequence"].create(seq_vals).id}
|
||||||
|
if journal.type in ("sale", "purchase") and journal.refund_sequence:
|
||||||
|
rseq_vals = journal._prepare_sequence(journal_vals, refund=True)
|
||||||
|
rseq_vals.update(journal._prepare_sequence_current_moves(refund=True))
|
||||||
|
vals["refund_sequence_id"] = env["ir.sequence"].create(rseq_vals).id
|
||||||
|
journal.write(vals)
|
||||||
|
return
|
5
account_move_name_sequence/readme/CONFIGURE.rst
Normal file
5
account_move_name_sequence/readme/CONFIGURE.rst
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
On the form view of an account journal, in the first tab, there is a many2one link to the sequence. When you create a new journal, you can keep this field empty and a new sequence will be automatically created when you save the journal.
|
||||||
|
|
||||||
|
On sale and purchase journals, you have an additional option to have another sequence dedicated to refunds.
|
||||||
|
|
||||||
|
Upon module installation, all existing journals will be updated with a journal entry sequence (and also a credit note sequence for sale and purchase journals). You should update the configuration of the sequences to fit your needs. You can uncheck the option *Dedicated Credit Note Sequence* on existing sale and purchase journals if you don't want it. For the journals which already have journal entries, you should update the sequence configuration to avoid a discontinuity in the numbering for the next journal entry.
|
12
account_move_name_sequence/readme/CONTRIBUTORS.rst
Normal file
12
account_move_name_sequence/readme/CONTRIBUTORS.rst
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
* `Akretion <https://www.akretion.com>`_:
|
||||||
|
|
||||||
|
* Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
|
||||||
|
* `Vauxoo <https://www.vauxoo.com>`_:
|
||||||
|
|
||||||
|
* Moisés López <moylop260@vauxoo.com>
|
||||||
|
* Francisco Luna <fluna@vauxoo.com>
|
||||||
|
|
||||||
|
* `Factor Libre <https://www.factorlibre.com>`_:
|
||||||
|
|
||||||
|
* Rodrigo Bonilla Martinez <rodrigo.bonilla@factorlibre.com>
|
62
account_move_name_sequence/readme/DESCRIPTION.rst
Normal file
62
account_move_name_sequence/readme/DESCRIPTION.rst
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
In Odoo version 13.0 and previous versions, the number of journal entries was generated from a sequence configured on the journal.
|
||||||
|
|
||||||
|
In Odoo version 14.0, the number of journal entries can be manually set by the user. Then, the number attributed for the next journal entries in the same journal is computed by a complex piece of code that guesses the format of the journal entry number from the number of the journal entry which was manually entered by the user. It has several drawbacks:
|
||||||
|
|
||||||
|
* the available options for the sequence are limited,
|
||||||
|
* it is not possible to configure the sequence in advance before the deployment in production,
|
||||||
|
* as it is error-prone, they added a *Resequence* wizard to re-generate the journal entry numbers, which can be considered as illegal in many countries,
|
||||||
|
* the `piece of code <https://github.com/odoo/odoo/blob/14.0/addons/account/models/sequence_mixin.py>`_ that handles this is not easy to understand and quite difficult to debug.
|
||||||
|
|
||||||
|
Odoo>=v14.0 raises new concurrency issues since it locks the last journal entry of the journal to get the new number causing a bottleneck
|
||||||
|
Even if you only are creating a draft journal entry it locks the last one
|
||||||
|
It applies to all accounting Journal Entries
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
- Customer Invoices
|
||||||
|
- Credit Notes
|
||||||
|
- Customer Payments
|
||||||
|
- Vendor Bills
|
||||||
|
- Vendor Refunds
|
||||||
|
- Vendor Payment
|
||||||
|
- Manual Journal Entries
|
||||||
|
|
||||||
|
Then, the following concurrency errors are being raised now frequently:
|
||||||
|
|
||||||
|
* Editing the last record used to get the new number from another process
|
||||||
|
* Creating a new draft invoice/payment (not only when posting it)
|
||||||
|
* Creating a transaction to create an invoice then payment or vice versa raises a deadlock error
|
||||||
|
* Reconciling the last record it could be a heavy process
|
||||||
|
* Creating 2 or more Invoices/Bills at the same time
|
||||||
|
* Creating 2 or more Payments at the same time (Even if your country allows to relax gaps in these kinds of documents, you are not able anymore to change the implementation to standard)
|
||||||
|
* Creating 2 or more Journal Entries at the same time
|
||||||
|
|
||||||
|
|
||||||
|
All these increases in concurrency errors bring more issues since that Odoo is not prepared:
|
||||||
|
|
||||||
|
* Using e-commerce, configured with Invoicing Policy Ordered and Automatic Invoice, the portal users will see errors in the checkout even if the payment was done, the sale order could be in state draft and request a new payment, so double charges
|
||||||
|
* Using `subscription_template.payment_mode=success_payment` you will see subscriptions with tag "payment exception"
|
||||||
|
* Using accounting creating invoice or payment, you will see errors then you will need to start the process again and again until you get the lock before another user
|
||||||
|
* The workers could be used for more time than before since that it could be waiting for release so less concurrent users supported or loading page is shown more frequently affecting the performance
|
||||||
|
|
||||||
|
The new accounting number is a significant bottleneck
|
||||||
|
|
||||||
|
.. image:: https://media.istockphoto.com/vectors/road-highways-with-many-different-vehicles-vector-id1328678690
|
||||||
|
|
||||||
|
|
||||||
|
If you do not believe all these issues are occurring, we have created the following issues and unittest to reproduce errors in v14.0 including the deadlock, but not v13.0:
|
||||||
|
|
||||||
|
- Passing unittest for `13.0 - [REF] account: Adding unittests for concurrency issues in account_move sequences <https://github.com/odoo/odoo/pull/91614>`_
|
||||||
|
- Concurrency errors for `14.0 - [REF] account: Adding unittests for concurrency issues in account_move sequences <https://github.com/odoo/odoo/pull/91525>`_
|
||||||
|
- `Stress testing and issue reported to Odoo <https://github.com/odoo/odoo/issues/90465>`_
|
||||||
|
- `[BUG] account: Concurrency errors increased considerably in account.move for Odoo>=v14.0 #91873 <https://github.com/odoo/odoo/issues/91873>`_
|
||||||
|
|
||||||
|
|
||||||
|
Using this module, you can configure what kind of documents the gap sequence may be relaxed
|
||||||
|
And even if you must use no-gap in your company or country it will reduce the concurrency issues since the module is using an extra table (ir_sequence) instead of locking the last record
|
||||||
|
|
||||||
|
For those like me who think that the implementation before Odoo v14.0 was much better, for the accountants who think it should not be possible to manually enter the sequence of a customer invoice, for the auditor who considers that resequencing journal entries is prohibited by law, this module may be a solution to get out of the nightmare.
|
||||||
|
|
||||||
|
The field names used in this module to configure the sequence on the journal are exactly the same as in Odoo version 13.0 and previous versions. That way, if you migrate to Odoo version 14.0 and you install this module immediately after the migration, you should keep the previous behavior and the same sequences will continue to be used.
|
||||||
|
|
||||||
|
The module removes access to the *Resequence* wizard on journal entries.
|
2
account_move_name_sequence/security/ir.model.access.csv
Normal file
2
account_move_name_sequence/security/ir.model.access.csv
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
|
account.access_account_resequence,Remove rights on account.resequence.wizard,account.model_account_resequence_wizard,account.group_account_manager,0,0,0,0
|
|
BIN
account_move_name_sequence/static/description/icon.png
Normal file
BIN
account_move_name_sequence/static/description/icon.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
486
account_move_name_sequence/static/description/index.html
Normal file
486
account_move_name_sequence/static/description/index.html
Normal file
@ -0,0 +1,486 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||||
|
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
|
||||||
|
<meta name="generator" content="Docutils 0.15.1: http://docutils.sourceforge.net/" />
|
||||||
|
<title>Account Move Number Sequence</title>
|
||||||
|
<style type="text/css">
|
||||||
|
|
||||||
|
/*
|
||||||
|
:Author: David Goodger (goodger@python.org)
|
||||||
|
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
|
||||||
|
:Copyright: This stylesheet has been placed in the public domain.
|
||||||
|
|
||||||
|
Default cascading style sheet for the HTML output of Docutils.
|
||||||
|
|
||||||
|
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
|
||||||
|
customize this style sheet.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* used to remove borders from tables and images */
|
||||||
|
.borderless, table.borderless td, table.borderless th {
|
||||||
|
border: 0 }
|
||||||
|
|
||||||
|
table.borderless td, table.borderless th {
|
||||||
|
/* Override padding for "table.docutils td" with "! important".
|
||||||
|
The right padding separates the table cells. */
|
||||||
|
padding: 0 0.5em 0 0 ! important }
|
||||||
|
|
||||||
|
.first {
|
||||||
|
/* Override more specific margin styles with "! important". */
|
||||||
|
margin-top: 0 ! important }
|
||||||
|
|
||||||
|
.last, .with-subtitle {
|
||||||
|
margin-bottom: 0 ! important }
|
||||||
|
|
||||||
|
.hidden {
|
||||||
|
display: none }
|
||||||
|
|
||||||
|
.subscript {
|
||||||
|
vertical-align: sub;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
.superscript {
|
||||||
|
vertical-align: super;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
a.toc-backref {
|
||||||
|
text-decoration: none ;
|
||||||
|
color: black }
|
||||||
|
|
||||||
|
blockquote.epigraph {
|
||||||
|
margin: 2em 5em ; }
|
||||||
|
|
||||||
|
dl.docutils dd {
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Uncomment (and remove this text!) to get bold-faced definition list terms
|
||||||
|
dl.docutils dt {
|
||||||
|
font-weight: bold }
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.abstract {
|
||||||
|
margin: 2em 5em }
|
||||||
|
|
||||||
|
div.abstract p.topic-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
div.admonition, div.attention, div.caution, div.danger, div.error,
|
||||||
|
div.hint, div.important, div.note, div.tip, div.warning {
|
||||||
|
margin: 2em ;
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em }
|
||||||
|
|
||||||
|
div.admonition p.admonition-title, div.hint p.admonition-title,
|
||||||
|
div.important p.admonition-title, div.note p.admonition-title,
|
||||||
|
div.tip p.admonition-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
div.attention p.admonition-title, div.caution p.admonition-title,
|
||||||
|
div.danger p.admonition-title, div.error p.admonition-title,
|
||||||
|
div.warning p.admonition-title, .code .error {
|
||||||
|
color: red ;
|
||||||
|
font-weight: bold ;
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
/* Uncomment (and remove this text!) to get reduced vertical space in
|
||||||
|
compound paragraphs.
|
||||||
|
div.compound .compound-first, div.compound .compound-middle {
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
div.compound .compound-last, div.compound .compound-middle {
|
||||||
|
margin-top: 0.5em }
|
||||||
|
*/
|
||||||
|
|
||||||
|
div.dedication {
|
||||||
|
margin: 2em 5em ;
|
||||||
|
text-align: center ;
|
||||||
|
font-style: italic }
|
||||||
|
|
||||||
|
div.dedication p.topic-title {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-style: normal }
|
||||||
|
|
||||||
|
div.figure {
|
||||||
|
margin-left: 2em ;
|
||||||
|
margin-right: 2em }
|
||||||
|
|
||||||
|
div.footer, div.header {
|
||||||
|
clear: both;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
div.line-block {
|
||||||
|
display: block ;
|
||||||
|
margin-top: 1em ;
|
||||||
|
margin-bottom: 1em }
|
||||||
|
|
||||||
|
div.line-block div.line-block {
|
||||||
|
margin-top: 0 ;
|
||||||
|
margin-bottom: 0 ;
|
||||||
|
margin-left: 1.5em }
|
||||||
|
|
||||||
|
div.sidebar {
|
||||||
|
margin: 0 0 0.5em 1em ;
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em ;
|
||||||
|
background-color: #ffffee ;
|
||||||
|
width: 40% ;
|
||||||
|
float: right ;
|
||||||
|
clear: right }
|
||||||
|
|
||||||
|
div.sidebar p.rubric {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-size: medium }
|
||||||
|
|
||||||
|
div.system-messages {
|
||||||
|
margin: 5em }
|
||||||
|
|
||||||
|
div.system-messages h1 {
|
||||||
|
color: red }
|
||||||
|
|
||||||
|
div.system-message {
|
||||||
|
border: medium outset ;
|
||||||
|
padding: 1em }
|
||||||
|
|
||||||
|
div.system-message p.system-message-title {
|
||||||
|
color: red ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
div.topic {
|
||||||
|
margin: 2em }
|
||||||
|
|
||||||
|
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
|
||||||
|
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
|
||||||
|
margin-top: 0.4em }
|
||||||
|
|
||||||
|
h1.title {
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
h2.subtitle {
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
hr.docutils {
|
||||||
|
width: 75% }
|
||||||
|
|
||||||
|
img.align-left, .figure.align-left, object.align-left, table.align-left {
|
||||||
|
clear: left ;
|
||||||
|
float: left ;
|
||||||
|
margin-right: 1em }
|
||||||
|
|
||||||
|
img.align-right, .figure.align-right, object.align-right, table.align-right {
|
||||||
|
clear: right ;
|
||||||
|
float: right ;
|
||||||
|
margin-left: 1em }
|
||||||
|
|
||||||
|
img.align-center, .figure.align-center, object.align-center {
|
||||||
|
display: block;
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.align-center {
|
||||||
|
margin-left: auto;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.align-left {
|
||||||
|
text-align: left }
|
||||||
|
|
||||||
|
.align-center {
|
||||||
|
clear: both ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
.align-right {
|
||||||
|
text-align: right }
|
||||||
|
|
||||||
|
/* reset inner alignment in figures */
|
||||||
|
div.align-right {
|
||||||
|
text-align: inherit }
|
||||||
|
|
||||||
|
/* div.align-center * { */
|
||||||
|
/* text-align: left } */
|
||||||
|
|
||||||
|
.align-top {
|
||||||
|
vertical-align: top }
|
||||||
|
|
||||||
|
.align-middle {
|
||||||
|
vertical-align: middle }
|
||||||
|
|
||||||
|
.align-bottom {
|
||||||
|
vertical-align: bottom }
|
||||||
|
|
||||||
|
ol.simple, ul.simple {
|
||||||
|
margin-bottom: 1em }
|
||||||
|
|
||||||
|
ol.arabic {
|
||||||
|
list-style: decimal }
|
||||||
|
|
||||||
|
ol.loweralpha {
|
||||||
|
list-style: lower-alpha }
|
||||||
|
|
||||||
|
ol.upperalpha {
|
||||||
|
list-style: upper-alpha }
|
||||||
|
|
||||||
|
ol.lowerroman {
|
||||||
|
list-style: lower-roman }
|
||||||
|
|
||||||
|
ol.upperroman {
|
||||||
|
list-style: upper-roman }
|
||||||
|
|
||||||
|
p.attribution {
|
||||||
|
text-align: right ;
|
||||||
|
margin-left: 50% }
|
||||||
|
|
||||||
|
p.caption {
|
||||||
|
font-style: italic }
|
||||||
|
|
||||||
|
p.credits {
|
||||||
|
font-style: italic ;
|
||||||
|
font-size: smaller }
|
||||||
|
|
||||||
|
p.label {
|
||||||
|
white-space: nowrap }
|
||||||
|
|
||||||
|
p.rubric {
|
||||||
|
font-weight: bold ;
|
||||||
|
font-size: larger ;
|
||||||
|
color: maroon ;
|
||||||
|
text-align: center }
|
||||||
|
|
||||||
|
p.sidebar-title {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold ;
|
||||||
|
font-size: larger }
|
||||||
|
|
||||||
|
p.sidebar-subtitle {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
p.topic-title {
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
pre.address {
|
||||||
|
margin-bottom: 0 ;
|
||||||
|
margin-top: 0 ;
|
||||||
|
font: inherit }
|
||||||
|
|
||||||
|
pre.literal-block, pre.doctest-block, pre.math, pre.code {
|
||||||
|
margin-left: 2em ;
|
||||||
|
margin-right: 2em }
|
||||||
|
|
||||||
|
pre.code .ln { color: grey; } /* line numbers */
|
||||||
|
pre.code, code { background-color: #eeeeee }
|
||||||
|
pre.code .comment, code .comment { color: #5C6576 }
|
||||||
|
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
|
||||||
|
pre.code .literal.string, code .literal.string { color: #0C5404 }
|
||||||
|
pre.code .name.builtin, code .name.builtin { color: #352B84 }
|
||||||
|
pre.code .deleted, code .deleted { background-color: #DEB0A1}
|
||||||
|
pre.code .inserted, code .inserted { background-color: #A3D289}
|
||||||
|
|
||||||
|
span.classifier {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-style: oblique }
|
||||||
|
|
||||||
|
span.classifier-delimiter {
|
||||||
|
font-family: sans-serif ;
|
||||||
|
font-weight: bold }
|
||||||
|
|
||||||
|
span.interpreted {
|
||||||
|
font-family: sans-serif }
|
||||||
|
|
||||||
|
span.option {
|
||||||
|
white-space: nowrap }
|
||||||
|
|
||||||
|
span.pre {
|
||||||
|
white-space: pre }
|
||||||
|
|
||||||
|
span.problematic {
|
||||||
|
color: red }
|
||||||
|
|
||||||
|
span.section-subtitle {
|
||||||
|
/* font-size relative to parent (h1..h6 element) */
|
||||||
|
font-size: 80% }
|
||||||
|
|
||||||
|
table.citation {
|
||||||
|
border-left: solid 1px gray;
|
||||||
|
margin-left: 1px }
|
||||||
|
|
||||||
|
table.docinfo {
|
||||||
|
margin: 2em 4em }
|
||||||
|
|
||||||
|
table.docutils {
|
||||||
|
margin-top: 0.5em ;
|
||||||
|
margin-bottom: 0.5em }
|
||||||
|
|
||||||
|
table.footnote {
|
||||||
|
border-left: solid 1px black;
|
||||||
|
margin-left: 1px }
|
||||||
|
|
||||||
|
table.docutils td, table.docutils th,
|
||||||
|
table.docinfo td, table.docinfo th {
|
||||||
|
padding-left: 0.5em ;
|
||||||
|
padding-right: 0.5em ;
|
||||||
|
vertical-align: top }
|
||||||
|
|
||||||
|
table.docutils th.field-name, table.docinfo th.docinfo-name {
|
||||||
|
font-weight: bold ;
|
||||||
|
text-align: left ;
|
||||||
|
white-space: nowrap ;
|
||||||
|
padding-left: 0 }
|
||||||
|
|
||||||
|
/* "booktabs" style (no vertical lines) */
|
||||||
|
table.docutils.booktabs {
|
||||||
|
border: 0px;
|
||||||
|
border-top: 2px solid;
|
||||||
|
border-bottom: 2px solid;
|
||||||
|
border-collapse: collapse;
|
||||||
|
}
|
||||||
|
table.docutils.booktabs * {
|
||||||
|
border: 0px;
|
||||||
|
}
|
||||||
|
table.docutils.booktabs th {
|
||||||
|
border-bottom: thin solid;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
|
||||||
|
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
|
||||||
|
font-size: 100% }
|
||||||
|
|
||||||
|
ul.auto-toc {
|
||||||
|
list-style-type: none }
|
||||||
|
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="document" id="account-move-number-sequence">
|
||||||
|
<h1 class="title">Account Move Number Sequence</h1>
|
||||||
|
|
||||||
|
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||||
|
!! This file is generated by oca-gen-addon-readme !!
|
||||||
|
!! changes will be overwritten. !!
|
||||||
|
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||||
|
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/15.0/account_move_name_sequence"><img alt="OCA/account-financial-tools" src="https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/account-financial-tools-15-0/account-financial-tools-15-0-account_move_name_sequence"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/92/15.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
|
||||||
|
<p>In Odoo version 13.0 and previous versions, the number of journal entries was generated from a sequence configured on the journal.</p>
|
||||||
|
<p>In Odoo version 14.0, the number of journal entries can be manually set by the user. Then, the number attributed for the next journal entries in the same journal is computed by a complex piece of code that guesses the format of the journal entry number from the number of the journal entry which was manually entered by the user. It has several drawbacks:</p>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>the available options for the sequence are limited,</li>
|
||||||
|
<li>it is not possible to configure the sequence in advance before the deployment in production,</li>
|
||||||
|
<li>as it is error-prone, they added a <em>Resequence</em> wizard to re-generate the journal entry numbers, which can be considered as illegal in many countries,</li>
|
||||||
|
<li>the <a class="reference external" href="https://github.com/odoo/odoo/blob/14.0/addons/account/models/sequence_mixin.py">piece of code</a> that handles this is not easy to understand and quite difficult to debug.</li>
|
||||||
|
</ul>
|
||||||
|
<p>Odoo>=v14.0 raises new concurrency issues since it locks the last journal entry of the journal to get the new number causing a bottleneck
|
||||||
|
Even if you only are creating a draft journal entry it locks the last one
|
||||||
|
It applies to all accounting Journal Entries</p>
|
||||||
|
<p>e.g.</p>
|
||||||
|
<blockquote>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Customer Invoices</li>
|
||||||
|
<li>Credit Notes</li>
|
||||||
|
<li>Customer Payments</li>
|
||||||
|
<li>Vendor Bills</li>
|
||||||
|
<li>Vendor Refunds</li>
|
||||||
|
<li>Vendor Payment</li>
|
||||||
|
<li>Manual Journal Entries</li>
|
||||||
|
</ul>
|
||||||
|
</blockquote>
|
||||||
|
<p>Then, the following concurrency errors are being raised now frequently:</p>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Editing the last record used to get the new number from another process</li>
|
||||||
|
<li>Creating a new draft invoice/payment (not only when posting it)</li>
|
||||||
|
<li>Creating a transaction to create an invoice then payment or vice versa raises a deadlock error</li>
|
||||||
|
<li>Reconciling the last record it could be a heavy process</li>
|
||||||
|
<li>Creating 2 or more Invoices/Bills at the same time</li>
|
||||||
|
<li>Creating 2 or more Payments at the same time (Even if your country allows to relax gaps in these kinds of documents, you are not able anymore to change the implementation to standard)</li>
|
||||||
|
<li>Creating 2 or more Journal Entries at the same time</li>
|
||||||
|
</ul>
|
||||||
|
<p>All these increases in concurrency errors bring more issues since that Odoo is not prepared:</p>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Using e-commerce, configured with Invoicing Policy Ordered and Automatic Invoice, the portal users will see errors in the checkout even if the payment was done, the sale order could be in state draft and request a new payment, so double charges</li>
|
||||||
|
<li>Using <cite>subscription_template.payment_mode=success_payment</cite> you will see subscriptions with tag “payment exception”</li>
|
||||||
|
<li>Using accounting creating invoice or payment, you will see errors then you will need to start the process again and again until you get the lock before another user</li>
|
||||||
|
<li>The workers could be used for more time than before since that it could be waiting for release so less concurrent users supported or loading page is shown more frequently affecting the performance</li>
|
||||||
|
</ul>
|
||||||
|
<p>The new accounting number is a significant bottleneck</p>
|
||||||
|
<img alt="https://media.istockphoto.com/vectors/road-highways-with-many-different-vehicles-vector-id1328678690" src="https://media.istockphoto.com/vectors/road-highways-with-many-different-vehicles-vector-id1328678690" />
|
||||||
|
<p>If you do not believe all these issues are occurring, we have created the following issues and unittest to reproduce errors in v14.0 including the deadlock, but not v13.0:</p>
|
||||||
|
<blockquote>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Passing unittest for <a class="reference external" href="https://github.com/odoo/odoo/pull/91614">13.0 - [REF] account: Adding unittests for concurrency issues in account_move sequences</a></li>
|
||||||
|
<li>Concurrency errors for <a class="reference external" href="https://github.com/odoo/odoo/pull/91525">14.0 - [REF] account: Adding unittests for concurrency issues in account_move sequences</a></li>
|
||||||
|
<li><a class="reference external" href="https://github.com/odoo/odoo/issues/90465">Stress testing and issue reported to Odoo</a></li>
|
||||||
|
<li><a class="reference external" href="https://github.com/odoo/odoo/issues/91873">[BUG] account: Concurrency errors increased considerably in account.move for Odoo>=v14.0 #91873</a></li>
|
||||||
|
</ul>
|
||||||
|
</blockquote>
|
||||||
|
<p>Using this module, you can configure what kind of documents the gap sequence may be relaxed
|
||||||
|
And even if you must use no-gap in your company or country it will reduce the concurrency issues since the module is using an extra table (ir_sequence) instead of locking the last record</p>
|
||||||
|
<p>For those like me who think that the implementation before Odoo v14.0 was much better, for the accountants who think it should not be possible to manually enter the sequence of a customer invoice, for the auditor who considers that resequencing journal entries is prohibited by law, this module may be a solution to get out of the nightmare.</p>
|
||||||
|
<p>The field names used in this module to configure the sequence on the journal are exactly the same as in Odoo version 13.0 and previous versions. That way, if you migrate to Odoo version 14.0 and you install this module immediately after the migration, you should keep the previous behavior and the same sequences will continue to be used.</p>
|
||||||
|
<p>The module removes access to the <em>Resequence</em> wizard on journal entries.</p>
|
||||||
|
<p><strong>Table of contents</strong></p>
|
||||||
|
<div class="contents local topic" id="contents">
|
||||||
|
<ul class="simple">
|
||||||
|
<li><a class="reference internal" href="#configuration" id="id2">Configuration</a></li>
|
||||||
|
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
|
||||||
|
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
|
||||||
|
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
|
||||||
|
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
|
||||||
|
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="configuration">
|
||||||
|
<h1><a class="toc-backref" href="#id2">Configuration</a></h1>
|
||||||
|
<p>On the form view of an account journal, in the first tab, there is a many2one link to the sequence. When you create a new journal, you can keep this field empty and a new sequence will be automatically created when you save the journal.</p>
|
||||||
|
<p>On sale and purchase journals, you have an additional option to have another sequence dedicated to refunds.</p>
|
||||||
|
<p>Upon module installation, all existing journals will be updated with a journal entry sequence (and also a credit note sequence for sale and purchase journals). You should update the configuration of the sequences to fit your needs. You can uncheck the option <em>Dedicated Credit Note Sequence</em> on existing sale and purchase journals if you don’t want it. For the journals which already have journal entries, you should update the sequence configuration to avoid a discontinuity in the numbering for the next journal entry.</p>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="bug-tracker">
|
||||||
|
<h1><a class="toc-backref" href="#id3">Bug Tracker</a></h1>
|
||||||
|
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-financial-tools/issues">GitHub Issues</a>.
|
||||||
|
In case of trouble, please check there if your issue has already been reported.
|
||||||
|
If you spotted it first, help us smashing it by providing a detailed and welcomed
|
||||||
|
<a class="reference external" href="https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_move_name_sequence%0Aversion:%2015.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
|
||||||
|
<p>Do not contact contributors directly about support or help with technical issues.</p>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="credits">
|
||||||
|
<h1><a class="toc-backref" href="#id4">Credits</a></h1>
|
||||||
|
<div class="section" id="authors">
|
||||||
|
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Akretion</li>
|
||||||
|
<li>Vauxoo</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="contributors">
|
||||||
|
<h2><a class="toc-backref" href="#id6">Contributors</a></h2>
|
||||||
|
<ul class="simple">
|
||||||
|
<li>Alexis de Lattre <<a class="reference external" href="mailto:alexis.delattre@akretion.com">alexis.delattre@akretion.com</a>></li>
|
||||||
|
<li>Moisés López <<a class="reference external" href="mailto:moylop260@vauxoo.com">moylop260@vauxoo.com</a>></li>
|
||||||
|
<li>Francisco Luna <<a class="reference external" href="mailto:fluna@vauxoo.com">fluna@vauxoo.com</a>></li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="section" id="maintainers">
|
||||||
|
<h2><a class="toc-backref" href="#id7">Maintainers</a></h2>
|
||||||
|
<p>This module is maintained by the OCA.</p>
|
||||||
|
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
|
||||||
|
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||||
|
mission is to support the collaborative development of Odoo features and
|
||||||
|
promote its widespread use.</p>
|
||||||
|
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainers</a>:</p>
|
||||||
|
<p><a class="reference external" href="https://github.com/alexis-via"><img alt="alexis-via" src="https://github.com/alexis-via.png?size=40px" /></a> <a class="reference external" href="https://github.com/moylop260"><img alt="moylop260" src="https://github.com/moylop260.png?size=40px" /></a> <a class="reference external" href="https://github.com/frahikLV"><img alt="frahikLV" src="https://github.com/frahikLV.png?size=40px" /></a></p>
|
||||||
|
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/15.0/account_move_name_sequence">OCA/account-financial-tools</a> project on GitHub.</p>
|
||||||
|
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
1
account_move_name_sequence/tests/__init__.py
Normal file
1
account_move_name_sequence/tests/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
from . import test_account_move_name_seq
|
339
account_move_name_sequence/tests/test_account_move_name_seq.py
Normal file
339
account_move_name_sequence/tests/test_account_move_name_seq.py
Normal file
@ -0,0 +1,339 @@
|
|||||||
|
# Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||||
|
# @author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
# @author: Moisés López <moylop260@vauxoo.com>
|
||||||
|
# @author: Francisco Luna <fluna@vauxoo.com>
|
||||||
|
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from freezegun import freeze_time
|
||||||
|
|
||||||
|
from odoo import fields
|
||||||
|
from odoo.exceptions import UserError, ValidationError
|
||||||
|
from odoo.tests import tagged
|
||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
|
||||||
|
@tagged("post_install", "-at_install")
|
||||||
|
class TestAccountMoveNameSequence(TransactionCase):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
self.company = self.env.ref("base.main_company")
|
||||||
|
self.misc_journal = self.env["account.journal"].create(
|
||||||
|
{
|
||||||
|
"name": "Test Journal Move name seq",
|
||||||
|
"code": "ADLM",
|
||||||
|
"type": "general",
|
||||||
|
"company_id": self.company.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.purchase_journal = self.env["account.journal"].create(
|
||||||
|
{
|
||||||
|
"name": "Test Purchase Journal Move name seq",
|
||||||
|
"code": "ADLP",
|
||||||
|
"type": "purchase",
|
||||||
|
"company_id": self.company.id,
|
||||||
|
"refund_sequence": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.accounts = self.env["account.account"].search(
|
||||||
|
[("company_id", "=", self.company.id)], limit=2
|
||||||
|
)
|
||||||
|
self.account1 = self.accounts[0]
|
||||||
|
self.account2 = self.accounts[1]
|
||||||
|
self.date = datetime.now()
|
||||||
|
|
||||||
|
def test_seq_creation(self):
|
||||||
|
self.assertTrue(self.misc_journal.sequence_id)
|
||||||
|
seq = self.misc_journal.sequence_id
|
||||||
|
self.assertEqual(seq.company_id, self.company)
|
||||||
|
self.assertEqual(seq.implementation, "no_gap")
|
||||||
|
self.assertEqual(seq.padding, 4)
|
||||||
|
self.assertTrue(seq.use_date_range)
|
||||||
|
self.assertTrue(self.purchase_journal.sequence_id)
|
||||||
|
self.assertTrue(self.purchase_journal.refund_sequence_id)
|
||||||
|
seq = self.purchase_journal.refund_sequence_id
|
||||||
|
self.assertEqual(seq.company_id, self.company)
|
||||||
|
self.assertEqual(seq.implementation, "no_gap")
|
||||||
|
self.assertEqual(seq.padding, 4)
|
||||||
|
self.assertTrue(seq.use_date_range)
|
||||||
|
|
||||||
|
def test_misc_move_name(self):
|
||||||
|
move = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"date": self.date,
|
||||||
|
"journal_id": self.misc_journal.id,
|
||||||
|
"line_ids": [
|
||||||
|
(0, 0, {"account_id": self.account1.id, "debit": 10}),
|
||||||
|
(0, 0, {"account_id": self.account2.id, "credit": 10}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(move.name, "/")
|
||||||
|
move.action_post()
|
||||||
|
seq = self.misc_journal.sequence_id
|
||||||
|
move_name = "%s%s" % (seq.prefix, "1".zfill(seq.padding))
|
||||||
|
move_name = move_name.replace("%(range_year)s", str(self.date.year))
|
||||||
|
self.assertEqual(move.name, move_name)
|
||||||
|
self.assertTrue(seq.date_range_ids)
|
||||||
|
drange_count = self.env["ir.sequence.date_range"].search_count(
|
||||||
|
[
|
||||||
|
("sequence_id", "=", seq.id),
|
||||||
|
("date_from", "=", fields.Date.add(self.date, month=1, day=1)),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
self.assertEqual(drange_count, 1)
|
||||||
|
move.button_draft()
|
||||||
|
move.action_post()
|
||||||
|
self.assertEqual(move.name, move_name)
|
||||||
|
|
||||||
|
def test_prefix_move_name_use_move_date(self):
|
||||||
|
seq = self.misc_journal.sequence_id
|
||||||
|
seq.prefix = "TEST-%(year)s-%(month)s-"
|
||||||
|
self.env["ir.sequence.date_range"].sudo().create(
|
||||||
|
{
|
||||||
|
"date_from": "2021-07-01",
|
||||||
|
"date_to": "2022-06-30",
|
||||||
|
"sequence_id": seq.id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
with freeze_time("2022-01-01"):
|
||||||
|
move = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"date": "2021-12-31",
|
||||||
|
"journal_id": self.misc_journal.id,
|
||||||
|
"line_ids": [
|
||||||
|
(0, 0, {"account_id": self.account1.id, "debit": 10}),
|
||||||
|
(0, 0, {"account_id": self.account2.id, "credit": 10}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
move.action_post()
|
||||||
|
self.assertEqual(move.name, "TEST-2021-12-0001")
|
||||||
|
with freeze_time("2022-01-01"):
|
||||||
|
move = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"date": "2022-06-30",
|
||||||
|
"journal_id": self.misc_journal.id,
|
||||||
|
"line_ids": [
|
||||||
|
(0, 0, {"account_id": self.account1.id, "debit": 10}),
|
||||||
|
(0, 0, {"account_id": self.account2.id, "credit": 10}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
move.action_post()
|
||||||
|
self.assertEqual(move.name, "TEST-2022-06-0002")
|
||||||
|
|
||||||
|
with freeze_time("2022-01-01"):
|
||||||
|
move = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"date": "2022-07-01",
|
||||||
|
"journal_id": self.misc_journal.id,
|
||||||
|
"line_ids": [
|
||||||
|
(0, 0, {"account_id": self.account1.id, "debit": 10}),
|
||||||
|
(0, 0, {"account_id": self.account2.id, "credit": 10}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
move.action_post()
|
||||||
|
self.assertEqual(move.name, "TEST-2022-07-0001")
|
||||||
|
|
||||||
|
def test_in_invoice_and_refund(self):
|
||||||
|
in_invoice = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"journal_id": self.purchase_journal.id,
|
||||||
|
"invoice_date": self.date,
|
||||||
|
"partner_id": self.env.ref("base.res_partner_3").id,
|
||||||
|
"move_type": "in_invoice",
|
||||||
|
"invoice_line_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"account_id": self.account1.id,
|
||||||
|
"price_unit": 42.0,
|
||||||
|
"quantity": 12,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"account_id": self.account1.id,
|
||||||
|
"price_unit": 48.0,
|
||||||
|
"quantity": 10,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(in_invoice.name, "/")
|
||||||
|
in_invoice.action_post()
|
||||||
|
|
||||||
|
move_reversal = (
|
||||||
|
self.env["account.move.reversal"]
|
||||||
|
.with_context(active_model="account.move", active_ids=in_invoice.ids)
|
||||||
|
.create(
|
||||||
|
{
|
||||||
|
"journal_id": in_invoice.journal_id.id,
|
||||||
|
"reason": "no reason",
|
||||||
|
"refund_method": "cancel",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
reversal = move_reversal.reverse_moves()
|
||||||
|
reversed_move = self.env["account.move"].browse(reversal["res_id"])
|
||||||
|
self.assertTrue(reversed_move)
|
||||||
|
self.assertEqual(reversed_move.state, "posted")
|
||||||
|
|
||||||
|
in_invoice = in_invoice.copy(
|
||||||
|
{
|
||||||
|
"invoice_date": self.date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
in_invoice.action_post()
|
||||||
|
|
||||||
|
move_reversal = (
|
||||||
|
self.env["account.move.reversal"]
|
||||||
|
.with_context(active_model="account.move", active_ids=in_invoice.ids)
|
||||||
|
.create(
|
||||||
|
{
|
||||||
|
"journal_id": in_invoice.journal_id.id,
|
||||||
|
"reason": "no reason",
|
||||||
|
"refund_method": "modify",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
reversal = move_reversal.reverse_moves()
|
||||||
|
draft_invoice = self.env["account.move"].browse(reversal["res_id"])
|
||||||
|
self.assertTrue(draft_invoice)
|
||||||
|
self.assertEqual(draft_invoice.state, "draft")
|
||||||
|
self.assertEqual(draft_invoice.move_type, "in_invoice")
|
||||||
|
|
||||||
|
in_invoice = in_invoice.copy(
|
||||||
|
{
|
||||||
|
"invoice_date": self.date,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
in_invoice.action_post()
|
||||||
|
|
||||||
|
move_reversal = (
|
||||||
|
self.env["account.move.reversal"]
|
||||||
|
.with_context(active_model="account.move", active_ids=in_invoice.ids)
|
||||||
|
.create(
|
||||||
|
{
|
||||||
|
"journal_id": in_invoice.journal_id.id,
|
||||||
|
"reason": "no reason",
|
||||||
|
"refund_method": "refund",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
reversal = move_reversal.reverse_moves()
|
||||||
|
draft_reversed_move = self.env["account.move"].browse(reversal["res_id"])
|
||||||
|
self.assertTrue(draft_reversed_move)
|
||||||
|
self.assertEqual(draft_reversed_move.state, "draft")
|
||||||
|
self.assertEqual(draft_reversed_move.move_type, "in_refund")
|
||||||
|
|
||||||
|
def test_in_refund(self):
|
||||||
|
in_refund_invoice = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"journal_id": self.purchase_journal.id,
|
||||||
|
"invoice_date": self.date,
|
||||||
|
"partner_id": self.env.ref("base.res_partner_3").id,
|
||||||
|
"move_type": "in_refund",
|
||||||
|
"invoice_line_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"account_id": self.account1.id,
|
||||||
|
"price_unit": 42.0,
|
||||||
|
"quantity": 12,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(in_refund_invoice.name, "/")
|
||||||
|
in_refund_invoice.action_post()
|
||||||
|
seq = self.purchase_journal.refund_sequence_id
|
||||||
|
move_name = "%s%s" % (seq.prefix, "1".zfill(seq.padding))
|
||||||
|
move_name = move_name.replace("%(range_year)s", str(self.date.year))
|
||||||
|
self.assertEqual(in_refund_invoice.name, move_name)
|
||||||
|
in_refund_invoice.button_draft()
|
||||||
|
in_refund_invoice.action_post()
|
||||||
|
self.assertEqual(in_refund_invoice.name, move_name)
|
||||||
|
|
||||||
|
def test_remove_invoice_error_secuence_no_grap(self):
|
||||||
|
invoice = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"date": self.date,
|
||||||
|
"journal_id": self.misc_journal.id,
|
||||||
|
"line_ids": [
|
||||||
|
(0, 0, {"account_id": self.account1.id, "debit": 10}),
|
||||||
|
(0, 0, {"account_id": self.account2.id, "credit": 10}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
self.assertEqual(invoice.name, "/")
|
||||||
|
invoice.action_post()
|
||||||
|
error_msg = "You cannot delete an item linked to a posted entry."
|
||||||
|
with self.assertRaisesRegex(UserError, error_msg):
|
||||||
|
invoice.unlink()
|
||||||
|
invoice.button_draft()
|
||||||
|
invoice.button_cancel()
|
||||||
|
error_msg = "You cannot delete this entry, as it has already consumed a"
|
||||||
|
with self.assertRaisesRegex(UserError, error_msg):
|
||||||
|
invoice.unlink()
|
||||||
|
|
||||||
|
def test_remove_invoice_error_secuence_standard(self):
|
||||||
|
implementation = {"implementation": "standard"}
|
||||||
|
self.purchase_journal.sequence_id.write(implementation)
|
||||||
|
self.purchase_journal.refund_sequence_id.write(implementation)
|
||||||
|
in_refund_invoice = self.env["account.move"].create(
|
||||||
|
{
|
||||||
|
"journal_id": self.purchase_journal.id,
|
||||||
|
"invoice_date": self.date,
|
||||||
|
"partner_id": self.env.ref("base.res_partner_3").id,
|
||||||
|
"move_type": "in_refund",
|
||||||
|
"invoice_line_ids": [
|
||||||
|
(
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
{
|
||||||
|
"account_id": self.account1.id,
|
||||||
|
"price_unit": 42.0,
|
||||||
|
"quantity": 12,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
in_refund_invoice._compute_split_sequence()
|
||||||
|
self.assertEqual(in_refund_invoice.name, "/")
|
||||||
|
in_refund_invoice.action_post()
|
||||||
|
error_msg = "You cannot delete an item linked to a posted entry."
|
||||||
|
with self.assertRaisesRegex(UserError, error_msg):
|
||||||
|
in_refund_invoice.unlink()
|
||||||
|
in_refund_invoice.button_draft()
|
||||||
|
in_refund_invoice.button_cancel()
|
||||||
|
self.assertTrue(in_refund_invoice.unlink())
|
||||||
|
|
||||||
|
def test_journal_check_journal_sequence(self):
|
||||||
|
new_journal = self.purchase_journal.copy()
|
||||||
|
# same sequence_id and refund_sequence_id
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
new_journal.write({"refund_sequence_id": new_journal.sequence_id})
|
||||||
|
|
||||||
|
# company_id in sequence_id or refund_sequence_id to False
|
||||||
|
new_sequence_id = new_journal.sequence_id.copy({"company_id": False})
|
||||||
|
new_refund_sequence_id = new_journal.refund_sequence_id.copy(
|
||||||
|
{"company_id": False}
|
||||||
|
)
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
new_journal.write({"sequence_id": new_sequence_id.id})
|
||||||
|
with self.assertRaises(ValidationError):
|
||||||
|
new_journal.write({"refund_sequence_id": new_refund_sequence_id.id})
|
||||||
|
|
||||||
|
def test_constrains_date_sequence_true(self):
|
||||||
|
self.assertTrue(self.env["account.move"]._constrains_date_sequence())
|
33
account_move_name_sequence/views/account_journal.xml
Normal file
33
account_move_name_sequence/views/account_journal.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<!--
|
||||||
|
Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||||
|
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
@author: Moisés López <moylop260@vauxoo.com>
|
||||||
|
@author: Francisco Luna <fluna@vauxoo.com>
|
||||||
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_account_journal_form" model="ir.ui.view">
|
||||||
|
<field name="model">account.journal</field>
|
||||||
|
<field name="inherit_id" ref="account.view_account_journal_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<field name="refund_sequence" position="before">
|
||||||
|
<field
|
||||||
|
name="sequence_id"
|
||||||
|
required="1"
|
||||||
|
context="{'default_name': name, 'default_company_id': company_id, 'default_implementation': 'no_gap', 'default_padding': 4, 'default_use_date_range': True, 'default_prefix': (code or 'UNKNOWN') + '/%%(range_year)s/'}"
|
||||||
|
/>
|
||||||
|
</field>
|
||||||
|
<field name="refund_sequence" position="after">
|
||||||
|
<field
|
||||||
|
name="refund_sequence_id"
|
||||||
|
attrs="{'invisible': ['|', ('type', 'not in', ('sale', 'purchase')), ('refund_sequence', '=', False)],
|
||||||
|
'required': [('type', 'in', ('sale', 'purchase')), ('refund_sequence', '=', True)]}"
|
||||||
|
context="{'default_name': name, 'default_company_id': company_id, 'default_implementation': 'no_gap', 'default_padding': 4, 'default_use_date_range': True, 'default_prefix': 'R' + (code or 'UNKNOWN') + '/%%(range_year)s/'}"
|
||||||
|
/>
|
||||||
|
</field>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
31
account_move_name_sequence/views/account_move.xml
Normal file
31
account_move_name_sequence/views/account_move.xml
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8" ?>
|
||||||
|
<!--
|
||||||
|
Copyright 2021 Akretion France (http://www.akretion.com/)
|
||||||
|
@author: Alexis de Lattre <alexis.delattre@akretion.com>
|
||||||
|
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
|
||||||
|
-->
|
||||||
|
<odoo>
|
||||||
|
|
||||||
|
<record id="view_move_form" model="ir.ui.view">
|
||||||
|
<field name="model">account.move</field>
|
||||||
|
<field name="inherit_id" ref="account.view_move_form" />
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//field[@name='highest_name']/.." position="attributes">
|
||||||
|
<attribute name="invisible">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath
|
||||||
|
expr="//div[hasclass('oe_title')]//field[@name='name']"
|
||||||
|
position="attributes"
|
||||||
|
>
|
||||||
|
<attribute name="attrs">{'invisible': [('name', '=', '/')]}</attribute>
|
||||||
|
<attribute name="readonly">1</attribute>
|
||||||
|
</xpath>
|
||||||
|
<xpath expr="//div[hasclass('oe_title')]//h1//span" position="attributes">
|
||||||
|
<attribute
|
||||||
|
name="attrs"
|
||||||
|
>{'invisible': ['|', ('state', '!=', 'draft'), ('name', '!=', '/')]}</attribute>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
@ -0,0 +1 @@
|
|||||||
|
../../../../account_move_name_sequence
|
6
setup/account_move_name_sequence/setup.py
Normal file
6
setup/account_move_name_sequence/setup.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import setuptools
|
||||||
|
|
||||||
|
setuptools.setup(
|
||||||
|
setup_requires=['setuptools-odoo'],
|
||||||
|
odoo_addon=True,
|
||||||
|
)
|
Loading…
Reference in New Issue
Block a user