diff --git a/account_invoice_constraint_chronology/__init__.py b/account_invoice_constraint_chronology/__init__.py index d6af3144..cf4da9f2 100644 --- a/account_invoice_constraint_chronology/__init__.py +++ b/account_invoice_constraint_chronology/__init__.py @@ -1,2 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from . import model from . import tests diff --git a/account_invoice_constraint_chronology/__manifest__.py b/account_invoice_constraint_chronology/__manifest__.py index 24db4af6..2bad0528 100644 --- a/account_invoice_constraint_chronology/__manifest__.py +++ b/account_invoice_constraint_chronology/__manifest__.py @@ -1,14 +1,13 @@ # Copyright 2015-2019 ACSONE SA/NV () +# Copyright 2021 CorporateHub (https://corporatehub.eu) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { "name": "Account Invoice Constraint Chronology", - "version": "12.0.1.0.2", - "author": "Odoo Community Association (OCA), ACSONE SA/NV", - "maintainer": "ACSONE SA/NV", - "website": "https://github.com/OCA/account-financial-tools/tree/12.0/" - "account_invoice_constraint_chronology", + "version": "13.0.1.0.0", + "author": "Odoo Community Association (OCA), ACSONE SA/NV, CorporateHub", + "website": "https://github.com/OCA/account-financial-tools/", "license": "AGPL-3", "category": "Accounting", "depends": ["account"], - "data": ["view/account_view.xml"], + "data": ["view/account_journal.xml"], } diff --git a/account_invoice_constraint_chronology/model/__init__.py b/account_invoice_constraint_chronology/model/__init__.py index 599c03ec..6a288ca7 100644 --- a/account_invoice_constraint_chronology/model/__init__.py +++ b/account_invoice_constraint_chronology/model/__init__.py @@ -1,2 +1,4 @@ +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + from . import account_journal -from . import account_invoice +from . import account_move diff --git a/account_invoice_constraint_chronology/model/account_invoice.py b/account_invoice_constraint_chronology/model/account_invoice.py deleted file mode 100644 index 31ddef27..00000000 --- a/account_invoice_constraint_chronology/model/account_invoice.py +++ /dev/null @@ -1,89 +0,0 @@ -# Copyright 2015-2019 ACSONE SA/NV () -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). - -import datetime - -from odoo import _, api, fields, models -from odoo.exceptions import UserError -from odoo.tools.misc import format_date - - -class AccountInvoice(models.Model): - _inherit = "account.invoice" - - @api.model - def _prepare_previous_invoices_domain(self, invoice): - domain = [ - ( - "state", - "not in", - ["open", "paid", "cancel", "in_payment", "proforma", "proforma2"], - ), - ("date_invoice", "!=", False), - ("date_invoice", "<", invoice.date_invoice), - ("journal_id", "=", invoice.journal_id.id), - ] - if ( - invoice.journal_id.refund_sequence - and invoice.journal_id.sequence_id != invoice.journal_id.refund_sequence_id - ): - domain.append(("type", "=", invoice.type)) - return domain - - @api.model - def _prepare_later_invoices_domain(self, invoice): - domain = [ - ("state", "in", ["open", "in_payment", "paid"]), - ("date_invoice", ">", invoice.date_invoice), - ("journal_id", "=", invoice.journal_id.id), - ] - if ( - invoice.journal_id.refund_sequence - and invoice.journal_id.sequence_id != invoice.journal_id.refund_sequence_id - ): - domain.append(("type", "=", invoice.type)) - return domain - - @api.multi - def action_move_create(self): - previously_validated = self.filtered(lambda inv: inv.move_name) - res = super(AccountInvoice, self).action_move_create() - for inv in self: - if not inv.journal_id.check_chronology: - continue - invoices = self.search(self._prepare_previous_invoices_domain(inv), limit=1) - if invoices: - date_invoice_format = datetime.datetime( - year=inv.date_invoice.year, - month=inv.date_invoice.month, - day=inv.date_invoice.day, - ) - date_invoice_tz = format_date( - self.env, fields.Date.context_today(self, date_invoice_format) - ) - raise UserError( - _( - "Chronology Error. Please confirm older draft invoices " - "before {date_invoice} and try again." - ).format(date_invoice=date_invoice_tz) - ) - if inv not in previously_validated: - invoices = self.search( - self._prepare_later_invoices_domain(inv), limit=1 - ) - if invoices: - date_invoice_format = datetime.datetime( - year=inv.date_invoice.year, - month=inv.date_invoice.month, - day=inv.date_invoice.day, - ) - date_invoice_tz = format_date( - self.env, fields.Date.context_today(self, date_invoice_format) - ) - raise UserError( - _( - "Chronology Error. There exist at least one invoice " - "with a later date to {date_invoice}." - ).format(date_invoice=date_invoice_tz) - ) - return res diff --git a/account_invoice_constraint_chronology/model/account_journal.py b/account_invoice_constraint_chronology/model/account_journal.py index 71f83a99..993827f2 100644 --- a/account_invoice_constraint_chronology/model/account_journal.py +++ b/account_invoice_constraint_chronology/model/account_journal.py @@ -1,16 +1,17 @@ # Copyright 2015-2019 ACSONE SA/NV () +# Copyright 2021 CorporateHub (https://corporatehub.eu) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from odoo import api, fields, models class AccountJournal(models.Model): - _inherit = ["account.journal"] + _inherit = "account.journal" - check_chronology = fields.Boolean(default=False,) + check_chronology = fields.Boolean() @api.onchange("type") def _onchange_type(self): - self.ensure_one() + super()._onchange_type() if self.type not in ["sale", "purchase"]: self.check_chronology = False diff --git a/account_invoice_constraint_chronology/model/account_move.py b/account_invoice_constraint_chronology/model/account_move.py new file mode 100644 index 00000000..e7fa076a --- /dev/null +++ b/account_invoice_constraint_chronology/model/account_move.py @@ -0,0 +1,79 @@ +# Copyright 2015-2019 ACSONE SA/NV () +# Copyright 2021 CorporateHub (https://corporatehub.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import _, models +from odoo.exceptions import UserError +from odoo.osv import expression +from odoo.tools.misc import format_date + + +class AccountMove(models.Model): + _inherit = "account.move" + + def _get_conflicting_invoices_domain(self): + self.ensure_one() + domain = [ + ("journal_id", "=", self.journal_id.id), + ("type", "!=", "entry"), + ] + if ( + self.journal_id.refund_sequence + and self.journal_id.sequence_id != self.journal_id.refund_sequence_id + ): + domain.append(("type", "=", self.type)) + return domain + + def _get_older_conflicting_invoices_domain(self): + self.ensure_one() + return expression.AND( + [ + self._get_conflicting_invoices_domain(), + [ + ("state", "=", "draft"), + ("invoice_date", "!=", False), + ("invoice_date", "<", self.invoice_date), + ], + ] + ) + + def _raise_older_conflicting_invoices(self): + self.ensure_one() + raise UserError( + _( + "Chronology conflict: A conflicting draft invoice dated before " + "{date_invoice} exists, please validate it first." + ).format(date_invoice=format_date(self.env, self.invoice_date)) + ) + + def _get_newer_conflicting_invoices_domain(self): + self.ensure_one() + return expression.AND( + [ + self._get_conflicting_invoices_domain(), + [("state", "=", "posted"), ("invoice_date", ">", self.invoice_date)], + ] + ) + + def _raise_newer_conflicting_invoices(self): + self.ensure_one() + raise UserError( + _( + "Chronology conflict: A conflicting validated invoice dated after " + "{date_invoice} exists." + ).format(date_invoice=format_date(self.env, self.invoice_date)) + ) + + def write(self, vals): + if vals.get("state") != "posted": + return super().write(vals) + + newly_posted = self.filtered(lambda move: move.state != "posted") + res = super().write(vals) + for move in newly_posted & self.filtered("journal_id.check_chronology"): + if self.search(move._get_older_conflicting_invoices_domain(), limit=1): + move._raise_older_conflicting_invoices() + if self.search(move._get_newer_conflicting_invoices_domain(), limit=1): + move._raise_newer_conflicting_invoices() + + return res diff --git a/account_invoice_constraint_chronology/readme/CONTRIBUTORS.rst b/account_invoice_constraint_chronology/readme/CONTRIBUTORS.rst index 3c2da210..d432e542 100644 --- a/account_invoice_constraint_chronology/readme/CONTRIBUTORS.rst +++ b/account_invoice_constraint_chronology/readme/CONTRIBUTORS.rst @@ -3,3 +3,6 @@ * Francesco Apruzzese * Thomas Binsfeld * Souheil Bejaoui +* `CorporateHub `__ + + * Alexey Pelykh diff --git a/account_invoice_constraint_chronology/tests/__init__.py b/account_invoice_constraint_chronology/tests/__init__.py index 1f3c5398..f2b0262b 100644 --- a/account_invoice_constraint_chronology/tests/__init__.py +++ b/account_invoice_constraint_chronology/tests/__init__.py @@ -1 +1,3 @@ -from . import test_account_constraint_chronology +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). + +from . import test_account_invoice_constraint_chronology diff --git a/account_invoice_constraint_chronology/tests/test_account_constraint_chronology.py b/account_invoice_constraint_chronology/tests/test_account_constraint_chronology.py deleted file mode 100644 index 4a2501c4..00000000 --- a/account_invoice_constraint_chronology/tests/test_account_constraint_chronology.py +++ /dev/null @@ -1,190 +0,0 @@ -# Copyright 2015-2019 ACSONE SA/NV () -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). - -from datetime import datetime, timedelta - -import odoo.tests.common as common -from odoo.exceptions import UserError -from odoo.tools import DEFAULT_SERVER_DATE_FORMAT - - -class TestAccountConstraintChronology(common.SavepointCase): - @classmethod - def setUpClass(cls): - super(TestAccountConstraintChronology, cls).setUpClass() - - # Needed to create invoice - - cls.account_type1 = cls.env["account.account.type"].create( - { - "name": "acc type test 1", - "type": "receivable", - "include_initial_balance": True, - } - ) - cls.account_type2 = cls.env["account.account.type"].create( - { - "name": "acc type test 2", - "type": "other", - "include_initial_balance": True, - } - ) - cls.account_account = cls.env["account.account"].create( - { - "name": "acc test", - "code": "X2020", - "user_type_id": cls.account_type1.id, - "reconcile": True, - } - ) - cls.account_account_line = cls.env["account.account"].create( - { - "name": "acc inv line test", - "code": "X2021", - "user_type_id": cls.account_type2.id, - "reconcile": True, - } - ) - cls.sequence = cls.env["ir.sequence"].create( - { - "name": "Journal Sale", - "prefix": "SALE", - "padding": 6, - "company_id": cls.env.ref("base.main_company").id, - } - ) - cls.account_journal_sale = cls.env["account.journal"].create( - { - "name": "Sale journal", - "code": "SALE", - "type": "sale", - "sequence_id": cls.sequence.id, - } - ) - cls.product = cls.env["product.product"].create({"name": "product name"}) - cls.analytic_account = cls.env["account.analytic.account"].create( - {"name": "test account"} - ) - - def get_journal_check(self, value): - journal = self.account_journal_sale.copy() - journal.check_chronology = value - return journal - - def create_simple_invoice(self, journal_id, date): - invoice = self.env["account.invoice"].create( - { - "partner_id": self.env.ref("base.res_partner_2").id, - "account_id": self.account_account.id, - "type": "in_invoice", - "journal_id": journal_id, - "date_invoice": date, - "state": "draft", - } - ) - - self.env["account.invoice.line"].create( - { - "product_id": self.product.id, - "quantity": 1.0, - "price_unit": 100.0, - "invoice_id": invoice.id, - "name": "product that cost 100", - "account_id": self.account_account_line.id, - "account_analytic_id": self.analytic_account.id, - } - ) - return invoice - - def test_invoice_draft(self): - journal = self.get_journal_check(True) - today = datetime.now() - yesterday = today - timedelta(days=1) - date = yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) - self.create_simple_invoice(journal.id, date) - date = today.strftime(DEFAULT_SERVER_DATE_FORMAT) - invoice_2 = self.create_simple_invoice(journal.id, date) - self.assertTrue( - (invoice_2.state == "draft"), "Initial invoice state is not Draft" - ) - with self.assertRaises(UserError): - invoice_2.action_invoice_open() - - def test_invoice_draft_no_check(self): - journal = self.get_journal_check(False) - today = datetime.now() - yesterday = today - timedelta(days=1) - date = yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) - self.create_simple_invoice(journal.id, date) - date = today.strftime(DEFAULT_SERVER_DATE_FORMAT) - invoice_2 = self.create_simple_invoice(journal.id, date) - self.assertTrue( - (invoice_2.state == "draft"), "Initial invoice state is not Draft" - ) - self.assertTrue(invoice_2.action_invoice_open()) - - def test_invoice_validate(self): - journal = self.get_journal_check(True) - today = datetime.now() - tomorrow = today + timedelta(days=1) - date_tomorrow = tomorrow.strftime(DEFAULT_SERVER_DATE_FORMAT) - invoice_1 = self.create_simple_invoice(journal.id, date_tomorrow) - self.assertTrue( - (invoice_1.state == "draft"), "Initial invoice state is not Draft" - ) - invoice_1.action_invoice_open() - date = today.strftime(DEFAULT_SERVER_DATE_FORMAT) - invoice_2 = self.create_simple_invoice(journal.id, date) - self.assertTrue( - (invoice_2.state == "draft"), "Initial invoice state is not Draft" - ) - with self.assertRaises(UserError): - invoice_2.action_invoice_open() - - def test_invoice_without_date(self): - journal = self.get_journal_check(True) - today = datetime.now() - yesterday = today - timedelta(days=1) - date = yesterday.strftime(DEFAULT_SERVER_DATE_FORMAT) - self.create_simple_invoice(journal.id, date) - invoice_2 = self.create_simple_invoice(journal.id, False) - self.assertTrue( - (invoice_2.state == "draft"), "Initial invoice state is not Draft" - ) - with self.assertRaises(UserError): - invoice_2.action_invoice_open() - - def test_journal_change_type(self): - self.account_journal_sale.check_chronology = True - self.assertTrue(self.account_journal_sale.check_chronology) - self.account_journal_sale.type = "bank" - self.account_journal_sale._onchange_type() - self.assertFalse(self.account_journal_sale.check_chronology) - - def test_invoice_refund(self): - journal = self.get_journal_check(True) - today = datetime.now() - tomorrow = today + timedelta(days=1) - date_tomorrow = tomorrow.strftime(DEFAULT_SERVER_DATE_FORMAT) - invoice_1 = self.create_simple_invoice(journal.id, date_tomorrow) - self.assertTrue( - (invoice_1.state == "draft"), "Initial invoice state is not Draft" - ) - invoice_1.action_invoice_open() - date = today.strftime(DEFAULT_SERVER_DATE_FORMAT) - refund_invoice_wiz = ( - self.env["account.invoice.refund"] - .with_context(active_ids=[invoice_1.id]) - .create( - { - "description": "test_invoice_refund", - "filter_refund": "cancel", - "date": date, - "date_invoice": date, - } - ) - ) - with self.assertRaises(UserError): - refund_invoice_wiz.invoice_refund() - invoice_1.journal_id.refund_sequence = True - refund_invoice_wiz.invoice_refund() diff --git a/account_invoice_constraint_chronology/tests/test_account_invoice_constraint_chronology.py b/account_invoice_constraint_chronology/tests/test_account_invoice_constraint_chronology.py new file mode 100644 index 00000000..97f8866e --- /dev/null +++ b/account_invoice_constraint_chronology/tests/test_account_invoice_constraint_chronology.py @@ -0,0 +1,131 @@ +# Copyright 2015-2019 ACSONE SA/NV () +# Copyright 2021 CorporateHub (https://corporatehub.eu) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from datetime import timedelta + +from odoo import fields +from odoo.exceptions import UserError +from odoo.tests import common + + +class TestAccountInvoiceConstraintChronology(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + + cls.company = cls.env.ref("base.main_company") + cls.partner_2 = cls.env.ref("base.res_partner_2") + cls.today = fields.Date.today() + cls.yesterday = cls.today - timedelta(days=1) + cls.tomorrow = cls.today + timedelta(days=1) + + cls.IrSequence = cls.env["ir.sequence"] + cls.sale_journal_sequence = cls.IrSequence.create( + { + "name": "Sale journal sequence", + "prefix": "SALE", + "padding": 6, + "company_id": cls.company.id, + } + ) + + cls.AccountJournal = cls.env["account.journal"] + cls.sale_journal = cls.AccountJournal.create( + { + "name": "Sale journal", + "code": "SALE", + "type": "sale", + "sequence_id": cls.sale_journal_sequence.id, + "check_chronology": True, + } + ) + + cls.ProductProduct = cls.env["product.product"] + cls.product = cls.ProductProduct.create({"name": "Product"}) + + cls.AccountMove = cls.env["account.move"] + with common.Form( + cls.AccountMove.with_context(default_type="out_invoice") + ) as invoice_form: + invoice_form.invoice_date = cls.today + invoice_form.partner_id = cls.partner_2 + invoice_form.journal_id = cls.sale_journal + with invoice_form.invoice_line_ids.new() as line_form: + line_form.product_id = cls.product + cls.invoice_1 = invoice_form.save() + cls.invoice_2 = cls.invoice_1.copy() + + cls.AccountMoveReversal = cls.env["account.move.reversal"] + + def test_journal_type_change(self): + self.assertTrue(self.sale_journal.check_chronology) + + with common.Form(self.sale_journal) as form: + form.type = "general" + self.assertFalse(self.sale_journal.check_chronology) + + with common.Form(self.sale_journal) as form: + form.type = "sale" + self.assertFalse(self.sale_journal.check_chronology) + + with common.Form(self.sale_journal) as form: + form.check_chronology = True + self.assertTrue(self.sale_journal.check_chronology) + + def test_invoice_draft(self): + self.invoice_1.invoice_date = self.yesterday + self.invoice_2.invoice_date = self.today + with self.assertRaises(UserError): + self.invoice_2.action_post() + + def test_invoice_draft_nocheck(self): + self.invoice_1.invoice_date = self.yesterday + self.invoice_2.invoice_date = self.today + self.sale_journal.check_chronology = False + self.invoice_2.action_post() + + def test_invoice_validate(self): + self.invoice_1.invoice_date = self.tomorrow + self.invoice_1.action_post() + self.invoice_2.invoice_date = self.today + with self.assertRaises(UserError): + self.invoice_2.action_post() + + def test_invoice_without_date(self): + self.invoice_1.invoice_date = self.yesterday + self.invoice_2.invoice_date = False + with self.assertRaises(UserError): + self.invoice_2.action_post() + + def test_invoice_refund_before(self): + self.invoice_1.invoice_date = self.tomorrow + self.invoice_1.action_post() + refund = ( + self.AccountMoveReversal.with_context( + active_model="account.move", active_ids=self.invoice_1.ids, + ) + .create( + {"date": self.today, "reason": "no reason", "refund_method": "refund"} + ) + .reverse_moves() + ) + refund = self.AccountMove.browse(refund["res_id"]) + refund.action_post() + + def test_invoice_refund_before_same_sequence(self): + self.sale_journal.refund_sequence = False + self.invoice_1.invoice_date = self.tomorrow + self.invoice_1.action_post() + refund = ( + self.AccountMoveReversal.with_context( + active_model="account.move", active_ids=self.invoice_1.ids, + ) + .create( + {"date": self.today, "reason": "no reason", "refund_method": "refund"} + ) + .reverse_moves() + ) + refund = self.AccountMove.browse(refund["res_id"]) + with self.assertRaises(UserError): + refund.action_post() diff --git a/account_invoice_constraint_chronology/view/account_view.xml b/account_invoice_constraint_chronology/view/account_journal.xml similarity index 50% rename from account_invoice_constraint_chronology/view/account_view.xml rename to account_invoice_constraint_chronology/view/account_journal.xml index 718bbe94..49957cba 100644 --- a/account_invoice_constraint_chronology/view/account_view.xml +++ b/account_invoice_constraint_chronology/view/account_journal.xml @@ -1,16 +1,21 @@ - + - account.journal.form (account_constraint_chronology) + + account.journal.form (account_invoice_constraint_chronology) + account.journal - +