diff --git a/account_netting/__manifest__.py b/account_netting/__manifest__.py
index 510ee012..5111cbd2 100644
--- a/account_netting/__manifest__.py
+++ b/account_netting/__manifest__.py
@@ -4,7 +4,7 @@
{
"name": "Account netting",
- "version": "15.0.1.0.0",
+ "version": "16.0.1.0.0",
"summary": "Compensate AR/AP accounts from the same partner",
"category": "Accounting & Finance",
"author": "Tecnativa, Odoo Community Association (OCA)",
diff --git a/account_netting/tests/test_account_netting.py b/account_netting/tests/test_account_netting.py
index dfdde521..19378a87 100644
--- a/account_netting/tests/test_account_netting.py
+++ b/account_netting/tests/test_account_netting.py
@@ -6,15 +6,16 @@
from datetime import datetime
import odoo.tests.common as common
-from odoo.exceptions import ValidationError
+from odoo.exceptions import UserError
from odoo.tests import Form, tagged
-@tagged("post_install")
+@tagged("post_install", "-at_install")
class TestAccountNetting(common.TransactionCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
+ cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
res_users_account_manager = cls.env.ref("account.group_account_manager")
partner_manager = cls.env.ref("base.group_partner_manager")
cls.env.user.write(
@@ -23,19 +24,25 @@ class TestAccountNetting(common.TransactionCase):
cls.company = cls.env.ref("base.main_company")
# only adviser can create an account
cls.aa_model = cls.env["account.account"]
- cls.account_receivable = cls._get_account(cls, "receivable")
- cls.account_payable = cls._get_account(cls, "payable")
- cls.account_revenue = cls._get_account(cls, "revenue")
- cls.account_expense = cls._get_account(cls, "expenses")
+ cls.account_receivable = cls._get_account(cls, "asset_receivable")
+ cls.account_payable = cls._get_account(cls, "liability_payable")
+ cls.account_revenue = cls._get_account(cls, "income")
+ cls.account_expense = cls._get_account(cls, "expense")
cls.partner_model = cls.env["res.partner"]
- cls.partner = cls._create_partner(cls, "Supplier/Customer")
cls.partner1 = cls._create_partner(cls, "Supplier/Customer 1")
+ cls.partner2 = cls._create_partner(cls, "Supplier/Customer 2")
cls.miscellaneous_journal = cls.env["account.journal"].search(
- [("type", "=", "general")], limit=1
+ [("type", "=", "general"), ("company_id", "=", cls.company.id)], limit=1
)
# We need a product with taxes at 0 so that the amounts are as expected.
cls.account_tax = cls.env["account.tax"].create(
- {"name": "0%", "amount_type": "fixed", "type_tax_use": "sale", "amount": 0}
+ {
+ "name": "0%",
+ "amount_type": "fixed",
+ "type_tax_use": "sale",
+ "amount": 0,
+ "company_id": cls.company.id,
+ }
)
cls.product = cls.env["product.product"].create(
{
@@ -44,40 +51,39 @@ class TestAccountNetting(common.TransactionCase):
"taxes_id": [(6, 0, [cls.account_tax.id])],
}
)
- out_invoice_partner = cls._create_move(cls, "out_invoice", cls.partner, 100)
- out_invoice_partner.action_post()
- cls.move_line_1 = out_invoice_partner.line_ids.filtered(
+ out_invoice_partner1 = cls._create_move(cls, "out_invoice", cls.partner1, 100)
+ out_invoice_partner1.action_post()
+ cls.move_line_1 = out_invoice_partner1.line_ids.filtered(
lambda x: x.account_id == cls.account_receivable
)
- in_invoice_partner = cls._create_move(cls, "in_invoice", cls.partner, 1200)
- in_invoice_partner.action_post()
- cls.move_line_2 = in_invoice_partner.line_ids.filtered(
+ in_invoice_partner1 = cls._create_move(cls, "in_invoice", cls.partner1, 1200)
+ in_invoice_partner1.action_post()
+ cls.move_line_2 = in_invoice_partner1.line_ids.filtered(
lambda x: x.account_id == cls.account_payable
)
- cls.move_line_3 = in_invoice_partner.line_ids.filtered(
+ cls.move_line_3 = in_invoice_partner1.line_ids.filtered(
lambda x: x.account_id == cls.account_expense
)
- in_invoice_partner1 = cls._create_move(cls, "in_invoice", cls.partner1, 200)
- in_invoice_partner1.action_post()
- cls.move_line_4 = in_invoice_partner1.line_ids.filtered(
+ in_invoice_partner2 = cls._create_move(cls, "in_invoice", cls.partner2, 200)
+ in_invoice_partner2.action_post()
+ cls.move_line_4 = in_invoice_partner2.line_ids.filtered(
lambda x: x.account_id == cls.account_payable
)
- in_refund_partner1 = cls._create_move(cls, "in_refund", cls.partner1, 200)
- in_refund_partner1.action_post()
- cls.move_line_5 = in_refund_partner1.line_ids.filtered(
+ in_refund_partner2 = cls._create_move(cls, "in_refund", cls.partner2, 200)
+ in_refund_partner2.action_post()
+ cls.move_line_5 = in_refund_partner2.line_ids.filtered(
lambda x: x.account_id == cls.account_payable
)
- in_refund_partner1 = cls._create_move(cls, "in_refund", cls.partner1, 200)
- in_refund_partner1.action_post()
- cls.move_line_6 = in_refund_partner1.line_ids.filtered(
+ in_refund_partner2 = cls._create_move(cls, "in_refund", cls.partner2, 200)
+ in_refund_partner2.action_post()
+ cls.move_line_6 = in_refund_partner2.line_ids.filtered(
lambda x: x.account_id == cls.account_payable
)
- def _get_account(self, user_type):
- user_type_ref = "account.data_account_type_%s" % user_type
+ def _get_account(self, account_type):
return self.aa_model.search(
[
- ("user_type_id", "=", self.env.ref(user_type_ref).id),
+ ("account_type", "=", account_type),
("company_id", "=", self.company.id),
],
limit=1,
@@ -94,7 +100,9 @@ class TestAccountNetting(common.TransactionCase):
def _create_move(self, move_type, partner, price):
move_form = Form(
- self.env["account.move"].with_context(
+ self.env["account.move"]
+ .with_company(self.company.id)
+ .with_context(
default_move_type=move_type,
)
)
@@ -106,22 +114,22 @@ class TestAccountNetting(common.TransactionCase):
return move_form.save()
def test_compensation(self):
- # Test exception line 33 from account_move_make_netting
+ # Test raise if 1 account.move.line selected
obj = self.env["account.move.make.netting"].with_context(
active_ids=[self.move_line_1.id]
)
- with self.assertRaises(ValidationError):
+ with self.assertRaises(UserError):
wizard = obj.create(
{
"move_line_ids": [(6, 0, [self.move_line_1.id])],
"journal_id": self.miscellaneous_journal.id,
}
)
- # Test exception line 39 from account_move_make_netting
+ # Test raise if not all accounts are payable/receivable
obj = self.env["account.move.make.netting"].with_context(
active_ids=[self.move_line_1.id, self.move_line_3.id]
)
- with self.assertRaises(ValidationError):
+ with self.assertRaises(UserError):
wizard = obj.create(
{
"move_line_ids": [
@@ -130,11 +138,11 @@ class TestAccountNetting(common.TransactionCase):
"journal_id": self.miscellaneous_journal.id,
}
)
- # Test exception line 45 from account_move_make_netting
+ # Test raise if same account
obj = self.env["account.move.make.netting"].with_context(
active_ids=[self.move_line_4.id, self.move_line_5.id]
)
- with self.assertRaises(ValidationError):
+ with self.assertRaises(UserError):
wizard = obj.create(
{
"move_line_ids": [
@@ -143,7 +151,7 @@ class TestAccountNetting(common.TransactionCase):
"journal_id": self.miscellaneous_journal.id,
}
)
- # Test exception line 42 from account_move_make_netting
+ # Test raise if reconciled lines
moves = self.env["account.move.line"].browse(
[self.move_line_4.id, self.move_line_5.id]
)
@@ -151,7 +159,7 @@ class TestAccountNetting(common.TransactionCase):
obj = self.env["account.move.make.netting"].with_context(
active_ids=[self.move_line_4.id, self.move_line_5.id]
)
- with self.assertRaises(ValidationError):
+ with self.assertRaises(UserError):
wizard = obj.create(
{
"move_line_ids": [
@@ -160,11 +168,11 @@ class TestAccountNetting(common.TransactionCase):
"journal_id": self.miscellaneous_journal.id,
}
)
- # Test exception line 52 from account_move_make_netting
+ # Test raise if different partners
obj = self.env["account.move.make.netting"].with_context(
active_ids=[self.move_line_1.id, self.move_line_6.id]
)
- with self.assertRaises(ValidationError):
+ with self.assertRaises(UserError):
wizard = obj.create(
{
"move_line_ids": [
@@ -182,6 +190,8 @@ class TestAccountNetting(common.TransactionCase):
"journal_id": self.miscellaneous_journal.id,
}
)
+ self.assertEqual(wizard.partner_id, self.partner1)
+ self.assertEqual(wizard.company_id, self.company)
res = wizard.button_compensate()
move = self.env["account.move"].browse(res["res_id"])
self.assertEqual(
diff --git a/account_netting/wizards/account_move_make_netting.py b/account_netting/wizards/account_move_make_netting.py
index 6554ca27..d986da1b 100644
--- a/account_netting/wizards/account_move_make_netting.py
+++ b/account_netting/wizards/account_move_make_netting.py
@@ -2,20 +2,29 @@
# 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
+from odoo import _, api, fields, models
+from odoo.exceptions import UserError
class AccountMoveMakeNetting(models.TransientModel):
_name = "account.move.make.netting"
_description = "Wizard to generate account moves for netting"
+ _check_company_auto = True
+ company_id = fields.Many2one("res.company", required=True)
journal_id = fields.Many2one(
comodel_name="account.journal",
required=True,
- domain="[('type', '=', 'general')]",
+ domain="[('type', '=', 'general'), ('company_id', '=', company_id)]",
+ check_company=True,
)
- move_line_ids = fields.Many2many(comodel_name="account.move.line")
- balance = fields.Float(readonly=True)
+ move_line_ids = fields.Many2many(
+ comodel_name="account.move.line",
+ check_company=True,
+ )
+ partner_id = fields.Many2one("res.partner", readonly=True)
+ company_currency_id = fields.Many2one(related="company_id.currency_id")
+ balance = fields.Monetary(readonly=True, currency_field="company_currency_id")
balance_type = fields.Selection(
selection=[("pay", "To pay"), ("receive", "To receive")],
readonly=True,
@@ -24,54 +33,79 @@ class AccountMoveMakeNetting(models.TransientModel):
@api.model
def default_get(self, fields_list):
if len(self.env.context.get("active_ids", [])) < 2:
- raise exceptions.ValidationError(
- _("You should compensate at least 2 journal entries.")
- )
+ raise UserError(_("You should compensate at least 2 journal items."))
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(
+ partners = self.env["res.partner"]
+ for line in move_lines:
+ if line.parent_state != "posted":
+ raise UserError(_("Line '%s' is not posted.") % line.display_name)
+ if line.account_id.account_type not in (
+ "liability_payable",
+ "asset_receivable",
+ ):
+ raise UserError(
+ _(
+ "Line '%(line)s' has account '%(account)s' which is not "
+ "a payable nor a receivable account."
+ )
+ % {
+ "line": line.display_name,
+ "account": line.account_id.display_name,
+ }
+ )
+ if line.reconciled:
+ raise UserError(
+ _("Line '%s' is already reconciled.") % line.display_name
+ )
+ if not line.partner_id:
+ raise UserError(
+ _("Line '%s' doesn't have a partner.") % line.display_name
+ )
+ partners |= line.partner_id
+
+ if len(move_lines.account_id) == 1:
+ raise UserError(
_(
"The 'Compensate' function is intended to balance "
- "operations on different accounts for the same partner.\n"
+ "operations on different accounts for the same partner. "
"In this case all selected entries belong to the same "
- "account.\n Please use the 'Reconcile' function."
+ "account '%s'. Use the 'Reconcile' function instead."
)
+ % move_lines.account_id.display_name
)
- if len(move_lines.mapped("partner_id")) != 1:
- raise exceptions.ValidationError(
+ if len(partners) != 1:
+ raise UserError(
_(
- "All entries should have a partner and the partner must "
- "be the same for all."
+ "The selected journal items have different partners: %s. "
+ "The partner must be the same for all the selected journal items."
)
+ % ", ".join([p.display_name for p in partners])
)
res = super().default_get(fields_list)
- res["move_line_ids"] = [(6, 0, move_lines.ids)]
+ company = self.env.company
+ ccur = company.currency_id
debit_move_lines_debit = move_lines.filtered("debit")
credit_move_lines_debit = move_lines.filtered("credit")
- balance = abs(sum(debit_move_lines_debit.mapped("amount_residual"))) - abs(
- sum(credit_move_lines_debit.mapped("amount_residual"))
+ balance = ccur.round(
+ abs(sum(debit_move_lines_debit.mapped("amount_residual")))
+ - abs(sum(credit_move_lines_debit.mapped("amount_residual")))
+ )
+ res.update(
+ {
+ "balance": abs(balance),
+ "balance_type": "pay"
+ if ccur.compare_amounts(balance, 0) < 0
+ else "receive",
+ "company_id": company.id,
+ "move_line_ids": move_lines.ids,
+ "partner_id": partners.id,
+ }
)
- res["balance"] = abs(balance)
- res["balance_type"] = "pay" if balance < 0 else "receive"
return res
- 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}
- )
+ def _prepare_account_move(self):
# Group amounts by account
account_groups = self.move_line_ids.read_group(
[("id", "in", self.move_line_ids.ids)],
@@ -80,44 +114,50 @@ class AccountMoveMakeNetting(models.TransientModel):
)
debtors = []
creditors = []
- total_debtors = 0
- total_creditors = 0
+ total_debtors = 0.0
+ total_creditors = 0.0
+ ccur = self.company_id.currency_id
for account_group in account_groups:
balance = account_group["amount_residual"]
group_vals = {
"account_id": account_group["account_id"][0],
"balance": abs(balance),
}
- if balance > 0:
+ if ccur.compare_amounts(balance, 0) > 0:
debtors.append(group_vals)
total_debtors += balance
else:
creditors.append(group_vals)
total_creditors += abs(balance)
- # Create move lines
+ # Compute 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,
- "name": move.ref,
+ field_map[i]: min(available_amount, account_group["balance"]),
+ "partner_id": self.partner_id.id,
"account_id": account_group["account_id"],
}
move_lines.append((0, 0, move_line_vals))
available_amount -= account_group["balance"]
- if available_amount <= 0:
+ if ccur.compare_amounts(available_amount, 0) <= 0:
break
- if move_lines:
- move.write({"line_ids": move_lines})
- move.action_post()
+ vals = {
+ "ref": _("AR/AP netting"),
+ "journal_id": self.journal_id.id,
+ "company_id": self.company_id.id,
+ "line_ids": move_lines,
+ }
+ return vals
+
+ def button_compensate(self):
+ self.ensure_one()
+ # Create account move
+ move = self.env["account.move"].create(self._prepare_account_move())
+ move.action_post()
# Make reconciliation
for move_line in move.line_ids:
to_reconcile = move_line + self.move_line_ids.filtered(
@@ -125,11 +165,15 @@ class AccountMoveMakeNetting(models.TransientModel):
)
to_reconcile.reconcile()
# Open created move
- action = self.env["ir.actions.act_window"]._for_xml_id(
+ action = self.env["ir.actions.actions"]._for_xml_id(
"account.action_move_journal_line"
)
- action["view_mode"] = "form"
- del action["views"]
- del action["view_id"]
- action["res_id"] = move.id
+ action.update(
+ {
+ "view_mode": "form",
+ "views": False,
+ "view_id": False,
+ "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
index fe2b21e6..4fc33d81 100644
--- a/account_netting/wizards/account_move_make_netting_view.xml
+++ b/account_netting/wizards/account_move_make_netting_view.xml
@@ -13,10 +13,13 @@