Merge branch 'mig/12.0/account_payment_disperse' into 12.0-test

This commit is contained in:
Jared Kipe
2019-07-21 14:31:16 -07:00
10 changed files with 432 additions and 0 deletions

View File

@@ -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 <https://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 <https://github.com/hibou-io/hibou-odoo-suite/blob/11.0/LICENSE>`_.
Copyright Hibou Corp. 2018

View File

@@ -0,0 +1,2 @@
from . import models
from . import wizard

View File

@@ -0,0 +1,19 @@
{
'name': 'Payment Disperse',
'version': '12.0.1.0.0',
'author': 'Hibou Corp. <hello@hibou.io>',
'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,
}

View File

@@ -0,0 +1 @@
from . import account

View File

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

View File

@@ -0,0 +1 @@
from . import test_payment_multi

View File

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

View File

@@ -0,0 +1 @@
from . import register_payment_wizard

View File

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

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_account_payment_from_invoices_inherited" model="ir.ui.view">
<field name="name">account.register.payments.wizard.inherited</field>
<field name="model">account.register.payments</field>
<field name="inherit_id" ref="account.view_account_payment_from_invoices"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='amount']" position="attributes">
<attribute name="attrs">{'readonly': [('multi', '=', True), ('is_manual_disperse', '!=', True)]}</attribute>
</xpath>
<xpath expr="//field[@name='payment_method_code']" position="after">
<field name="is_manual_disperse"/>
</xpath>
<xpath expr="//group[3]" position="attributes">
<attribute name="attrs">{'invisible': ['|', ('payment_difference', '=', 0.0), ('is_manual_disperse', '=', True)]}</attribute>
</xpath>
<xpath expr="//form/group[1]" position="after">
<group name="invoice_lines" attrs="{'invisible': [('is_manual_disperse', '=', False)]}">
<group>
<button type="object" name="action_fill_residual"
string="Fill with Remaining" class="btn-default"/>
<button type="object" name="action_fill_residual_due"
string="Fill with Due" class="btn-default"/>
</group>
<field name="invoice_line_ids" nolabel="1">
<tree editable="bottom" create="false">
<field name="wizard_id" invisible="1"/>
<field name="partner_id" readonly="1"/>
<field name="invoice_id" readonly="1" force_save="1"/>
<field name="residual" readonly="1" sum="Total Residual"/>
<field name="residual_due" readonly="1" sum="Total Due"/>
<field name="amount" sum="Total Amount"/>
<field name="difference" readonly="1" sum="Total Difference"/>
<field name="writeoff_acc_id"/>
</tree>
</field>
</group>
<group name="invoice_totals" attrs="{'invisible': [('is_manual_disperse', '=', False)]}">
<group>
<field name="writeoff_journal_id"/>
<field name="due_date_cutoff"/>
</group>
<group/>
</group>
</xpath>
</field>
</record>
</odoo>