Merge pull request #556 from Tecnativa/10.0-mig-account_netting

[MIG] account_netting: Migration to 10.0
This commit is contained in:
Pedro M. Baeza
2017-12-02 16:22:17 +01:00
committed by GitHub
10 changed files with 542 additions and 0 deletions

View File

@@ -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
<https://github.com/OCA/account-financial-tools/issues>`_. In case of trouble,
please check there if your issue has already been reported. If you spotted it
first, help us smash it by providing detailed and welcomed feedback.
Credits
=======
Contributors
------------
* Pedro M. Baeza <pedro.baeza@tecnativa.com>
* Vicent Cubells <vicent.cubells@serviciosbaeza.com>
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.

View File

@@ -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

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Copyright 2015 Pedro M. Baeza
# Copyright 2017 Vicent Cubells - Tecnativa <vicent.cubells@tecnativa.com>
# 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,
}

138
account_netting/i18n/es.po Normal file
View File

@@ -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."

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -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

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<act_window name="Compensate"
res_model="account.move.make.netting"
src_model="account.move.line"
view_mode="form"
target="new"
key2="client_action_multi"
multi="True"
id="act_account_move_make_netting"/>
<record id="view_account_move_make_netting_form" model="ir.ui.view">
<field name="name">Compensate entries</field>
<field name="model">account.move.make.netting</field>
<field name="arch" type="xml">
<form string="Compensate entries">
<label string="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:"/>
<group>
<field name="balance"/>
<field name="balance_type"/>
</group>
<group string="Select the journal where storing the journal entries">
<field name="journal_id"/>
</group>
<footer>
<button name="button_compensate" string="Compensate" type="object" default_focus="1" class="btn-primary"/>
<button string="Cancel" class="btn-default" special="cancel" />
</footer>
</form>
</field>
</record>
</odoo>