diff --git a/account_payment_disperse/README.rst b/account_payment_disperse/README.rst
new file mode 100644
index 00000000..c25ff27a
--- /dev/null
+++ b/account_payment_disperse/README.rst
@@ -0,0 +1,38 @@
+**********************************
+Hibou - Manually Disperse Payments
+**********************************
+
+What happens when a vendor pays you and the payment needs to be split
+between multiple invoices? What happens when partial payment needs to
+be applied to some or all of those invoices? We've got you covered.
+
+For more information and add-ons, visit `Hibou.io `_.
+
+
+=============
+Main Features
+=============
+
+* Ability to disperse payment manually.
+* Choose to leave an open balance or write-off the difference.
+* Easily pay only the portion of the balance that is currently due (or due before a specific date).
+* **Remaining Column** that displays the residual balance.
+* **Due Date** cutoff field (defaults to current date).
+* **Due Column** which displays current amount due. This recalculates when the due date cutoff is changed.
+* New buttons to easily fill or pre-populate amount remaining or amount due.
+* Tests to ensure that all the stock payment mechanisms work, and that we make the right reconciliations.
+
+.. image:: https://user-images.githubusercontent.com/15882954/39149575-62a0a1d6-46f4-11e8-8e59-b315cf8f9277.png
+ :alt: 'Register Payment Detail'
+ :width: 988
+ :align: left
+
+
+=======
+License
+=======
+
+Please see `LICENSE `_.
+
+Copyright Hibou Corp. 2018
+
diff --git a/account_payment_disperse/__init__.py b/account_payment_disperse/__init__.py
new file mode 100644
index 00000000..9b429614
--- /dev/null
+++ b/account_payment_disperse/__init__.py
@@ -0,0 +1,2 @@
+from . import models
+from . import wizard
diff --git a/account_payment_disperse/__manifest__.py b/account_payment_disperse/__manifest__.py
new file mode 100644
index 00000000..a66efe60
--- /dev/null
+++ b/account_payment_disperse/__manifest__.py
@@ -0,0 +1,19 @@
+{
+ 'name': 'Payment Disperse',
+ 'version': '12.0.1.0.0',
+ 'author': 'Hibou Corp. ',
+ 'category': 'Accounting',
+ 'summary': 'Pay multiple invoices with one Payment',
+ 'description': """
+Pay multiple invoices with one Payment, and manually disperse the amount per invoice.
+""",
+ 'website': 'https://hibou.io/',
+ 'depends': [
+ 'account',
+ ],
+ 'data': [
+ 'wizard/register_payment_wizard_views.xml',
+ ],
+ 'installable': True,
+ 'auto_install': False,
+}
diff --git a/account_payment_disperse/models/__init__.py b/account_payment_disperse/models/__init__.py
new file mode 100644
index 00000000..33f37402
--- /dev/null
+++ b/account_payment_disperse/models/__init__.py
@@ -0,0 +1 @@
+from . import account
\ No newline at end of file
diff --git a/account_payment_disperse/models/account.py b/account_payment_disperse/models/account.py
new file mode 100644
index 00000000..e39340a5
--- /dev/null
+++ b/account_payment_disperse/models/account.py
@@ -0,0 +1,68 @@
+from odoo import api, fields, models
+
+
+class AccountPayment(models.Model):
+ _inherit = 'account.payment'
+
+ def _create_payment_entry(self, amount):
+ wizard_id = self.env.context.get('payment_wizard_id')
+ if wizard_id:
+ wizard = self.env['account.register.payments'].browse(wizard_id)
+ assert wizard
+ if wizard.is_manual_disperse:
+ payment_amount = sum(wizard.invoice_line_ids.filtered(lambda p: p.partner_id == self.partner_id).mapped('amount'))
+ if amount < 0:
+ payment_amount = -payment_amount
+ return self._create_payment_entry_manual_disperse(payment_amount, wizard)
+
+ return super(AccountPayment, self)._create_payment_entry(amount)
+
+ def _create_payment_entry_manual_disperse(self, amount, wizard):
+ # When registering payments for multiple partners at the same time, without setting
+ # the amount again, then the payment will not match the accounting.
+ self.amount = abs(amount)
+ if hasattr(self, 'check_amount_in_words'):
+ self.check_amount_in_words = self.currency_id.amount_to_text(self.amount)
+
+ aml_obj = self.env['account.move.line'].with_context(check_move_validity=False)
+ debit, credit, amount_currency, currency_id = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(amount, self.currency_id, self.company_id.currency_id)
+
+ move = self.env['account.move'].create(self._get_move_vals())
+
+ inv_lines = []
+ for partial_invoice in wizard.invoice_line_ids.filtered(lambda p: p.amount and p.partner_id == self.partner_id):
+ # Note that for customer payments, the amount will be reversed.
+ inv_amount = partial_invoice.amount if amount > 0 else -partial_invoice.amount
+ i_debit, i_credit, i_amount_currency, i_currency_id = aml_obj.with_context(date=self.payment_date)._compute_amount_fields(inv_amount, self.currency_id, self.company_id.currency_id)
+ counterpart_aml_dict = self._get_shared_move_line_vals(i_debit, i_credit, i_amount_currency, move.id, False)
+ counterpart_aml_dict.update(self._get_counterpart_move_line_vals(partial_invoice.invoice_id))
+ counterpart_aml_dict.update({'currency_id': currency_id})
+ counterpart_aml = aml_obj.create(counterpart_aml_dict)
+ counterpart_aml |= partial_invoice.invoice_id.move_id.line_ids.filtered(
+ lambda r: not r.reconciled and r.account_id.internal_type in ('payable', 'receivable'))
+ inv_lines.append((counterpart_aml, partial_invoice.writeoff_acc_id))
+
+ # Useful for debugging
+ # for aml_ids, writeoff_acc_id in inv_lines:
+ # _logger.warn('pair:' + (' writeoff: ' + str(writeoff_acc_id)) if writeoff_acc_id else '')
+ # for l in aml_ids:
+ # _logger.warn(' ' + str(l) + ' debit: ' + str(l.debit) + ' credit: ' + str(l.credit))
+
+ # Write counterpart lines
+ if not self.currency_id.is_zero(self.amount):
+ if not self.currency_id != self.company_id.currency_id:
+ amount_currency = 0
+ liquidity_aml_dict = self._get_shared_move_line_vals(credit, debit, -amount_currency, move.id, False)
+ liquidity_aml_dict.update(self._get_liquidity_move_line_vals(-amount))
+ other = aml_obj.create(liquidity_aml_dict)
+ #_logger.warn('other line -- debit: ' + str(other.debit) + ' credit: ' + str(other.credit))
+
+ # validate the payment
+ if not self.journal_id.post_at_bank_rec:
+ move.post()
+
+ # reconcile the invoice receivable/payable line(s) with the payment
+ for aml_ids, writeoff_acc_id in inv_lines:
+ aml_ids.reconcile(writeoff_acc_id, wizard.writeoff_journal_id)
+
+ return move
diff --git a/account_payment_disperse/tests/__init__.py b/account_payment_disperse/tests/__init__.py
new file mode 100644
index 00000000..de54650c
--- /dev/null
+++ b/account_payment_disperse/tests/__init__.py
@@ -0,0 +1 @@
+from . import test_payment_multi
diff --git a/account_payment_disperse/tests/test_payment_multi.py b/account_payment_disperse/tests/test_payment_multi.py
new file mode 100644
index 00000000..e07fcabb
--- /dev/null
+++ b/account_payment_disperse/tests/test_payment_multi.py
@@ -0,0 +1,157 @@
+from odoo.addons.account.tests.test_payment import TestPayment
+from odoo.exceptions import ValidationError
+import time
+
+
+class PaymentMultiTest(TestPayment):
+
+ def test_multiple_payments_partial(self):
+ """ Create test to pay several vendor bills/invoices at once """
+ # One payment for inv_1 and inv_2 (same partner)
+ inv_1 = self.create_invoice(amount=100, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ inv_2 = self.create_invoice(amount=500, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+
+ ids = [inv_1.id, inv_2.id]
+ register_payments = self.register_payments_model.with_context(active_ids=ids).create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'journal_id': self.bank_journal_euro.id,
+ 'payment_method_id': self.payment_method_manual_in.id,
+ })
+ register_payments.amount = 399.0
+ register_payments.is_manual_disperse = True
+
+ with self.assertRaises(ValidationError):
+ register_payments.create_payments()
+
+ for line in register_payments.invoice_line_ids:
+ if line.invoice_id == inv_1:
+ line.amount = 99.0
+ if line.invoice_id == inv_2:
+ line.amount = 300.0
+
+ register_payments.create_payments()
+
+ payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
+ self.assertEqual(len(payment_ids), 1, 'Need only one payment.')
+ self.assertEqual(payment_ids.amount, 399.0)
+
+ self.assertEqual(inv_1.residual_signed, 1.0)
+ self.assertEqual(inv_2.residual_signed, 200.0)
+
+ register_payments = self.register_payments_model.with_context(active_ids=ids).create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'journal_id': self.bank_journal_euro.id,
+ 'payment_method_id': self.payment_method_manual_in.id,
+ })
+ register_payments.amount = 200.0
+ register_payments.is_manual_disperse = True
+
+ for line in register_payments.invoice_line_ids:
+ if line.invoice_id == inv_2:
+ line.amount = 200.0
+
+ register_payments.create_payments()
+ self.assertEqual(inv_1.residual_signed, 1.0)
+ self.assertEqual(inv_2.residual_signed, 0.0)
+
+ def test_multiple_payments_write_off(self):
+ inv_1 = self.create_invoice(amount=100, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ inv_2 = self.create_invoice(amount=500, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+
+ ids = [inv_1.id, inv_2.id]
+ register_payments = self.register_payments_model.with_context(active_ids=ids).create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'journal_id': self.bank_journal_euro.id,
+ 'payment_method_id': self.payment_method_manual_in.id,
+ })
+ register_payments.amount = 400.0
+ register_payments.is_manual_disperse = True
+ register_payments.writeoff_journal_id = inv_1.journal_id
+
+ with self.assertRaises(ValidationError):
+ register_payments.create_payments()
+
+ for line in register_payments.invoice_line_ids:
+ if line.invoice_id == inv_1:
+ line.amount = 100.0
+ if line.invoice_id == inv_2:
+ line.amount = 300.0
+ line.writeoff_acc_id = self.account_revenue
+
+ register_payments.create_payments()
+
+ payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
+ self.assertEqual(len(payment_ids), 1, 'Need only one payment.')
+ self.assertEqual(payment_ids.amount, 400.0)
+
+ self.assertEqual(inv_1.residual_signed, 0.0)
+ self.assertEqual(inv_2.residual_signed, 0.0)
+
+ def test_multiple_payments_partial_multi(self):
+ """ Create test to pay several vendor bills/invoices at once """
+ # One payment for inv_1 and inv_2 (same partner)
+ inv_1 = self.create_invoice(amount=100, currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ inv_2 = self.create_invoice(amount=500, currency_id=self.currency_eur_id, partner=self.partner_china_exp.id)
+
+ ids = [inv_1.id, inv_2.id]
+ register_payments = self.register_payments_model.with_context(active_ids=ids).create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'journal_id': self.bank_journal_euro.id,
+ 'payment_method_id': self.payment_method_manual_in.id,
+ })
+ register_payments.amount = 400.0
+ register_payments.is_manual_disperse = True
+
+ for line in register_payments.invoice_line_ids:
+ if line.invoice_id == inv_1:
+ line.amount = 100.0
+ if line.invoice_id == inv_2:
+ line.amount = 300.0
+
+ register_payments.create_payments()
+
+ payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
+ self.assertEqual(len(payment_ids), 2, 'Need two payments.')
+ # Useful for logging amounts of payments and their accounting
+ # for pay in payment_ids:
+ # _logger.warn(str(pay) + ' amount: ' + str(pay.amount))
+ # for line in pay.move_line_ids:
+ # _logger.warn(' ' +
+ # str(line) + ' name: ' + str(line.name) + ' :: credit: ' + str(line.credit) + ' debit: ' +
+ # str(line.debit))
+ self.assertEqual(sum(payment_ids.mapped('amount')), 400.0)
+
+ self.assertEqual(inv_1.residual_signed, 0.0)
+ self.assertEqual(inv_2.residual_signed, 200.0)
+
+ def test_vendor_multiple_payments_write_off(self):
+ inv_1 = self.create_invoice(amount=100, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ inv_2 = self.create_invoice(amount=500, type='in_invoice', currency_id=self.currency_eur_id, partner=self.partner_agrolait.id)
+ ids = [inv_1.id, inv_2.id]
+ register_payments = self.register_payments_model.with_context(active_ids=ids).create({
+ 'payment_date': time.strftime('%Y') + '-07-15',
+ 'journal_id': self.bank_journal_euro.id,
+ 'payment_method_id': self.payment_method_manual_out.id,
+ })
+ register_payments.amount = 400.0
+ register_payments.is_manual_disperse = True
+ register_payments.writeoff_journal_id = inv_1.journal_id
+
+ with self.assertRaises(ValidationError):
+ register_payments.create_payments()
+
+ for line in register_payments.invoice_line_ids:
+ if line.invoice_id == inv_1:
+ line.amount = 100.0
+ if line.invoice_id == inv_2:
+ line.amount = 300.0
+ line.writeoff_acc_id = self.account_revenue
+
+ register_payments.create_payments()
+
+ payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
+ self.assertEqual(len(payment_ids), 1, 'Need only one payment.')
+ self.assertEqual(payment_ids.amount, 400.0)
+
+ self.assertEqual(inv_1.residual_signed, 0.0)
+ self.assertEqual(inv_2.residual_signed, 0.0)
diff --git a/account_payment_disperse/wizard/__init__.py b/account_payment_disperse/wizard/__init__.py
new file mode 100644
index 00000000..8b631b8b
--- /dev/null
+++ b/account_payment_disperse/wizard/__init__.py
@@ -0,0 +1 @@
+from . import register_payment_wizard
diff --git a/account_payment_disperse/wizard/register_payment_wizard.py b/account_payment_disperse/wizard/register_payment_wizard.py
new file mode 100644
index 00000000..3153045a
--- /dev/null
+++ b/account_payment_disperse/wizard/register_payment_wizard.py
@@ -0,0 +1,97 @@
+from odoo import api, fields, models
+from odoo.exceptions import ValidationError
+
+
+class AccountRegisterPayments(models.TransientModel):
+ _inherit = 'account.register.payments'
+
+ is_manual_disperse = fields.Boolean(string='Disperse Manually')
+ invoice_line_ids = fields.One2many('account.register.payments.invoice.line', 'wizard_id', string='Invoices')
+ writeoff_journal_id = fields.Many2one('account.journal', string='Write-off Journal')
+ due_date_cutoff = fields.Date(string='Due Date Cutoff', default=fields.Date.today)
+
+ @api.model
+ def default_get(self, fields):
+ rec = super(AccountRegisterPayments, self).default_get(fields)
+ if 'invoice_ids' in rec:
+ invoice_ids = rec['invoice_ids'][0][2]
+ rec['invoice_line_ids'] = [(0, 0, {'invoice_id': i, 'amount': 0.0}) for i in invoice_ids]
+ return rec
+
+ @api.multi
+ def create_payments(self):
+ for payment in self.filtered(lambda p: p.is_manual_disperse):
+ line_amount = sum(payment.invoice_line_ids.mapped('amount'))
+ if abs(line_amount - payment.amount) >= 0.01:
+ raise ValidationError('Cannot pay for %0.2f worth of invoices with %0.2f total.' %
+ (line_amount, payment.amount))
+ if not payment.writeoff_journal_id and payment.invoice_line_ids.filtered(lambda l: l.writeoff_acc_id):
+ raise ValidationError('Cannot write off without a write off journal.')
+ new_self = self.with_context(payment_wizard_id=self.id)
+ return super(AccountRegisterPayments, new_self).create_payments()
+
+ @api.multi
+ def action_fill_residual(self):
+ for payment in self:
+ for line in payment.invoice_line_ids:
+ line.amount = line.residual
+ action = self.env.ref('account.action_account_payment_from_invoices').read()[0]
+ action['res_id'] = payment.id
+ return action
+
+ @api.multi
+ def action_fill_residual_due(self):
+ for payment in self:
+ for line in payment.invoice_line_ids:
+ line.amount = line.residual_due
+ action = self.env.ref('account.action_account_payment_from_invoices').read()[0]
+ action['res_id'] = payment.id
+ return action
+
+
+class AccountRegisterPaymentsInvoiceLine(models.TransientModel):
+ _name = 'account.register.payments.invoice.line'
+
+ wizard_id = fields.Many2one('account.register.payments')
+ invoice_id = fields.Many2one('account.invoice', string='Invoice', required=True)
+ partner_id = fields.Many2one('res.partner', string='Partner', compute='_compute_balances', compute_sudo=True)
+ residual = fields.Float(string='Remaining', compute='_compute_balances', compute_sudo=True)
+ residual_due = fields.Float(string='Due', compute='_compute_balances', compute_sudo=True)
+ difference = fields.Float(string='Difference', default=0.0)
+ amount = fields.Float(string='Amount')
+ writeoff_acc_id = fields.Many2one('account.account', string='Write-off Account')
+
+ @api.depends('invoice_id', 'wizard_id.due_date_cutoff', 'invoice_id.partner_id')
+ def _compute_balances(self):
+ for line in self:
+ # Bug in the ORM 12.0? The invoice is set, but there is no residual
+ # on anything other than the first invoice/line processed.
+ invoice = line.invoice_id.browse(line.invoice_id.id)
+ residual = invoice.residual
+
+ cutoff_date = line.wizard_id.due_date_cutoff
+ total_amount = 0.0
+ total_reconciled = 0.0
+ for move_line in invoice.move_id.line_ids.filtered(lambda r: (
+ not r.reconciled
+ and r.account_id.internal_type in ('payable', 'receivable')
+ and r.date_maturity <= cutoff_date
+ )):
+ amount = abs(move_line.debit - move_line.credit)
+ total_amount += amount
+ for partial_line in move_line.matched_debit_ids:
+ total_reconciled += partial_line.amount
+ for partial_line in move_line.matched_credit_ids:
+ total_reconciled += partial_line.amount
+ values = {
+ 'residual': residual,
+ 'residual_due': total_amount - total_reconciled,
+ 'difference': residual - (line.amount or 0.0),
+ 'partner_id': invoice.partner_id.id,
+ }
+ line.update(values)
+
+ @api.onchange('amount')
+ def _onchange_amount(self):
+ for line in self:
+ line.difference = line.residual - line.amount
diff --git a/account_payment_disperse/wizard/register_payment_wizard_views.xml b/account_payment_disperse/wizard/register_payment_wizard_views.xml
new file mode 100644
index 00000000..3e428048
--- /dev/null
+++ b/account_payment_disperse/wizard/register_payment_wizard_views.xml
@@ -0,0 +1,48 @@
+
+
+
+ account.register.payments.wizard.inherited
+ account.register.payments
+
+
+
+ {'readonly': [('multi', '=', True), ('is_manual_disperse', '!=', True)]}
+
+
+
+
+
+ {'invisible': ['|', ('payment_difference', '=', 0.0), ('is_manual_disperse', '=', True)]}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file