diff --git a/account_move_name_sequence/README.rst b/account_move_name_sequence/README.rst new file mode 100644 index 00000000000..976fed118fc --- /dev/null +++ b/account_move_name_sequence/README.rst @@ -0,0 +1,163 @@ +.. image:: https://odoo-community.org/readme-banner-image + :target: https://odoo-community.org/get-involved?utm_source=readme + :alt: Odoo Community Association + +============================ +Account Move Number Sequence +============================ + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:0c2654e763b1efefa3bdde71142ff86c5b15b5bc1ade423a7b2af05b98a17c2d + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/license-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/19.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-19-0/account-financial-tools-19-0-account_move_name_sequence + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png + :target: https://runboat.odoo-community.org/builds?repo=OCA/account-financial-tools&target_branch=19.0 + :alt: Try me on Runboat + +|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 `__ + that handles this is not easy to understand and quite difficult to + debug. + +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 `_. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +`feedback `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Akretion +* Vauxoo + +Contributors +------------ + +- `Akretion `__: + + - Alexis de Lattre + +- `Vauxoo `__: + + - Moisés López + - Francisco Luna + +- `Factor Libre `__: + + - Rodrigo Bonilla Martinez + +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-luisg123v| image:: https://github.com/luisg123v.png?size=40px + :target: https://github.com/luisg123v + :alt: luisg123v + +Current `maintainers `__: + +|maintainer-alexis-via| |maintainer-moylop260| |maintainer-luisg123v| + +This module is part of the `OCA/account-financial-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/account_move_name_sequence/__init__.py b/account_move_name_sequence/__init__.py new file mode 100644 index 00000000000..1d353d71e70 --- /dev/null +++ b/account_move_name_sequence/__init__.py @@ -0,0 +1,2 @@ +from .hooks import post_init_hook +from . import models diff --git a/account_move_name_sequence/__manifest__.py b/account_move_name_sequence/__manifest__.py new file mode 100644 index 00000000000..cb173d05f5a --- /dev/null +++ b/account_move_name_sequence/__manifest__.py @@ -0,0 +1,31 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# Copyright 2022 Vauxoo (https://www.vauxoo.com/) +# @author: Alexis de Lattre +# @author: Moisés López +# @author: Francisco Luna +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "Account Move Number Sequence", + "version": "19.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", "luisg123v"], + "website": "https://github.com/OCA/account-financial-tools", + "depends": [ + "account", + ], + "demo": [ + "demo/ir_sequence_demo.xml", + "demo/account_journal_demo.xml", + ], + "data": [ + "views/account_journal_views.xml", + "views/account_move_views.xml", + "security/ir.model.access.csv", + ], + "post_init_hook": "post_init_hook", + "installable": True, +} diff --git a/account_move_name_sequence/demo/account_journal_demo.xml b/account_move_name_sequence/demo/account_journal_demo.xml new file mode 100644 index 00000000000..fb5f0c8546e --- /dev/null +++ b/account_move_name_sequence/demo/account_journal_demo.xml @@ -0,0 +1,19 @@ + + + + Standard Sale Journal Demo + SSJD + sale + + + + + + Standard Cash Journal Demo + SCJD + cash + + + + + diff --git a/account_move_name_sequence/demo/ir_sequence_demo.xml b/account_move_name_sequence/demo/ir_sequence_demo.xml new file mode 100644 index 00000000000..26f29007212 --- /dev/null +++ b/account_move_name_sequence/demo/ir_sequence_demo.xml @@ -0,0 +1,21 @@ + + + + Standard Sale Sequence Demo + SSS_demo/%(range_year)s/ + + + + + standard + + + Standard Cash Sequence Demo + SCS_demo/%(range_year)s/ + + + + + standard + + diff --git a/account_move_name_sequence/hooks.py b/account_move_name_sequence/hooks.py new file mode 100644 index 00000000000..85dc1090474 --- /dev/null +++ b/account_move_name_sequence/hooks.py @@ -0,0 +1,35 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# Copyright 2022 Vauxoo (https://www.vauxoo.com/) +# @author: Alexis de Lattre +# @author: Moisés López +# @author: Francisco Luna +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). +from odoo import tools + + +def post_init_hook(env): + create_journal_sequences(env) + + +@tools.mute_logger("odoo.addons.account_move_name_sequence.models.account_journal") +def create_journal_sequences(env): + 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 diff --git a/account_move_name_sequence/i18n/account_move_name_sequence.pot b/account_move_name_sequence/i18n/account_move_name_sequence.pot new file mode 100644 index 00000000000..307ac5a0b87 --- /dev/null +++ b/account_move_name_sequence/i18n/account_move_name_sequence.pot @@ -0,0 +1,134 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.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_journal__has_sequence_holes +msgid "Has Sequence Holes" +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 +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__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +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 +msgid "Number" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note" +" Entry Sequence." +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 "" diff --git a/account_move_name_sequence/i18n/ar.po b/account_move_name_sequence/i18n/ar.po new file mode 100644 index 00000000000..e4788ae6846 --- /dev/null +++ b/account_move_name_sequence/i18n/ar.po @@ -0,0 +1,148 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-09-02 21:55+0000\n" +"Last-Translator: Hussain Hammad \n" +"Language-Team: none\n" +"Language: ar\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=6; plural=n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 " +"&& n%100<=10 ? 3 : n%100>=11 ? 4 : 5;\n" +"X-Generator: Weblate 4.17\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 "" +"لا يمكن ترحيل قيد و اسمه \"/\" او فارغ\n" +"الرجاء التأكد من تسلسل اليومية" + +#. 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_journal__has_sequence_holes +msgid "Has Sequence Holes" +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 +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__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +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 +msgid "Number" +msgstr "رقم" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note " +"Entry Sequence." +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "مرتجع" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "تسلسل" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 "" + +#~ msgid "Sequence Number" +#~ msgstr "رقم التسلسل" + +#~ msgid "Sequence Prefix" +#~ msgstr "قبل التسلسل" diff --git a/account_move_name_sequence/i18n/es.po b/account_move_name_sequence/i18n/es.po new file mode 100644 index 00000000000..a14b80dfd99 --- /dev/null +++ b/account_move_name_sequence/i18n/es.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-04-24 11:34+0000\n" +"Last-Translator: Ivorra78 \n" +"Language-Team: none\n" +"Language: es\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\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 "" +"Un asiento no puede ser publicado con el nombre \"/\" o vacío \n" +"Comprueba la secuencia de diario, por favor" + +#. 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 "" +"Marca esta casilla si no quieres compartir la misma secuencia para las " +"facturas y facturas rectificativas hechas en este diario" + +#. 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 "Secuencia de facturas rectificativas" + +#. 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 "Secuencia de facturas rectificativas dedicada" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__sequence_id +msgid "Entry Sequence" +msgstr "Secuencia de asiento" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__has_sequence_holes +msgid "Has Sequence Holes" +msgstr "Hay agujeros en la secuencia" + +#. 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 +msgid "Highest Name" +msgstr "Número más alto" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_journal +msgid "Journal" +msgstr "Dario" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_move +msgid "Journal Entry" +msgstr "Asiento" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +msgstr "Creado agujero en secuencia" + +#. 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 +msgid "Number" +msgstr "Número" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note " +"Entry Sequence." +msgstr "" +"En el diario '%s', se ha usado la misma secuencia para la Secuencia de " +"asiento y la Secuencia de facturas rectificativas." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "Rectificativa" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "Secuencia" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "Demostración del Libro de Caja Estándar" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "Demostración del Diario de Venta Estándar" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured as credit note " +"sequence of journal '%(journal)s'." +msgstr "" +"La compañía no tiene establecida la secuencia '%(sequence)s' configurada " +"como secuencia de facturas rectificativas del diario '%(journal)s'." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured on journal " +"'%(journal)s'." +msgstr "" +"La compañía no tiene establecida la secuencia '%(sequence)s' configurada en " +"el diario '%(journal)s'." + +#. 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 "" +"Esta secuencia se utilizará para generar el número de asientos para " +"rectificaciones." + +#. 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 "" +"Esta secuencia se usará para establecer para generar el número de asiento " +"contable." + +#~ msgid "Sequence Number" +#~ msgstr "Número de secuencia" + +#~ msgid "Sequence Prefix" +#~ msgstr "Prefijo de la secuencia" diff --git a/account_move_name_sequence/i18n/fr.po b/account_move_name_sequence/i18n/fr.po new file mode 100644 index 00000000000..7fef9f39243 --- /dev/null +++ b/account_move_name_sequence/i18n/fr.po @@ -0,0 +1,155 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-06-09 00:10+0000\n" +"Last-Translator: Alexis de Lattre \n" +"Language-Team: none\n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\n" +"X-Generator: Weblate 4.17\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 "" +"Une pièce comptable ne peut pas être comptabilisé car elle n'a pas de numéro " +"attribué.\n" +"Vérifiez la configuration de la séquence sur le journal concerné." + +#. 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 "" +"Cocher cette case si vous souhaitez créer 2 séquences distinctes pour les " +"factures et avoirs pour ce journal" + +#. 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 "Séquence pour les avoirs" + +#. 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 "Séquence dédiée pour les avoirs" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__sequence_id +msgid "Entry Sequence" +msgstr "Séquence des pièces comptables" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__has_sequence_holes +msgid "Has Sequence Holes" +msgstr "A des trous dans la séquence" + +#. 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 +msgid "Highest Name" +msgstr "Plus grand numéro de séquence" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_journal +msgid "Journal" +msgstr "Journal" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_move +msgid "Journal Entry" +msgstr "Pièce comptable" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +msgstr "Cause un trou dans la séquence" + +#. 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 +msgid "Number" +msgstr "Nombre" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note " +"Entry Sequence." +msgstr "" +"Pour le journal '%s', la même séquence est utilisée pour les factures et les " +"avoirs." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "Avoir" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "Séquence" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured as credit note " +"sequence of journal '%(journal)s'." +msgstr "" +"Aucune société n'est configurée sur la séquence '%(sequence)s' utilisée " +"comme séquence pour les avoirs sur le journal '%(journal)s'." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured on journal " +"'%(journal)s'." +msgstr "" +"Aucune société n'est configurée sur la séquence '%(sequence)s' configurée " +"sur le journal '%(journal)s'." + +#. 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 "Cette séquence sera utilisée pour générer les numéros des avoirs." + +#. 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 "" +"Cette séquence sera utilisée pour générer les numéros des pièces comptables." + +#~ msgid "Sequence Number" +#~ msgstr "Nombre de la séquence" + +#~ msgid "Sequence Prefix" +#~ msgstr "Préfixe de la séquence" diff --git a/account_move_name_sequence/i18n/hr.po b/account_move_name_sequence/i18n/hr.po new file mode 100644 index 00000000000..d1d9937a5a7 --- /dev/null +++ b/account_move_name_sequence/i18n/hr.po @@ -0,0 +1,156 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-02-16 10:09+0000\n" +"Last-Translator: Bole \n" +"Language-Team: none\n" +"Language: hr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && " +"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n" +"X-Generator: Weblate 4.14.1\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 "" +"Temeljnicu nije moguće potvrditi sa nazivom \"/\" ili praznim poljem \n" +"Provjerite sekvencu dnevnika, molimo" + +#. 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 "" +"Označite ovu kućicu ako ne želite dijeliti istu sekvencu za račune i " +"odobrenja/storna napravljena iz ovog dnevnika" + +#. 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 "Ulazna sekvenca za odobrenja" + +#. 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 "Dedicirana sekvenca za odobrenja" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__sequence_id +msgid "Entry Sequence" +msgstr "Ulazna sekvenca" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__has_sequence_holes +msgid "Has Sequence Holes" +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 +msgid "Highest Name" +msgstr "Najviši broj" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_journal +msgid "Journal" +msgstr "Dnevnik" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_move +msgid "Journal Entry" +msgstr "Stavka dnevnika" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +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 +msgid "Number" +msgstr "Broj" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note " +"Entry Sequence." +msgstr "" +"Na dnevniku '%s', ista sekvenca se koristi za regularna i storno knjiženja." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "Storno/Odobrenje" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "Sekvenca" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured as credit note " +"sequence of journal '%(journal)s'." +msgstr "" +"Tvrtka nije postavljena na sekvenci '%(sequence)s' postavljenoj kao sekvenca " +"za storno/odobrenja na dnevniku '%(journal)s'." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured on journal " +"'%(journal)s'." +msgstr "" +"Tvrtka nije postavljena na sekvenci '%(sequence)s' postavljenoj na dnevniku " +"'%(journal)s'." + +#. 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 "" +"Ova sekvenca će biti korištena za generiranje broja knjiženja za odobrenja/" +"storno." + +#. 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 "" +"Ova sekvenca će biti korištena za generiranje broja knjiženja dnevnika." + +#~ msgid "Sequence Number" +#~ msgstr "Broj sekvence" + +#~ msgid "Sequence Prefix" +#~ msgstr "Prefiks sekvence" diff --git a/account_move_name_sequence/i18n/it.po b/account_move_name_sequence/i18n/it.po new file mode 100644 index 00000000000..7689515d498 --- /dev/null +++ b/account_move_name_sequence/i18n/it.po @@ -0,0 +1,157 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 17.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-04-23 08:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\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 "" +"Un movimento non può essere inserito conconem \"/\" o un valore vuoto\n" +"Controllare la sequenza del registro" + +#. 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 "" +"Spuntare questa opzione se non si vuole condividere la stessa sequenza per " +"le fatture e le note di credito fatte per questo registro" + +#. 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 "Sequenza registrazione nota di credito" + +#. 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 "Sequenza nota di credito dedicata" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__sequence_id +msgid "Entry Sequence" +msgstr "Sequenza registrazione" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__has_sequence_holes +msgid "Has Sequence Holes" +msgstr "Ha mancanze nella sequenza" + +#. 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 +msgid "Highest Name" +msgstr "Primo nome" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_journal +msgid "Journal" +msgstr "Registro" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_move +msgid "Journal Entry" +msgstr "Registrazione contabile" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +msgstr "Generata discontinuità nella sequenza" + +#. 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 +msgid "Number" +msgstr "Numero" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note " +"Entry Sequence." +msgstr "" +"Nel registro '%s', la stessa sequenza è utilizzata come sequenza di " +"registrazione e sequenza di registrazione nota di credito." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "Rimborso" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "Sequenza" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "Demo registro contanti standard" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "Demo registro vendite standard" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured as credit note " +"sequence of journal '%(journal)s'." +msgstr "" +"L'azienda non è impostata per la sequenza '%(sequence)s' configurata come " +"sequenza nota di credito del registro '%(journal)s'." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"The company is not set on sequence '%(sequence)s' configured on journal " +"'%(journal)s'." +msgstr "" +"L'azienda non è impostata per la sequenza '%(sequence)s' configurata nel " +"registro '%(journal)s'." + +#. 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 "" +"Questa sequenza verrà utilizzata per generare il numero della registrazione " +"contabile per i rimborsi." + +#. 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 "" +"Questa sequenza verrà utilizzata per generare il numero della registrazione " +"contabile." + +#~ msgid "Sequence Number" +#~ msgstr "Numero sequenza" + +#~ msgid "Sequence Prefix" +#~ msgstr "Prefisso sequenza" diff --git a/account_move_name_sequence/i18n/pt_BR.po b/account_move_name_sequence/i18n/pt_BR.po new file mode 100644 index 00000000000..e6e4ff5d104 --- /dev/null +++ b/account_move_name_sequence/i18n/pt_BR.po @@ -0,0 +1,135 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 18.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: Automatically generated\n" +"Language-Team: none\n" +"Language: pt_BR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n > 1;\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_journal__has_sequence_holes +msgid "Has Sequence Holes" +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 +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__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +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 +msgid "Number" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note " +"Entry Sequence." +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 "" diff --git a/account_move_name_sequence/i18n/sl.po b/account_move_name_sequence/i18n/sl.po new file mode 100644 index 00000000000..c4c8400f6ea --- /dev/null +++ b/account_move_name_sequence/i18n/sl.po @@ -0,0 +1,169 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_move_name_sequence +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 14.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2023-03-30 12:22+0000\n" +"Last-Translator: Matjaz Mozetic \n" +"Language-Team: none\n" +"Language: sl\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=4; plural=n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || " +"n%100==4 ? 2 : 3;\n" +"X-Generator: Weblate 4.14.1\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 "" +"Temeljnice ni mogoče knjižiti z nazivom \"/\" ali prazno vrednostjo\n" +"Prosimo, da preverite zaporedje v dnevniku" + +#. 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 "" +"Označite, če želite v tem dnevniku uporabljati isto zaporedje za račune in " +"dobropise" + +#. 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 "Zaporedje za dobropise" + +#. 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 "Ločeno zaporedje za dobropise" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__sequence_id +msgid "Entry Sequence" +msgstr "Zaporedje vnosa" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_journal__has_sequence_holes +msgid "Has Sequence Holes" +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 +msgid "Highest Name" +msgstr "Najvišji naziv" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_journal +msgid "Journal" +msgstr "Dnevnik" + +#. module: account_move_name_sequence +#: model:ir.model,name:account_move_name_sequence.model_account_move +msgid "Journal Entry" +msgstr "Dnevniški vnos" + +#. module: account_move_name_sequence +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_bank_statement_line__made_sequence_hole +#: model:ir.model.fields,field_description:account_move_name_sequence.field_account_move__made_sequence_hole +msgid "Made Sequence Hole" +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 +msgid "Number" +msgstr "Številka" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "" +"On journal '%s', the same sequence is used as Entry Sequence and Credit Note " +"Entry Sequence." +msgstr "V dnevniku '%s' se uporablja isto zaporedje za vnose in dobropise." + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +msgid "Refund" +msgstr "Povračilo" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +#: model:ir.model,name:account_move_name_sequence.model_ir_sequence +msgid "Sequence" +msgstr "Zaporedje" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_cash_std_demo +msgid "Standard Cash Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#: model:account.journal,name:account_move_name_sequence.journal_sale_std_demo +msgid "Standard Sale Journal Demo" +msgstr "" + +#. module: account_move_name_sequence +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 +#. odoo-python +#: code:addons/account_move_name_sequence/models/account_journal.py:0 +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 "To zaporedje bo v rabi za knjigovodske vnose dobropisov." + +#. 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 "To zaporedje bo v rabi za knjigovodske vnose dobropisov." + +#~ msgid "Sequence Number" +#~ msgstr "Številka zaporedja" + +#~ msgid "Sequence Prefix" +#~ msgstr "Predpona zaporedja" + +#~ msgid "Display Name" +#~ msgstr "Prikazani naziv" + +#~ msgid "ID" +#~ msgstr "ID" + +#~ msgid "Last Modified on" +#~ msgstr "Zadnjič spremenjeno" + +#, python-format +#~ msgid "" +#~ "The company is not set on sequence '%s' configured as credit note " +#~ "sequence of journal '%s'." +#~ msgstr "" +#~ "Pri zaporedju '%s' za dobropise v dnevniku '%s' ni nastavljena družba." + +#, python-format +#~ msgid "The company is not set on sequence '%s' configured on journal '%s'." +#~ msgstr "" +#~ "Pri zaporedju '%s' nastavljenem v dnevniku '%s' ni nastavljena družba." diff --git a/account_move_name_sequence/models/__init__.py b/account_move_name_sequence/models/__init__.py new file mode 100644 index 00000000000..069c9b592b7 --- /dev/null +++ b/account_move_name_sequence/models/__init__.py @@ -0,0 +1,3 @@ +from . import account_journal +from . import account_move +from . import ir_sequence diff --git a/account_move_name_sequence/models/account_journal.py b/account_move_name_sequence/models/account_journal.py new file mode 100644 index 00000000000..a79c4cd8078 --- /dev/null +++ b/account_move_name_sequence/models/account_journal.py @@ -0,0 +1,255 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# Copyright 2022 Vauxoo (https://www.vauxoo.com/) +# @author: Alexis de Lattre +# @author: Moisés López +# @author: Francisco Luna +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +import logging + +from odoo import Command, api, fields, models +from odoo.exceptions import ValidationError +from odoo.fields import Domain + +_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) + # has_sequence_holes is not relevant anymore (since based on sequence_prefix/number) + # -> compute=False to improve perf and to avoid displaying warning + has_sequence_holes = fields.Boolean(compute=False) + + @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( + self.env._( + "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 = self.env._( + "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 = self.env._( + "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 = "{}{}/%(range_year)s/".format(refund and "R" or "", code) + seq_vals = { + "name": "{}{}".format( + vals.get("name", self.env._("Sequence")), + refund and " " + self.env._("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) + domain = Domain([(key, "=", value) for key, value in seq_vals.items()]) + existing = self.env["ir.sequence"].search(domain, limit=1) + if existing: + return existing + 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 = Domain( + [ + ("journal_id", "=", self.id), + ("name", "!=", "/"), + ] + ) + if self.refund_sequence: + #  Based on original Odoo behavior + if refund: + move_domain &= Domain("move_type", "in", ("out_refund", "in_refund")) + else: + move_domain &= Domain( + "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 {} could not get sequence {} values based on current moves. " + "Using default values.".format(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 = "{}{}{}{}{}%".format( + prefix1, + "_" * seq_format_values["year_length"], + prefix2, + "_" * bool(month) * 2, + prefix3, + ) + prefixes = prefix1 + prefix2 + select_year = ( + f"split_part(name, '{prefix2}', {prefixes.count(prefix2)})" + if prefix2 + else "''" + ) + prefixes += prefix3 + select_month = ( + f"split_part(name, '{prefix3}', {prefixes.count(prefix3)})" + if prefix3 + else "''" + ) + select_max_number = ( + f"MAX(split_part(name, '{prefixes[-1]}', " + f"{prefixes.count(prefixes[-1]) + 1}):" + f":INTEGER) AS max_number" + ) + query = ( + f"SELECT {select_year}, {select_month}, " + f"{select_max_number} FROM account_move " + f"WHERE name LIKE %s AND journal_id=%s GROUP BY 1,2" + ) + + # 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(f"{year}-{month}-1") + date_to = fields.Date.end_of(date_from, "month") + else: + date_from = fields.Date.to_date(f"{year}-1-1") + date_to = fields.Date.to_date(f"{year}-12-31") + seq_vals["date_range_ids"].append( + Command.create( + { + "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 {} diff --git a/account_move_name_sequence/models/account_move.py b/account_move_name_sequence/models/account_move.py new file mode 100644 index 00000000000..81a1cf8061a --- /dev/null +++ b/account_move_name_sequence/models/account_move.py @@ -0,0 +1,138 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# 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 is not needed any more + # -> compute=False to improve perf + highest_name = fields.Char(compute=False) + # made_sequence_hole is not relevant anymore (since based on sequence_prefix/number) + # -> compute=False to improve perf and to avoid displaying warning + made_sequence_hole = fields.Boolean(compute=False) + + _name_state_diagonal = models.Constraint( + "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("name") + def _compute_split_sequence(self): + """ + Replace original compute function from Odoo account module + to compute sequend and prefix from name only if journal is + in secured mode (restrict_mode_hash_table) + Since both sequence_prefix and sequence_number are needed for + computing hash since Odoo v18.0 + """ + moves = self.filtered( + lambda move: move.name + and move.name != "/" + and move.journal_id + and move.move_type + and move.restrict_mode_hash_table + ) + # Handle moves grouped by journal / move_type and year + for (journal, move_type, year), grouped_moves in moves.grouped( + lambda m: (m.journal_id, m.move_type, m.date.year) + ).items(): + # Get the correct sequence in case there is a specific + # one for refund configured on journal + if ( + move_type in ["in_refund", "out_refund"] + and journal.refund_sequence + and journal.refund_sequence_id + ): + sequence = journal.refund_sequence_id + else: + sequence = journal.sequence_id + # Retrieve prefix and suffix to extract number + prefix, suffix = sequence._get_prefix_suffix( + date=f"{year}-01-01", date_range=f"{year}-01-01" + ) + # Set prefix + grouped_moves.sequence_prefix = prefix + for move in grouped_moves: + # Get number for each move + move.sequence_number = int( + move.name[len(prefix) : len(move.name) - len(suffix)] + ) + + @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 + # Force compute of sequence_prefix and sequence_number + self._compute_split_sequence() + # Force compute of fields depending on name + self._inverse_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_reference(self, matching_states=("draft", "posted")): + moves = self.filtered( + lambda m: m.is_sale_document() or m.is_purchase_document() and m.ref + ) + if moves: + self.flush_model(["name", "journal_id", "move_type", "state"]) + return super()._fetch_duplicate_reference(matching_states=matching_states) + + def _get_last_sequence(self, relaxed=False, with_prefix=None): + return super()._get_last_sequence(relaxed, None) + + @api.onchange("journal_id") + def _onchange_journal_id(self): + if not self.quick_edit_mode: + self.name = "/" + self._compute_name_by_sequence() + + def _post(self, soft=True): + self.flush_recordset() + return super()._post(soft=soft) + + @api.depends() + def _compute_name(self): + """Overwrite account module method in order to + avoid side effect if legacy code call it directly + like when creating entry from email. + """ + return self._compute_name_by_sequence() diff --git a/account_move_name_sequence/models/ir_sequence.py b/account_move_name_sequence/models/ir_sequence.py new file mode 100644 index 00000000000..a8442d39cb5 --- /dev/null +++ b/account_move_name_sequence/models/ir_sequence.py @@ -0,0 +1,57 @@ +from odoo import fields, models +from odoo.fields import Domain + + +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 = f"{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( + Domain( + [ + ("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( + Domain( + [ + ("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 diff --git a/account_move_name_sequence/pyproject.toml b/account_move_name_sequence/pyproject.toml new file mode 100644 index 00000000000..4231d0cccb3 --- /dev/null +++ b/account_move_name_sequence/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/account_move_name_sequence/readme/CONFIGURE.md b/account_move_name_sequence/readme/CONFIGURE.md new file mode 100644 index 00000000000..f4332d94bf3 --- /dev/null +++ b/account_move_name_sequence/readme/CONFIGURE.md @@ -0,0 +1,16 @@ +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. diff --git a/account_move_name_sequence/readme/CONTRIBUTORS.md b/account_move_name_sequence/readme/CONTRIBUTORS.md new file mode 100644 index 00000000000..588d502ea68 --- /dev/null +++ b/account_move_name_sequence/readme/CONTRIBUTORS.md @@ -0,0 +1,7 @@ +- [Akretion](https://www.akretion.com): + - Alexis de Lattre \<\> +- [Vauxoo](https://www.vauxoo.com): + - Moisés López \<\> + - Francisco Luna \<\> +- [Factor Libre](https://www.factorlibre.com): + - Rodrigo Bonilla Martinez \<\> diff --git a/account_move_name_sequence/readme/DESCRIPTION.md b/account_move_name_sequence/readme/DESCRIPTION.md new file mode 100644 index 00000000000..cf1fb231519 --- /dev/null +++ b/account_move_name_sequence/readme/DESCRIPTION.md @@ -0,0 +1,38 @@ +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. + +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. diff --git a/account_move_name_sequence/security/ir.model.access.csv b/account_move_name_sequence/security/ir.model.access.csv new file mode 100644 index 00000000000..6247bc1e335 --- /dev/null +++ b/account_move_name_sequence/security/ir.model.access.csv @@ -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 diff --git a/account_move_name_sequence/static/description/icon.png b/account_move_name_sequence/static/description/icon.png new file mode 100644 index 00000000000..3a0328b516c Binary files /dev/null and b/account_move_name_sequence/static/description/icon.png differ diff --git a/account_move_name_sequence/static/description/index.html b/account_move_name_sequence/static/description/index.html new file mode 100644 index 00000000000..d207e61ca24 --- /dev/null +++ b/account_move_name_sequence/static/description/index.html @@ -0,0 +1,495 @@ + + + + + +README.rst + + + +
+ + + +Odoo Community Association + +
+

