diff --git a/account_reconcile_restrict_partner_mismatch/README.rst b/account_reconcile_restrict_partner_mismatch/README.rst
new file mode 100644
index 00000000..e69de29b
diff --git a/account_reconcile_restrict_partner_mismatch/__init__.py b/account_reconcile_restrict_partner_mismatch/__init__.py
new file mode 100755
index 00000000..bf588bc8
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import report
diff --git a/account_reconcile_restrict_partner_mismatch/__manifest__.py b/account_reconcile_restrict_partner_mismatch/__manifest__.py
new file mode 100644
index 00000000..a9e05f06
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/__manifest__.py
@@ -0,0 +1,19 @@
+# Copyright 2019 Camptocamp SA
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+{
+ 'name': 'Reconcile restrict partner mismatch',
+ 'summary': 'Restrict reconciliation on receivable '
+ 'and payable accounts to the same partner',
+ 'version': '12.0.1.0.0',
+ 'depends': ['account'],
+ 'author': 'Camptocamp, Odoo Community Association (OCA)',
+ 'website': 'http://www.github.com/OCA/account-reconcile',
+ 'category': 'Finance',
+ 'license': 'AGPL-3',
+ 'data': [
+ 'report/account_move_lines_report.xml',
+ 'security/ir.model.access.csv',
+ ],
+ 'installable': True,
+}
diff --git a/account_reconcile_restrict_partner_mismatch/models/__init__.py b/account_reconcile_restrict_partner_mismatch/models/__init__.py
new file mode 100755
index 00000000..8795b3be
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/models/__init__.py
@@ -0,0 +1 @@
+from . import account_move_line
diff --git a/account_reconcile_restrict_partner_mismatch/models/account_move_line.py b/account_reconcile_restrict_partner_mismatch/models/account_move_line.py
new file mode 100644
index 00000000..8ab5f13e
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/models/account_move_line.py
@@ -0,0 +1,29 @@
+# Copyright 2019 Camptocamp SA
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+from odoo import api, models, _
+from odoo.exceptions import UserError
+from odoo.tools import config
+
+
+class AccountMoveLine(models.Model):
+ _inherit = "account.move.line"
+
+ @api.multi
+ def reconcile(self, writeoff_acc_id=False, writeoff_journal_id=False):
+ if (config['test_enable']
+ and not self.env.context.get('test_partner_mismatch')):
+ return super().reconcile(writeoff_acc_id, writeoff_journal_id)
+
+ # to be consistent with parent method
+ if not self:
+ return True
+ partners = set()
+ for line in self:
+ if line.account_id.internal_type in ('receivable', 'payable'):
+ partners.add(line.partner_id.id)
+ if len(partners) > 1:
+ raise UserError(_('The partner has to be the same on all'
+ ' lines for receivable and payable accounts!'))
+ return super().reconcile(
+ writeoff_acc_id, writeoff_journal_id)
diff --git a/account_reconcile_restrict_partner_mismatch/readme/DESCRIPTION.rst b/account_reconcile_restrict_partner_mismatch/readme/DESCRIPTION.rst
new file mode 100644
index 00000000..a546b3fa
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/readme/DESCRIPTION.rst
@@ -0,0 +1,10 @@
+This module restricts reconciliation between journal items when:
+
+ - both items have different partners
+ - one item is with partner and the other without it
+
+This rule applies only for journal items using receivable and payable account type.
+
+As at the moment of installation some journal items could have been reconciled
+using different partners, you can detect them in menu Accounting > Adviser >
+Reconciled items with partner mismatch.
diff --git a/account_reconcile_restrict_partner_mismatch/report/__init__.py b/account_reconcile_restrict_partner_mismatch/report/__init__.py
new file mode 100644
index 00000000..f32b29ee
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/report/__init__.py
@@ -0,0 +1 @@
+from . import report_reconciled_lines
diff --git a/account_reconcile_restrict_partner_mismatch/report/account_move_lines_report.xml b/account_reconcile_restrict_partner_mismatch/report/account_move_lines_report.xml
new file mode 100644
index 00000000..14b7276f
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/report/account_move_lines_report.xml
@@ -0,0 +1,61 @@
+
+
+
+ Reconciled items with partner mismatch
+ account.reconcile.partner.mismatch.report
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ account.reconcile.partner.mismatch.report.form
+ account.reconcile.partner.mismatch.report
+
+
+
+
+
+
+ Reconciled items with partner mismatch
+ account.reconcile.partner.mismatch.report
+ tree,form
+
+
+
+
diff --git a/account_reconcile_restrict_partner_mismatch/report/report_reconciled_lines.py b/account_reconcile_restrict_partner_mismatch/report/report_reconciled_lines.py
new file mode 100644
index 00000000..ac2cfc57
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/report/report_reconciled_lines.py
@@ -0,0 +1,65 @@
+# Copyright 2019 Camptocamp SA
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+
+from odoo import api, fields, models, tools
+
+
+class AccountReconcilePartnerMismatchReport(models.Model):
+ _name = 'account.reconcile.partner.mismatch.report'
+ _auto = False
+
+ partial_reconcile_id = fields.Many2one(
+ 'account.partial.reconcile',
+ string="Partial Reconcile"
+ )
+ full_reconcile_id = fields.Many2one('account.full.reconcile')
+ account_id = fields.Many2one(
+ 'account.account',
+ string="Account"
+ )
+ account_type_id = fields.Many2one(
+ 'account.account.type',
+ string="Account type",
+ )
+ debit_move_id = fields.Many2one('account.move.line', string="Debit move")
+ debit_amount = fields.Float("Debit amount")
+ debit_partner_id = fields.Many2one('res.partner', string="Debit partner")
+ credit_move_id = fields.Many2one('account.move.line', string="Credit move")
+ credit_amount = fields.Float("Credit amount")
+ credit_partner_id = fields.Many2one('res.partner', string="Credit partner")
+
+ @api.model_cr
+ def init(self):
+ """Select lines which violate defined rules"""
+ tools.drop_view_if_exists(self.env.cr, self._table)
+ self._cr.execute(
+ """CREATE OR REPLACE VIEW %s AS (
+ SELECT pr.id id
+ , pr.id partial_reconcile_id
+ , pr.full_reconcile_id
+ , pr.debit_move_id
+ , daml.debit debit_amount
+ , aat.id account_type_id
+ , daml.partner_id debit_partner_id
+ , daml.account_id account_id
+ , pr.credit_move_id
+ , caml.credit credit_amount
+ , caml.partner_id credit_partner_id
+ FROM account_partial_reconcile pr
+ LEFT JOIN account_move_line daml
+ ON daml.id = pr.debit_move_id
+ LEFT JOIN account_move_line caml
+ ON caml.id = pr.credit_move_id
+ LEFT JOIN account_account_type aat
+ ON daml.user_type_id = aat.id
+ WHERE aat.type in ('receivable', 'payable')
+ AND (daml.partner_id <> caml.partner_id
+ OR (daml.partner_id IS NULL
+ AND caml.partner_id IS NOT NULL)
+ OR (caml.partner_id IS NULL
+ AND daml.partner_id IS NOT NULL))
+ )
+ """
+ % self._table
+ )
diff --git a/account_reconcile_restrict_partner_mismatch/security/ir.model.access.csv b/account_reconcile_restrict_partner_mismatch/security/ir.model.access.csv
new file mode 100644
index 00000000..8dea174a
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/security/ir.model.access.csv
@@ -0,0 +1,2 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_account_reconcile_partner_mismatch_report,access_account_reconcile_partner_mismatch_report,model_account_reconcile_partner_mismatch_report,account.group_account_user,1,0,0,0
diff --git a/account_reconcile_restrict_partner_mismatch/static/description/icon.png b/account_reconcile_restrict_partner_mismatch/static/description/icon.png
new file mode 100644
index 00000000..3a0328b5
Binary files /dev/null and b/account_reconcile_restrict_partner_mismatch/static/description/icon.png differ
diff --git a/account_reconcile_restrict_partner_mismatch/static/description/index.html b/account_reconcile_restrict_partner_mismatch/static/description/index.html
new file mode 100644
index 00000000..876a1f6c
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/static/description/index.html
@@ -0,0 +1,419 @@
+
+
+
+
+
+
+Account Set Reconcilable
+
+
+
+
+
Account Set Reconcilable
+
+
+

