diff --git a/account_netting/README.rst b/account_netting/README.rst new file mode 100644 index 000000000..50d73ee4d --- /dev/null +++ b/account_netting/README.rst @@ -0,0 +1,75 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 + +============= +AR/AP netting +============= + +This module allows to compensate the balance of a receivable account with the +balance of a payable account for the same partner, creating a journal item +that reflects this operation. + +**WARNING**: This operation can be forbidden in your country by the accounting +regulations, so you should check current laws before using it. For example, in +Spain, this is not allowed at first instance, unless you document well the +operation from both parties. + +Usage +===== + +From any account journal entries view: + +* Accounting/Journal Entries/Journal Items + +select all the lines that corresponds to both AR/AP operations from the same +partner. Click on "More > Compensate". If the items don't correspond to the +same partner or they aren't AR/AP accounts, you will get an error. + +On contrary, a dialog box will be presented with the result of the operation +and a selection of the journal to register the operation. When you click on the +"Compensate" button, a journal entry is created with the corresponding +counterparts of the AR/AP operations. + + +.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas + :alt: Try me on Runbot + :target: https://runbot.odoo-community.org/runbot/92/10.0 + +Known issues / Roadmap +====================== + +* We can add the possibility to pay the netting result amount directly from + the wizard. + +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 smash it by providing detailed and welcomed feedback. + +Credits +======= + +Contributors +------------ + +* Pedro M. Baeza +* Vicent Cubells + +Maintainer +---------- + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +This module is maintained by the OCA. + +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. + +To contribute to this module, please visit https://odoo-community.org. diff --git a/account_netting/__init__.py b/account_netting/__init__.py new file mode 100644 index 000000000..ee6156516 --- /dev/null +++ b/account_netting/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Pedro M. Baeza +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from . import wizards diff --git a/account_netting/__manifest__.py b/account_netting/__manifest__.py new file mode 100644 index 000000000..e4840f923 --- /dev/null +++ b/account_netting/__manifest__.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Pedro M. Baeza +# Copyright 2017 Vicent Cubells - Tecnativa +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +{ + 'name': 'Account netting', + 'version': '10.0.1.0.0', + 'summary': 'Compensate AR/AP accounts from the same partner', + 'category': 'Accounting & Finance', + 'author': 'Tecnativa, ' + 'Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': 'https://github.com/OCA/account-financial-tools/', + 'depends': [ + 'account', + ], + 'data': [ + 'wizards/account_move_make_netting_view.xml', + ], + 'installable': True, +} diff --git a/account_netting/i18n/es.po b/account_netting/i18n/es.po new file mode 100644 index 000000000..109f78d31 --- /dev/null +++ b/account_netting/i18n/es.po @@ -0,0 +1,138 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * account_netting +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 10.0\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2017-10-13 15:28+0000\n" +"PO-Revision-Date: 2017-10-13 15:28+0000\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_netting +#: code:addons/account_netting/wizards/account_move_make_netting.py:73 +#, python-format +msgid "AR/AP netting" +msgstr "Compensación a cobrar/a pagar" + +#. module: account_netting +#: code:addons/account_netting/wizards/account_move_make_netting.py:42 +#, python-format +msgid "All entries must have a receivable or payable account" +msgstr "Todos los apuntes deben tener una cuenta a pagar o a cobrar" + +#. module: account_netting +#: code:addons/account_netting/wizards/account_move_make_netting.py:45 +#, python-format +msgid "All entries mustn't been reconciled" +msgstr "Ningún apunte debe estar conciliado" + +#. module: account_netting +#: code:addons/account_netting/wizards/account_move_make_netting.py:57 +#, python-format +msgid "All entries should have a partner and the partner must be the same for all." +msgstr "Todos los apuntes deben tener una empresa y la empresa debe ser la misma para todos." + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_balance +msgid "Balance" +msgstr "Saldo" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_balance_type +msgid "Balance type" +msgstr "Tipo de saldo" + +#. module: account_netting +#: model:ir.ui.view,arch_db:account_netting.view_account_move_make_netting_form +msgid "Cancel" +msgstr "Cancelar" + +#. module: account_netting +#: model:ir.actions.act_window,name:account_netting.act_account_move_make_netting +#: model:ir.ui.view,arch_db:account_netting.view_account_move_make_netting_form +msgid "Compensate" +msgstr "Compensar" + +#. module: account_netting +#: model:ir.ui.view,arch_db:account_netting.view_account_move_make_netting_form +msgid "Compensate entries" +msgstr "Compensar apuntes" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_create_uid +msgid "Created by" +msgstr "Creado por" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_create_date +msgid "Created on" +msgstr "Creado el" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_journal_id +msgid "Journal id" +msgstr "Diario" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting___last_update +msgid "Last Modified on" +msgstr "Última modificación el" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_write_uid +msgid "Last Updated by" +msgstr "Última actualización por" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_write_date +msgid "Last Updated on" +msgstr "Última actualización en" + +#. module: account_netting +#: model:ir.model.fields,field_description:account_netting.field_account_move_make_netting_move_line_ids +msgid "Move line ids" +msgstr "Apuntes" + +#. module: account_netting +#: model:ir.ui.view,arch_db:account_netting.view_account_move_make_netting_form +msgid "Select the journal where storing the journal entries" +msgstr "Seleccione el diario en el que se guardarán los apuntes" + +#. module: account_netting +#: code:addons/account_netting/wizards/account_move_make_netting.py:48 +#, python-format +msgid "The 'Compensate' function is intended to balance operations on different accounts for the same partner.\n" +"In this case all selected entries belong to the same account.\n" +" Please use the 'Reconcile' function." +msgstr "la función 'Compensar' pretende compensar operaciones sobre diferentes cuentas del mismo cliente.\n" +"En este caso todos los apuntes seleccionados pertenencen a la misma cuenta.\n" +"Use entonces la función 'Reconciliar'." + +#. module: account_netting +#: model:ir.ui.view,arch_db:account_netting.view_account_move_make_netting_form +msgid "This operation will generate account entries that are counterpart of the receivable/payable accounts selected, and reconcile each other, letting this balance in the partner:" +msgstr "Esta operación generará apuntes que serán la contrapartida de las cuentas a cobrar/a pagar seleccionadas, y las reconciliará entre ellas, dejando este saldo en la empresa:" + +#. module: account_netting +#: selection:account.move.make.netting,balance_type:0 +msgid "To pay" +msgstr "A pagar" + +#. module: account_netting +#: selection:account.move.make.netting,balance_type:0 +msgid "To receive" +msgstr "A cobrar" + +#. module: account_netting +#: code:addons/account_netting/wizards/account_move_make_netting.py:36 +#, python-format +msgid "You should compensate at least 2 journal entries." +msgstr "Debe compensar al menos 2 apuntes." + diff --git a/account_netting/static/description/icon.png b/account_netting/static/description/icon.png new file mode 100644 index 000000000..3a0328b51 Binary files /dev/null and b/account_netting/static/description/icon.png differ diff --git a/account_netting/tests/__init__.py b/account_netting/tests/__init__.py new file mode 100644 index 000000000..544877d64 --- /dev/null +++ b/account_netting/tests/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Pedro M. Baeza +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from . import test_account_netting diff --git a/account_netting/tests/test_account_netting.py b/account_netting/tests/test_account_netting.py new file mode 100644 index 000000000..97800a638 --- /dev/null +++ b/account_netting/tests/test_account_netting.py @@ -0,0 +1,124 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Pedro M. Baeza +# Copyright 2017 Tecnativa - Vicent Cubells +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +import openerp.tests.common as common + + +class TestAccountNetting(common.SavepointCase): + @classmethod + def setUpClass(cls): + super(TestAccountNetting, cls).setUpClass() + cls.partner = cls.env['res.partner'].create({ + 'supplier': True, + 'customer': True, + 'name': "Supplier/Customer", + }) + res_users_account_manager = cls.env.ref( + 'account.group_account_manager') + partner_manager = cls.env.ref('base.group_partner_manager') + cls.env.user.write({ + 'groups_id': [ + (6, 0, [res_users_account_manager.id, partner_manager.id]) + ], + }) + # only adviser can create an account + cls.account_receivable = cls.env['account.account'].create({ + 'code': 'cust_acc', + 'name': 'customer account', + 'user_type_id': cls.env.ref( + 'account.data_account_type_receivable').id, + 'reconcile': True, + }) + cls.account_payable = cls.env['account.account'].create({ + 'code': 'supp_acc', + 'name': 'supplier account', + 'user_type_id': cls.env.ref( + 'account.data_account_type_payable').id, + 'reconcile': True, + }) + cls.account_revenue = cls.env['account.account'].search([ + ('user_type_id', '=', cls.env.ref( + 'account.data_account_type_revenue').id) + ], limit=1) + cls.account_expense = cls.env['account.account'].search([ + ('user_type_id', '=', cls.env.ref( + 'account.data_account_type_expenses').id) + ], limit=1) + cls.journal = cls.env['account.journal'].create({ + 'name': 'Test sale journal', + 'type': 'sale', + 'code': 'TEST', + }) + cls.expenses_journal = cls.env['account.journal'].create({ + 'name': 'Test expense journal', + 'type': 'purchase', + 'code': 'EXP', + }) + cls.miscellaneous_journal = cls.env['account.journal'].create({ + 'name': 'Miscellaneus journal', + 'type': 'general', + 'code': 'OTHER', + }) + cls.customer_invoice = cls.env['account.invoice'].create({ + 'journal_id': cls.journal.id, + 'type': 'out_invoice', + 'partner_id': cls.partner.id, + 'account_id': cls.account_receivable.id, + 'invoice_line_ids': [(0, 0, { + 'name': 'Test', + 'price_unit': 100.0, + 'account_id': cls.account_revenue.id, + })], + }) + cls.customer_invoice.action_invoice_open() + customer_move = cls.customer_invoice.move_id + cls.move_line_1 = customer_move.line_ids.filtered( + lambda x: x.account_id == cls.account_receivable) + cls.supplier_invoice = cls.env['account.invoice'].create({ + 'journal_id': cls.expenses_journal.id, + 'type': 'in_invoice', + 'partner_id': cls.partner.id, + 'account_id': cls.account_payable.id, + 'invoice_line_ids': [(0, 0, { + 'name': 'Test', + 'price_unit': 1200.0, + 'account_id': cls.account_expense.id, + })], + }) + cls.supplier_invoice.action_invoice_open() + supplier_move = cls.supplier_invoice.move_id + cls.move_line_2 = supplier_move.line_ids.filtered( + lambda x: x.account_id == cls.account_payable) + + def test_compensation(self): + obj = self.env['account.move.make.netting'].with_context( + active_ids=[self.move_line_1.id, self.move_line_2.id]) + wizard = obj.create( + {'move_line_ids': + [(6, 0, [self.move_line_1.id, self.move_line_2.id])], + 'journal_id': self.miscellaneous_journal.id}) + res = wizard.button_compensate() + move = self.env['account.move'].browse(res['res_id']) + self.assertEqual( + len(move.line_ids), 2, + 'AR/AP netting move has an incorrect line number') + move_line_receivable = move.line_ids.filtered( + lambda x: x.account_id == self.account_receivable) + self.assertEqual( + move_line_receivable.credit, 100, + 'Incorrect credit amount for receivable move line') + self.assertTrue( + move_line_receivable.reconciled and + move_line_receivable.full_reconcile_id, + 'Receivable move line should be totally reconciled') + move_line_payable = move.line_ids.filtered( + lambda x: x.account_id == self.account_payable) + self.assertEqual( + move_line_payable.debit, 100, + 'Incorrect debit amount for payable move line') + self.assertTrue( + move_line_payable.reconciled and not + move_line_payable.full_reconcile_id, + 'Receivable move line should be partially reconciled') diff --git a/account_netting/wizards/__init__.py b/account_netting/wizards/__init__.py new file mode 100644 index 000000000..ca14d9be6 --- /dev/null +++ b/account_netting/wizards/__init__.py @@ -0,0 +1,5 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Pedro M. Baeza +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from . import account_move_make_netting diff --git a/account_netting/wizards/account_move_make_netting.py b/account_netting/wizards/account_move_make_netting.py new file mode 100644 index 000000000..8f9dae99f --- /dev/null +++ b/account_netting/wizards/account_move_make_netting.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +# Copyright 2015 Pedro M. Baeza +# Copyright 2017 Tecnativa - Vicent Cubells +# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html + +from odoo import _, api, exceptions, fields, models + + +class AccountMoveMakeNetting(models.TransientModel): + _name = "account.move.make.netting" + + journal_id = fields.Many2one( + comodel_name="account.journal", + required=True, + domain="[('type', '=', 'general')]", + ) + move_line_ids = fields.Many2many( + comodel_name="account.move.line", + ) + balance = fields.Float( + readonly=True, + ) + balance_type = fields.Selection( + selection=[ + ('pay', 'To pay'), + ('receive', 'To receive'), + ], + readonly=True, + ) + + @api.model + def default_get(self, fields): + if len(self.env.context.get('active_ids', [])) < 2: + raise exceptions.ValidationError( + _("You should compensate at least 2 journal entries.")) + move_lines = self.env['account.move.line'].browse( + self.env.context['active_ids']) + if (any(x not in ('payable', 'receivable') for + x in move_lines.mapped('account_id.user_type_id.type'))): + raise exceptions.ValidationError( + _("All entries must have a receivable or payable account")) + if any(move_lines.mapped('reconciled')): + raise exceptions.ValidationError( + _("All entries mustn't been reconciled")) + if len(move_lines.mapped('account_id')) == 1: + raise exceptions.ValidationError( + _("The 'Compensate' function is intended to balance " + "operations on different accounts for the same partner.\n" + "In this case all selected entries belong to the same " + "account.\n Please use the 'Reconcile' function.")) + partner_id = None + for move in move_lines: + if (not move.partner_id or ( + move.partner_id != partner_id and partner_id is not None)): + raise exceptions.ValidationError( + _("All entries should have a partner and the partner must " + "be the same for all.")) + partner_id = move.partner_id + res = super(AccountMoveMakeNetting, self).default_get(fields) + res['move_line_ids'] = [(6, 0, move_lines.ids)] + balance = (sum(move_lines.mapped('debit')) - + sum(move_lines.mapped('credit'))) + res['balance'] = abs(balance) + res['balance_type'] = 'pay' if balance < 0 else 'receive' + return res + + @api.multi + def button_compensate(self): + self.ensure_one() + # Create account move + move = self.env['account.move'].create({ + 'ref': _('AR/AP netting'), + 'journal_id': self.journal_id.id, + }) + # Group amounts by account + account_groups = self.move_line_ids.read_group([ + ('id', 'in', self.move_line_ids.ids)], + ['account_id', 'debit', 'credit'], + ['account_id'], + ) + debtors = [] + creditors = [] + total_debtors = 0 + total_creditors = 0 + for account_group in account_groups: + balance = account_group['debit'] - account_group['credit'] + group_vals = { + 'account_id': account_group['account_id'][0], + 'balance': abs(balance), + } + if balance > 0: + debtors.append(group_vals) + total_debtors += balance + else: + creditors.append(group_vals) + total_creditors += abs(balance) + # Create move lines + netting_amount = min(total_creditors, total_debtors) + field_map = {1: 'debit', 0: 'credit'} + move_lines = [] + for i, group in enumerate([debtors, creditors]): + available_amount = netting_amount + for account_group in group: + if account_group['balance'] > available_amount: + amount = available_amount + else: + amount = account_group['balance'] + move_line_vals = { + field_map[i]: amount, + 'partner_id': self.move_line_ids[0].partner_id.id, + 'date': move.date, + 'journal_id': move.journal_id.id, + 'name': move.ref, + 'account_id': account_group['account_id'], + } + move_lines.append((0, 0, move_line_vals)) + available_amount -= account_group['balance'] + if available_amount <= 0: + break + if move_lines: + move.write({'line_ids': move_lines}) + # Make reconciliation + for move_line in move.line_ids: + to_reconcile = move_line + self.move_line_ids.filtered( + lambda x: x.account_id == move_line.account_id) + to_reconcile.reconcile() + # Open created move + action = self.env.ref('account.action_move_journal_line').read()[0] + action['view_mode'] = 'form' + del action['views'] + del action['view_id'] + action['res_id'] = move.id + return action diff --git a/account_netting/wizards/account_move_make_netting_view.xml b/account_netting/wizards/account_move_make_netting_view.xml new file mode 100644 index 000000000..08ff7704d --- /dev/null +++ b/account_netting/wizards/account_move_make_netting_view.xml @@ -0,0 +1,35 @@ + + + + + + + Compensate entries + account.move.make.netting + +
+
+
+ + +