Account Move Number Sequence

+ +

Beta License: AGPL-3 OCA/account-financial-tools Translate me on Weblate Try me on Runboat

+

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 +that handles this is not easy to understand and quite difficult to +debug.
  • +
+

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

+ +
+

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. +In case of trouble, please check there if your issue has already been reported. +If you spotted it first, help us to smash it by providing a detailed and welcomed +feedback.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Akretion
  • +
  • Vauxoo
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+ +Odoo Community Association + +

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.

+

Current maintainers:

+

alexis-via moylop260 luisg123v

+

This module is part of the OCA/account-financial-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+
+ + diff --git a/account_move_name_sequence/tests/__init__.py b/account_move_name_sequence/tests/__init__.py new file mode 100644 index 00000000000..49b19d8c3bb --- /dev/null +++ b/account_move_name_sequence/tests/__init__.py @@ -0,0 +1,4 @@ +from . import test_account_move_name_seq +from . import test_account_move_name_seq_hashed_journal +from . import test_sequence_concurrency +from . import test_account_incoming_supplier_invoice diff --git a/account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py b/account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py new file mode 100644 index 00000000000..d321aa2086f --- /dev/null +++ b/account_move_name_sequence/tests/test_account_incoming_supplier_invoice.py @@ -0,0 +1,98 @@ +import json + +from markupsafe import Markup + +from odoo.tests import tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon + + +@tagged("post_install", "-at_install") +class TestAccountIncomingSupplierInvoice(AccountTestInvoicingCommon): + """Testing creating account move fetching mail.alias""" + + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.env["ir.config_parameter"].sudo().set_param( + "mail.catchall.domain", "test-company.odoo.com" + ) + + cls.internal_user = cls.env["res.users"].create( + { + "name": "Internal User", + "login": "internal.user@test.odoo.com", + "email": "internal.user@test.odoo.com", + } + ) + + cls.supplier_partner = cls.env["res.partner"].create( + { + "name": "Your Supplier", + "email": "supplier@other.company.com", + "supplier_rank": 10, + } + ) + + cls.journal = cls.company_data["default_journal_purchase"] + + journal_alias = cls.env["mail.alias"].create( + { + "alias_name": "test-bill", + "alias_model_id": cls.env.ref("account.model_account_move").id, + "alias_defaults": json.dumps( + { + "move_type": "in_invoice", + "company_id": cls.env.user.company_id.id, + "journal_id": cls.journal.id, + } + ), + } + ) + cls.journal.write({"alias_id": journal_alias.id}) + + def test_supplier_invoice_mailed_from_supplier(self): + """this test is mainly inspired from + addons.account.tests.test_account_incoming_supplier_invoice + python module but we make sure account move is draft without + name + """ + message_parsed = { + "message_id": "message-id-dead-beef", + "subject": "Incoming bill", + "from": f"{self.supplier_partner.name} <{self.supplier_partner.email}>", + "to": f"{self.journal.alias_id.alias_name}@" + f"{self.journal.alias_id.alias_domain}", + "body": "You know, that thing that you bought.", + "attachments": [b"Hello, invoice"], + } + + invoice = ( + self.env["account.move"] + .with_context( + tracking_disable=False, + mail_create_nolog=False, + mail_create_nosubscribe=False, + mail_notrack=False, + ) + .message_new( + message_parsed, + {"move_type": "in_invoice", "journal_id": self.journal.id}, + ) + ) + + message_ids = invoice.message_ids + self.assertEqual( + len(message_ids), 1, "Only one message should be posted in the chatter" + ) + self.assertEqual( + message_ids.body, + Markup("

Vendor Bill Created

"), + "Only the invoice creation should be posted", + ) + + following_partners = invoice.message_follower_ids.mapped("partner_id") + self.assertEqual(following_partners, self.env.user.partner_id) + self.assertEqual(invoice.state, "draft") + self.assertEqual(invoice.name, "/") diff --git a/account_move_name_sequence/tests/test_account_move_name_seq.py b/account_move_name_sequence/tests/test_account_move_name_seq.py new file mode 100644 index 00000000000..57c2482ce1f --- /dev/null +++ b/account_move_name_sequence/tests/test_account_move_name_seq.py @@ -0,0 +1,388 @@ +# Copyright 2021 Akretion France (http://www.akretion.com/) +# @author: Alexis de Lattre +# @author: Moisés López +# @author: Francisco Luna +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from datetime import datetime +from unittest.mock import patch + +from freezegun import freeze_time + +from odoo import Command, fields +from odoo.exceptions import UserError, ValidationError +from odoo.tests import Form, TransactionCase, tagged + + +@tagged("post_install", "-at_install") +class TestAccountMoveNameSequence(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.company = cls.env.ref("base.main_company") + cls.partner = cls.env["res.partner"].create({"name": "Demo User"}) + cls.misc_journal = cls.env["account.journal"].create( + { + "name": "Test Journal Move name seq", + "code": "ADLM", + "type": "general", + "company_id": cls.company.id, + } + ) + cls.sales_seq = cls.env["ir.sequence"].create( + { + "name": "TB2C", + "implementation": "no_gap", + "prefix": "TB2CSEQ/%(range_year)s/", + "use_date_range": True, + "number_increment": 1, + "padding": 4, + "company_id": cls.company.id, + } + ) + cls.sales_journal = cls.env["account.journal"].create( + { + "name": "TB2C", + "code": "TB2C", + "type": "sale", + "company_id": cls.company.id, + "refund_sequence": True, + "sequence_id": cls.sales_seq.id, + } + ) + cls.purchase_journal = cls.env["account.journal"].create( + { + "name": "Test Purchase Journal Move name seq", + "code": "ADLP", + "type": "purchase", + "company_id": cls.company.id, + "refund_sequence": True, + } + ) + cls.accounts = cls.env["account.account"].search( + [("company_ids", "=", cls.company.id)], limit=2 + ) + cls.account1 = cls.accounts[0] + cls.account2 = cls.accounts[1] + cls.date = datetime.now() + cls.purchase_journal2 = cls.purchase_journal.copy() + + cls.journals = ( + cls.misc_journal + | cls.purchase_journal + | cls.sales_journal + | cls.purchase_journal2 + ) + # This patch was added to avoid test failures in the CI pipeline caused by the + # `account_journal_restrict_mode` module. It prevents a validation error when + # disabling restrict mode on journals used in the test, allowing moves to be + # set to draft and deleted. + with patch("odoo.models.BaseModel._validate_fields"): + cls.journals.restrict_mode_hash_table = False + + cls.lines = [ + Command.create({"account_id": cls.account1.id, "debit": 10}), + Command.create({"account_id": cls.account2.id, "credit": 10}), + ] + cls.invoice_line = [ + Command.create( + { + "account_id": cls.account1.id, + "price_unit": 42.0, + "quantity": 12, + }, + ) + ] + + 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": self.lines, + } + ) + self.assertEqual(move.name, "/") + move.action_post() + seq = self.misc_journal.sequence_id + move_name = "{}{}".format(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": self.lines, + } + ) + 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": self.lines, + } + ) + 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": self.lines, + } + ) + move.action_post() + self.assertEqual(move.name, "TEST-2022-07-0001") + + def test_prefix_move_name_use_move_date_2(self): + seq = self.misc_journal.sequence_id + seq.prefix = "TEST-%(range_month)s-" + with freeze_time("2022-01-01"): + move = self.env["account.move"].create( + { + "date": "2022-06-30", + "journal_id": self.misc_journal.id, + "line_ids": self.lines, + } + ) + move.action_post() + self.assertEqual(move.name, "TEST-06-0001") + + def test_prefix_move_name_use_move_date_3(self): + seq = self.misc_journal.sequence_id + seq.prefix = "TEST-%(range_day)s-" + with freeze_time("2022-01-01"): + move = self.env["account.move"].create( + { + "date": "2022-01-01", + "journal_id": self.misc_journal.id, + "line_ids": self.lines, + } + ) + move.action_post() + self.assertEqual(move.name, "TEST-01-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.partner.id, + "move_type": "in_invoice", + "invoice_line_ids": self.invoice_line + + [ + Command.create( + { + "account_id": self.account1.id, + "price_unit": 48.0, + "quantity": 10, + } + ) + ], + } + ) + self.assertEqual(in_invoice.name, "/") + in_invoice.action_post() + + in_invoice = in_invoice.copy( + { + "invoice_date": self.date, + } + ) + in_invoice.action_post() + + move_reversal = self.env["account.move.reversal"].create( + { + "move_ids": in_invoice.ids, + "journal_id": in_invoice.journal_id.id, + "reason": "no reason", + } + ) + reversal = move_reversal.modify_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"].create( + { + "move_ids": in_invoice.ids, + "journal_id": in_invoice.journal_id.id, + "reason": "no reason", + } + ) + reversal = move_reversal.refund_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.partner.id, + "move_type": "in_refund", + "invoice_line_ids": self.invoice_line, + } + ) + self.assertEqual(in_refund_invoice.name, "/") + in_refund_invoice.action_post() + seq = self.purchase_journal.refund_sequence_id + move_name = "{}{}".format(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": self.lines, + } + ) + self.assertEqual(invoice.name, "/") + invoice.action_post() + error_msg = ( + "You can't delete a posted journal item. " + "Don’t play games with your accounting records; " + "reset the journal entry to draft before deleting it." + ) + with self.assertRaisesRegex(UserError, error_msg): + invoice.unlink() + invoice.button_draft() + invoice.button_cancel() + 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.partner.id, + "move_type": "in_refund", + "invoice_line_ids": self.invoice_line, + } + ) + self.assertEqual(in_refund_invoice.name, "/") + in_refund_invoice.action_post() + error_msg = ( + "You can't delete a posted journal item. " + "Don’t play games with your accounting records; " + "reset the journal entry to draft before deleting it." + ) + 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_journal2 + # 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()) + + def test_prefix_move_name_journal_onchange(self): + product = self.env["product.product"].create({"name": "Product"}) + with Form( + self.env["account.move"].with_context(default_move_type="out_invoice") + ) as invoice_form: + invoice_form.invoice_date = fields.Date.today() + invoice_form.partner_id = self.partner + with invoice_form.invoice_line_ids.new() as line_form: + line_form.product_id = product + invoice = invoice_form.save() + self.assertEqual(invoice.name, "/") + invoice.journal_id = self.sales_journal + self.assertEqual(invoice.name, "/", "name based on journal instead of sequence") + invoice.action_post() + self.assertIn("TB2CSEQ/", invoice.name, "name was not based on sequence") + + def test_is_end_of_seq_chain(self): + self.env.user.group_ids -= self.env.ref("account.group_account_manager") + invoice = self.env["account.move"].create( + { + "date": self.date, + "journal_id": self.misc_journal.id, + "line_ids": self.lines, + } + ) + invoice.action_post() + error_msg = ( + "You cannot delete this entry, as it has already consumed " + "a sequence number and is not the last one in the chain. " + "You should probably revert it instead." + ) + with self.assertRaisesRegex(UserError, error_msg): + invoice._unlink_forbid_parts_of_chain() diff --git a/account_move_name_sequence/tests/test_account_move_name_seq_hashed_journal.py b/account_move_name_sequence/tests/test_account_move_name_seq_hashed_journal.py new file mode 100644 index 00000000000..77785ebf226 --- /dev/null +++ b/account_move_name_sequence/tests/test_account_move_name_seq_hashed_journal.py @@ -0,0 +1,66 @@ +# Copyright 2025 Le Filament (https://le-filament.com) +# @author: Rémi - Le Filament +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + + +from freezegun import freeze_time + +from odoo.tests import tagged + +from odoo.addons.account_move_name_sequence.tests.test_account_move_name_seq import ( + TestAccountMoveNameSequence, +) + + +@tagged("post_install", "-at_install") +class TestAccountMoveNameSequenceHashedJournal(TestAccountMoveNameSequence): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.partner = cls.env["res.partner"].create({"name": "Demo User"}) + cls.sales_journal.restrict_mode_hash_table = True + + def test_account_move_hashed(self): + seq = self.sales_journal.sequence_id + seq.prefix = "TEST-%(range_year)s-" + with freeze_time("2022-05-31"): + move = self.env["account.move"].create( + { + "journal_id": self.sales_journal.id, + "partner_id": self.partner.id, + "move_type": "out_invoice", + "invoice_line_ids": self.invoice_line, + } + ) + move2 = move.copy() + move.action_post() + move2.action_post() + self.assertEqual(move.name, "TEST-2022-0001") + self.assertEqual(move2.name, "TEST-2022-0002") + self.assertEqual(move.sequence_prefix, "TEST-2022-") + self.assertEqual(move2.sequence_prefix, "TEST-2022-") + self.assertEqual(move.sequence_number, 1) + self.assertEqual(move2.sequence_number, 2) + + def test_account_move_hashed_suffix(self): + seq = self.sales_journal.sequence_id + seq.prefix = "TEST-%(range_year)s-" + seq.suffix = "-TEST_SUFFIX" + with freeze_time("2022-05-31"): + move = self.env["account.move"].create( + { + "journal_id": self.sales_journal.id, + "partner_id": self.partner.id, + "move_type": "out_invoice", + "invoice_line_ids": self.invoice_line, + } + ) + move2 = move.copy() + move.action_post() + move2.action_post() + self.assertEqual(move.name, "TEST-2022-0001-TEST_SUFFIX") + self.assertEqual(move2.name, "TEST-2022-0002-TEST_SUFFIX") + self.assertEqual(move.sequence_prefix, "TEST-2022-") + self.assertEqual(move2.sequence_prefix, "TEST-2022-") + self.assertEqual(move.sequence_number, 1) + self.assertEqual(move2.sequence_number, 2) diff --git a/account_move_name_sequence/tests/test_sequence_concurrency.py b/account_move_name_sequence/tests/test_sequence_concurrency.py new file mode 100644 index 00000000000..ad53253c0e2 --- /dev/null +++ b/account_move_name_sequence/tests/test_sequence_concurrency.py @@ -0,0 +1,454 @@ +import logging +import threading +import time +from unittest.mock import patch + +import psycopg2 + +from odoo import SUPERUSER_ID, api, fields, tools +from odoo.fields import Domain +from odoo.modules.registry import DummyRLock, Registry +from odoo.tests import Form, TransactionCase, tagged + +_logger = logging.getLogger(__name__) + + +class ThreadRaiseJoin(threading.Thread): + """Custom Thread Class to raise the exception to main thread in the join""" + + def run(self, *args, **kwargs): + self.exc = None + try: + return super().run(*args, **kwargs) + except BaseException as e: + self.exc = e + + def join(self, *args, **kwargs): + res = super().join(*args, **kwargs) + # Wait for the thread finishes + while self.is_alive(): + pass + # raise exception in the join + # to raise it in the main thread + if self.exc: + raise self.exc + return res + + +@tagged("post_install", "-at_install", "test_move_sequence") +class TestSequenceConcurrency(TransactionCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls._registry_lock_patcher = patch.object(Registry, "_lock", DummyRLock()) + cls.startClassPatcher(cls._registry_lock_patcher) + + with cls.env.registry.cursor() as cr: + env = api.Environment(cr, api.SUPERUSER_ID, {}) + + Product = env["product.product"] + Partner = env["res.partner"] + Sequence = env["ir.sequence"] + Journal = env["account.journal"] + + product = Product.create({"name": "Test Product"}) + partner = Partner.create({"name": "Test Partner 1"}) + partner2 = Partner.create({"name": "Test Partner 2"}) + date = fields.Date.to_date("1985-04-14") + journal_sale_seq = Sequence.create( + { + "name": "Standard Sale Sequence Demo", + "prefix": "SSS_demo/%(range_year)s/", + "use_date_range": True, + "number_next": 1, + "number_increment": 1, + "company_id": env.ref("base.main_company").id, + "implementation": "standard", + } + ) + journal_sale_std = Journal.create( + { + "name": "Standard Sale Journal Demo", + "code": "SSJD", + "type": "sale", + "refund_sequence": True, + "company_id": env.ref("base.main_company").id, + "sequence_id": journal_sale_seq.id, + } + ) + journal_cash_seq = Sequence.create( + { + "name": "Standard Cash Sequence Demo", + "prefix": "SCS_demo/%(range_year)s/", + "use_date_range": True, + "number_next": 1, + "number_increment": 1, + "company_id": env.ref("base.main_company").id, + "implementation": "standard", + } + ) + journal_cash_std = Journal.create( + { + "name": "Standard Cash Journal Demo", + "code": "SCJD", + "type": "cash", + "refund_sequence": True, + "company_id": env.ref("base.main_company").id, + "sequence_id": journal_cash_seq.id, + } + ) + cls.data = { + "date": date, + "product_id": product.id, + "partner_id": partner.id, + "partner2_id": partner2.id, + "journal_sale_seq_id": journal_sale_seq.id, + "journal_cash_seq_id": journal_cash_seq.id, + "journal_sale_id": journal_sale_std.id, + "journal_cash_id": journal_cash_std.id, + } + env.cr.commit() + + cls.cr0 = cls.registry.cursor() + cls.env0 = api.Environment(cls.cr0, SUPERUSER_ID, {}) + cls.cr1 = cls.registry.cursor() + cls.env1 = api.Environment(cls.cr1, SUPERUSER_ID, {}) + cls.cr2 = cls.registry.cursor() + cls.env2 = api.Environment(cls.cr2, SUPERUSER_ID, {}) + for cr in [cls.cr0, cls.cr1, cls.cr2]: + # Set a 10-second timeout to avoid waiting too long for release locks + cr.execute("SET LOCAL statement_timeout = '10s'") + cls.last_existing_move_id = ( + cls.env["account.move"].search([], limit=1, order="id desc").id or 0 + ) + cls.addClassCleanup(cls._cleanup) + + @classmethod + def _cleanup(cls): + with cls.env.registry.cursor() as cr: + env = api.Environment(cr, api.SUPERUSER_ID, {}) + moves = ( + env["account.move"] + .with_context(force_delete=True) + .search(Domain("id", ">", cls.last_existing_move_id)) + ) + payments = moves.payment_ids + moves_without_payments = moves - payments.move_id + if payments: + payments.action_draft() + payments.unlink() + if moves_without_payments: + moves_without_payments.filtered( + lambda move: move.state != "draft" + ).button_draft() + moves_without_payments.unlink() + + try: + journals = env["account.journal"].browse( + [ + cls.data["journal_sale_id"], + cls.data["journal_cash_id"], + ] + ) + journals.unlink() + except Exception as e: + _logger.warning("Failed to delete journals: %s", e) + + try: + sequences = env["ir.sequence"].browse( + [ + cls.data["journal_sale_seq_id"], + cls.data["journal_cash_seq_id"], + ] + ) + sequences.unlink() + except Exception as e: + _logger.warning("Failed to delete sequences: %s", e) + + try: + partners = env["res.partner"].browse( + [ + cls.data["partner_id"], + cls.data["partner2_id"], + ] + ) + partners.unlink() + except Exception as e: + _logger.warning("Failed to delete partners: %s", e) + + try: + product = env["product.product"].browse( + [ + cls.data["product_id"], + ] + ) + product.unlink() + except Exception as e: + _logger.warning("Failed to delete products: %s", e) + env.cr.commit() + + for cr in [cls.cr0, cls.cr1, cls.cr2]: + if not cr.closed: + try: + cr.close() + except Exception as e: + _logger.warning("Failed to close cursor: %s", e) + + def _commit_crs(self, *envs): + for env in envs: + env.cr.commit() + + def _create_invoice_form( + self, env, post=True, partner_id=None, ir_sequence_standard=False + ): + ctx = {"default_move_type": "out_invoice"} + with Form(env["account.move"].with_context(**ctx)) as invoice_form: + # Use another partner to bypass "increase_rank" lock error + invoice_form.partner_id = ( + partner_id + and env["res.partner"].browse(partner_id) + or env["res.partner"].browse(self.data["partner_id"]) + ) + invoice_form.invoice_date = self.data["date"] + + with invoice_form.invoice_line_ids.new() as line_form: + line_form.product_id = env["product.product"].browse( + self.data["product_id"] + ) + line_form.price_unit = 100.0 + line_form.tax_ids.clear() + invoice = invoice_form.save() + if ir_sequence_standard: + invoice.journal_id = env["account.journal"].browse( + self.data["journal_sale_id"] + ) + if post: + # This patch was added to avoid test failures in the CI pipeline caused by + # the `account_journal_restrict_mode` module. It avoids errors when setting + # posted moves to draft and deleting them by bypassing the method that + # writes the hash field used for validation. + with patch( + "odoo.addons.account.models.account_move.AccountMove._hash_moves" + ): + invoice.action_post() + return invoice + + def _create_payment_form(self, env, partner_id=None, ir_sequence_standard=False): + with Form( + env["account.payment"].with_context( + default_payment_type="inbound", + default_partner_type="customer", + default_move_journal_types=("bank", "cash"), + ) + ) as payment_form: + payment_form.partner_id = ( + partner_id + and env["res.partner"].browse(partner_id) + or env["res.partner"].browse(self.data["partner_id"]) + ) + payment_form.amount = 100 + payment_form.date = self.data["date"] + if ir_sequence_standard: + payment_form.journal_id = env["account.journal"].browse( + self.data["journal_cash_id"] + ) + payment = payment_form.save() + # This patch was added to avoid test failures in the CI pipeline caused by + # the `account_journal_restrict_mode` module. It avoids errors when setting + # posted moves to draft and deleting them by bypassing the method that + # writes the hash field used for validation. + with patch("odoo.addons.account.models.account_move.AccountMove._hash_moves"): + payment.action_post() + return payment + + def _create_invoice_payment( + self, deadlock_timeout, payment_first=False, ir_sequence_standard=False + ): + with self.registry.cursor() as cr: + env = api.Environment(cr, SUPERUSER_ID, {}) + cr_pid = cr.connection.get_backend_pid() + # Avoid waiting for a long time and it needs to be less than deadlock + cr.execute("SET LOCAL statement_timeout = '%ss'", (deadlock_timeout + 10,)) + if payment_first: + _logger.info("[PID %s] Creating payment", cr_pid) + self._create_payment_form( + env, ir_sequence_standard=ir_sequence_standard + ) + _logger.info("[PID %s] Creating invoice", cr_pid) + self._create_invoice_form( + env, ir_sequence_standard=ir_sequence_standard + ) + else: + _logger.info("[PID %s] Creating invoice", cr_pid) + self._create_invoice_form( + env, ir_sequence_standard=ir_sequence_standard + ) + _logger.info("[PID %s] Creating payment", cr_pid) + self._create_payment_form( + env, ir_sequence_standard=ir_sequence_standard + ) + # sleep in order to avoid release the locks too faster + # It could be many methods called after creating these + # kind of records e.g. reconcile + _logger.info( + "[PID %s] Finishing, wait %ss before release", cr_pid, deadlock_timeout + 12 + ) + time.sleep(deadlock_timeout + 12) + + def test_sequence_concurrency_10_draft_invoices(self): + """Creating 2 DRAFT invoices not should raises errors""" + # Create "last move" to lock + self._create_invoice_form(self.env0) + self.env0.cr.commit() + invoice1 = self._create_invoice_form(self.env1, post=False) + self.assertEqual(invoice1.state, "draft") + invoice2 = self._create_invoice_form(self.env2, post=False) + self.assertEqual(invoice2.state, "draft") + self._commit_crs(self.env0, self.env1, self.env2) + + def test_sequence_concurrency_20_editing_last_invoice(self): + """Edit last invoice and create a new invoice + should not raises errors""" + # Create "last move" to lock + invoice = self._create_invoice_form(self.env0) + self.env0.cr.commit() + # Edit something in "last move" + invoice.write({"write_uid": self.env0.uid}) + self.env0.flush_all() + self._create_invoice_form(self.env1) + self._commit_crs(self.env0, self.env1) + + def test_sequence_concurrency_30_editing_last_payment(self): + """Edit last payment and create a new payment + should not raises errors""" + # Create "last move" to lock + payment = self._create_payment_form(self.env0) + payment_move = payment.move_id + self.env0.cr.commit() + # Edit something in "last move" + payment_move.write({"write_uid": self.env0.uid}) + self.env0.flush_all() + self._create_payment_form(self.env1) + self._commit_crs(self.env0, self.env1) + + @tools.mute_logger("odoo.sql_db") + def test_sequence_concurrency_40_reconciling_last_invoice(self): + """Reconcile last invoice and create a new one + should not raises errors""" + # Create "last move" to lock + invoice = self._create_invoice_form(self.env0) + payment = self._create_payment_form(self.env0) + payment_move = payment.move_id + self.env0.cr.commit() + lines2reconcile = ( + (payment_move | invoice) + .mapped("line_ids") + .filtered(lambda line: line.account_id.account_type == "asset_receivable") + ) + # Reconciling "last move" + # reconcile a payment with many invoices spend a lot so it could + # lock records too many time + lines2reconcile.reconcile() + # Many pieces of code call flush directly + self.env0.flush_all() + self._create_invoice_form(self.env1) + self._commit_crs(self.env0, self.env1) + + def test_sequence_concurrency_50_reconciling_last_payment(self): + """Reconcile last payment and create a new one + should not raises errors""" + # Create "last move" to lock + invoice = self._create_invoice_form(self.env0) + payment = self._create_payment_form(self.env0) + payment_move = payment.move_id + self.env0.cr.commit() + lines2reconcile = ( + (payment_move | invoice) + .mapped("line_ids") + .filtered(lambda line: line.account_id.account_type == "asset_receivable") + ) + # Reconciling "last move" + # reconcile a payment with many invoices spend a lot so it could + # lock records too many time + lines2reconcile.reconcile() + # Many pieces of code call flush directly + self.env0.flush_all() + self._create_payment_form(self.env1) + self._commit_crs(self.env0, self.env1) + + def test_sequence_concurrency_90_payments(self): + """Creating concurrent payments should not raises errors""" + # Create "last move" to lock + self._create_payment_form(self.env0, ir_sequence_standard=True) + self.env0.cr.commit() + self._create_payment_form(self.env1, ir_sequence_standard=True) + self._create_payment_form(self.env2, ir_sequence_standard=True) + self._commit_crs(self.env0, self.env1, self.env2) + + def test_sequence_concurrency_92_invoices(self): + """Creating concurrent invoices should not raises errors""" + # Create "last move" to lock + self._create_invoice_form(self.env0, ir_sequence_standard=True) + self.env0.cr.commit() + self._create_invoice_form(self.env1, ir_sequence_standard=True) + # Using another partner to bypass "increase_rank" lock error + self._create_invoice_form( + self.env2, partner_id=self.data["partner2_id"], ir_sequence_standard=True + ) + self._commit_crs(self.env0, self.env1, self.env2) + + @tools.mute_logger("odoo.sql_db") + def test_sequence_concurrency_95_pay2inv_inv2pay(self): + """Creating concurrent payment then invoice and invoice then payment + should not raises errors + It raises deadlock sometimes""" + # Create "last move" to lock + self._create_invoice_form(self.env0) + # Create "last move" to lock + self._create_payment_form(self.env0) + self.env0.cr.commit() + self.env0.cr.execute( + "SELECT setting FROM pg_settings WHERE name = 'deadlock_timeout'" + ) + deadlock_timeout = int(self.env0.cr.fetchone()[0]) # ms + # You could not have permission to set this parameter + # psycopg2.errors.InsufficientPrivilege + self.assertTrue( + deadlock_timeout, + "You need to configure PG parameter deadlock_timeout='1s'", + ) + deadlock_timeout = int(deadlock_timeout / 1000) # s + t_pay_inv = ThreadRaiseJoin( + target=self._create_invoice_payment, + args=(deadlock_timeout, True, True), + name="Thread payment invoice", + ) + t_inv_pay = ThreadRaiseJoin( + target=self._create_invoice_payment, + args=(deadlock_timeout, False, True), + name="Thread invoice payment", + ) + t_pay_inv.start() + t_inv_pay.start() + # the thread could raise the error before to wait for it so disable coverage + self._thread_join(t_pay_inv, deadlock_timeout + 15) + self._thread_join(t_inv_pay, deadlock_timeout + 15) + + def _thread_join(self, thread_obj, timeout): + try: + thread_obj.join(timeout=timeout) # pragma: no cover + self.assertFalse( + thread_obj.is_alive(), + "The thread wait is over. but the cursor may still be in use!", + ) + except psycopg2.OperationalError as e: + if e.pgcode in [ + psycopg2.errorcodes.SERIALIZATION_FAILURE, + psycopg2.errorcodes.LOCK_NOT_AVAILABLE, + ]: # pragma: no cover + # Concurrency error is expected but not deadlock so ok + pass + elif e.pgcode == psycopg2.errorcodes.DEADLOCK_DETECTED: # pragma: no cover + self.assertFalse(True, "Deadlock detected.") + else: # pragma: no cover + raise diff --git a/account_move_name_sequence/views/account_journal_views.xml b/account_move_name_sequence/views/account_journal_views.xml new file mode 100644 index 00000000000..282d48573de --- /dev/null +++ b/account_move_name_sequence/views/account_journal_views.xml @@ -0,0 +1,45 @@ + + + + + account.journal + + + + + + + + + + + diff --git a/account_move_name_sequence/views/account_move_views.xml b/account_move_name_sequence/views/account_move_views.xml new file mode 100644 index 00000000000..69bddd022f7 --- /dev/null +++ b/account_move_name_sequence/views/account_move_views.xml @@ -0,0 +1,27 @@ + + + + + account.move + + + + 1 + + + name == '/' + 1 + + + state != 'draft' or name != '/' + + + +