+
Allows to set as reconcilable a non reconcilable account that already have journal items.
+
Table of contents
+
+
+
+
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 smashing it by providing a detailed and welcomed
+feedback.
+
Do not contact contributors directly about support or help with technical issues.
+
+
+
+
+
+
+
+
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.
+
This module is part of the OCA/account-reconcile project on GitHub.
+
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
+
+
+
+
+
diff --git a/account_reconcile_restrict_partner_mismatch/tests/__init__.py b/account_reconcile_restrict_partner_mismatch/tests/__init__.py
new file mode 100644
index 00000000..5eaab019
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_reconciliation
diff --git a/account_reconcile_restrict_partner_mismatch/tests/test_reconciliation.py b/account_reconcile_restrict_partner_mismatch/tests/test_reconciliation.py
new file mode 100644
index 00000000..a21c6af9
--- /dev/null
+++ b/account_reconcile_restrict_partner_mismatch/tests/test_reconciliation.py
@@ -0,0 +1,104 @@
+# Copyright 2019 Camptocamp SA
+# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
+
+from odoo.addons.account.tests.account_test_classes import AccountingTestCase
+from odoo.exceptions import UserError
+
+
+class TestReconciliation(AccountingTestCase):
+
+ def setUp(self):
+ super().setUp()
+ self.env = self.env(context=dict(
+ self.env.context, tracking_disable=True,
+ test_partner_mismatch=True)
+ )
+
+ self.partner = self.env.ref("base.res_partner_2")
+ self.partner_id = self.partner.id
+ rec_type = self.env['account.account'].search([
+ ('user_type_id', '=',
+ self.env.ref('account.data_account_type_receivable').id)
+ ], limit=1)
+ pay_type = self.env['account.account'].search([
+ ('user_type_id', '=',
+ self.env.ref('account.data_account_type_payable').id)
+ ], limit=1)
+ self.account_rcv = (self.partner.property_account_receivable_id
+ or rec_type)
+ self.account_rsa = self.partner.property_account_payable_id or pay_type
+
+ self.bank_journal = self.env['account.journal']. \
+ create({'name': 'Bank', 'type': 'bank', 'code': 'BNK67'})
+ self.aml = self.init_moves()
+ self.wizard = self.env['account.move.line.reconcile.writeoff']. \
+ with_context(active_ids=[x.id for x in self.aml]).create({
+ 'journal_id': self.bank_journal.id,
+ 'writeoff_acc_id': self.account_rsa.id
+ })
+
+ def create_move(self, name, amount):
+ debit_line_vals = {
+ 'name': name,
+ 'debit': amount > 0 and amount or 0.0,
+ 'credit': amount < 0 and -amount or 0.0,
+ 'account_id': self.account_rcv.id,
+ }
+ credit_line_vals = debit_line_vals.copy()
+ credit_line_vals['debit'] = debit_line_vals['credit']
+ credit_line_vals['credit'] = debit_line_vals['debit']
+ credit_line_vals['account_id'] = self.account_rsa.id
+ vals = {
+ 'journal_id': self.bank_journal.id,
+ 'line_ids': [(0, 0, debit_line_vals), (0, 0, credit_line_vals)]
+ }
+ return self.env['account.move'].create(vals).id
+
+ def init_moves(self):
+ move_list_vals = [
+ ('1', -1.83),
+ ('2', 728.35),
+ ('3', -4.46),
+ ('4', 0.32),
+ ('5', 14.72),
+ ('6', -737.10),
+ ]
+ move_ids = []
+ for name, amount in move_list_vals:
+ move_ids.append(self.create_move(name, amount))
+ aml_recs = self.env['account.move.line'].search([
+ ('move_id', 'in', move_ids),
+ ('account_id', '=', self.account_rcv.id)
+ ])
+ return aml_recs
+
+ def test_reconcile_no_partner(self):
+ self.wizard.trans_rec_reconcile()
+ self.assertTrue(all(self.aml.mapped('reconciled')))
+
+ def test_reconcile_partner_mismatch(self):
+ self.aml[0].partner_id = self.partner.id
+ with self.assertRaises(UserError):
+ self.wizard.trans_rec_reconcile()
+ # all lines with same partner allowed
+ self.aml.write({'partner_id': self.partner.id})
+ self.wizard.trans_rec_reconcile()
+ self.assertTrue(all(self.aml.mapped('reconciled')))
+
+ def test_reconcile_accounts_excluded(self):
+ self.aml[0].partner_id = self.partner.id
+ with self.assertRaises(UserError):
+ self.wizard.trans_rec_reconcile()
+ # reconciliation forbiden only for certain types of accounts
+ account = self.env['account.account'].search([
+ ('user_type_id.type', '=', 'other')
+ ], limit=1)
+ account.reconcile = True
+ self.aml[0].account_id = account.id
+ with self.assertRaises(UserError):
+ self.wizard.trans_rec_reconcile()
+ # reconciliation for different partners allowed
+ # for not forbidden types
+ self.aml.write({'account_id': account.id})
+ self.wizard.trans_rec_reconcile()
+ self.assertTrue(all(self.aml.mapped('reconciled')))