diff --git a/.docker_files/main/__manifest__.py b/.docker_files/main/__manifest__.py index 9d1f3be6..e5cc5972 100644 --- a/.docker_files/main/__manifest__.py +++ b/.docker_files/main/__manifest__.py @@ -15,6 +15,7 @@ "account_bank_menu", "account_closing_journal", "account_invoice_constraint_chronology_forced", + "account_negative_debit_credit", "account_payment_cancel_group", "account_show_full_features", "invoice_refund_not_earlier", diff --git a/Dockerfile b/Dockerfile index b242319b..2686259a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,6 +18,7 @@ USER odoo COPY account_bank_menu /mnt/extra-addons/account_bank_menu COPY account_closing_journal /mnt/extra-addons/account_closing_journal COPY account_invoice_constraint_chronology_forced /mnt/extra-addons/account_invoice_constraint_chronology_forced +COPY account_negative_debit_credit /mnt/extra-addons/account_negative_debit_credit COPY account_payment_cancel_group /mnt/extra-addons/account_payment_cancel_group COPY account_show_full_features /mnt/extra-addons/account_show_full_features COPY invoice_refund_not_earlier /mnt/extra-addons/invoice_refund_not_earlier diff --git a/account_negative_debit_credit/README.rst b/account_negative_debit_credit/README.rst new file mode 100644 index 00000000..59fc8e07 --- /dev/null +++ b/account_negative_debit_credit/README.rst @@ -0,0 +1,33 @@ +===================== +Negative Debit/Credit +===================== +In accounting, the following logic is often repeated: + +* If my invoice line is positive, put the amount in the debit column. +* Otherwise, put the inverse amount in the credit column. + +The Problem +----------- +The problem with that logic is that it is repeated almost everywhere an accounting entry is created +(invoicing, payments, expenses, payroll, etc.). + +When an exception is raised because of a comparison error, the message is very nonspeaking to the user +and very hard to debug for the developper. + +The Solution +------------ +What the system could do instead is the following: + +* Put the amount in the debit column. + +Then, in a lower layer of code, the system would apply the following logic: + +* If the debit is negative, put the inverse amount in the credit column. +* If the credit is negative, put the inverse amount in the credit column. + +This is what this module does. It replaces the debit/credit amounts in the appropriate column +when creating accounting entries. + +Contributors +------------ +* Numigi (tm) and all its contributors (https://bit.ly/numigiens) diff --git a/account_negative_debit_credit/__init__.py b/account_negative_debit_credit/__init__.py new file mode 100644 index 00000000..63bd6ae3 --- /dev/null +++ b/account_negative_debit_credit/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import models diff --git a/account_negative_debit_credit/__manifest__.py b/account_negative_debit_credit/__manifest__.py new file mode 100644 index 00000000..379d4263 --- /dev/null +++ b/account_negative_debit_credit/__manifest__.py @@ -0,0 +1,16 @@ +# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +{ + "name": "Negative Debit/Credit", + "version": "16.0.1.0.0", + "author": "Numigi", + "maintainer": "Numigi", + "website": "https://www.numigi.com", + "license": "LGPL-3", + "category": "Accounting", + "summary": "Allow writing negative amounts in debit/credit columns.", + "depends": ["account"], + "installable": True, + "application": False, +} diff --git a/account_negative_debit_credit/models/__init__.py b/account_negative_debit_credit/models/__init__.py new file mode 100644 index 00000000..ee65327d --- /dev/null +++ b/account_negative_debit_credit/models/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import account_move_line diff --git a/account_negative_debit_credit/models/account_move_line.py b/account_negative_debit_credit/models/account_move_line.py new file mode 100644 index 00000000..2703b217 --- /dev/null +++ b/account_negative_debit_credit/models/account_move_line.py @@ -0,0 +1,30 @@ +# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from odoo import api, models + + +class AccountMoveLine(models.Model): + + _inherit = "account.move.line" + + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + _update_vals_debit_credit(vals) + return super().create(vals_list) + + def write(self, vals): + _update_vals_debit_credit(vals) + return super().write(vals) + + +def _update_vals_debit_credit(vals): + """Update debit and credit fields in a values dictionnary. + + If either the debit or the credit is negative, permutate the two fields. + + :param vals: a dictionnary of values + """ + if vals.get("debit", 0) < 0 or vals.get("credit", 0) < 0: + vals["debit"], vals["credit"] = -vals.get("credit", 0), -vals.get("debit", 0) diff --git a/account_negative_debit_credit/static/description/icon.png b/account_negative_debit_credit/static/description/icon.png new file mode 100644 index 00000000..92a86b10 Binary files /dev/null and b/account_negative_debit_credit/static/description/icon.png differ diff --git a/account_negative_debit_credit/tests/__init__.py b/account_negative_debit_credit/tests/__init__.py new file mode 100644 index 00000000..b24c13bb --- /dev/null +++ b/account_negative_debit_credit/tests/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from . import test_account_move_line diff --git a/account_negative_debit_credit/tests/test_account_move_line.py b/account_negative_debit_credit/tests/test_account_move_line.py new file mode 100644 index 00000000..eb268061 --- /dev/null +++ b/account_negative_debit_credit/tests/test_account_move_line.py @@ -0,0 +1,166 @@ +# Copyright 2024-today Numigi and all its contributors (https://bit.ly/numigiens) +# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). + +from datetime import datetime +from odoo.tests import common + + +class TestAccountMoveLine(common.SavepointCase): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.journal = cls.env["account.journal"].create( + { + "company_id": cls.env.user.company_id.id, + "code": "TEST", + "name": "Test Journal", + "type": "general", + } + ) + + cls.expense = cls.env["account.account"].create( + { + "account_type": "expense", + "name": "Revenus", + "code": "400001", + } + ) + + cls.asset = cls.env["account.account"].create( + { + "account_type": "asset_current", + "name": "Assets", + "code": "100001", + } + ) + + def _get_expense_line(self, move): + return move.line_ids.filtered(lambda l: l.account_id == self.expense) + + def _get_asset_line(self, move): + return move.line_ids.filtered(lambda l: l.account_id == self.asset) + + def test_negative_debit(self): + move = self._create_move( + [ + ( + 0, + 0, + { + "account_id": self.expense.id, + "debit": -10, + }, + ), + ( + 0, + 0, + { + "account_id": self.asset.id, + "debit": 10, + }, + ), + ] + ) + + line = move.line_ids[0] + assert line.debit == 0 + assert line.credit == 10 + + def test_negative_credit(self): + move = self._create_move( + [ + ( + 0, + 0, + { + "account_id": self.expense.id, + "credit": 10, + }, + ), + ( + 0, + 0, + { + "account_id": self.asset.id, + "credit": -10, + }, + ), + ] + ) + + line = move.line_ids[1] + assert line.debit == 10 + assert line.credit == 0 + + def test_negative_debit_and_credit(self): + move = self._create_move( + [ + ( + 0, + 0, + { + "account_id": self.expense.id, + "credit": 10, + }, + ), + ( + 0, + 0, + { + "account_id": self.asset.id, + "credit": -10, + }, + ), + ] + ) + + expense_line = move.line_ids[0] + assert expense_line.debit == 0 + assert expense_line.credit == 10 + + asset_line = move.line_ids[1] + assert asset_line.debit == 10 + assert asset_line.credit == 0 + + def test_write(self): + move = self._create_move( + [ + ( + 0, + 0, + { + "account_id": self.expense.id, + "debit": 10, + }, + ), + ( + 0, + 0, + { + "account_id": self.asset.id, + "credit": 10, + }, + ), + ] + ) + lines = move.line_ids.sorted() + move.write( + { + "line_ids": [ + (1, lines[0].id, {"credit": -10}), + (1, lines[1].id, {"debit": -10}), + ], + } + ) + + assert move.line_ids[1].credit == 10 + assert move.line_ids[0].debit == 10 + + def _create_move(self, line_vals): + return self.env["account.move"].create( + { + "journal_id": self.journal.id, + "date": datetime.now(), + "line_ids": line_vals, + } + )