Merge branch 'mig/13.0/account_payment_disperse' into '13.0'

mig/13.0/account_payment_disperse into 13.0

See merge request hibou-io/hibou-odoo/suite!897
This commit is contained in:
Jared Kipe
2021-05-06 17:11:49 +00:00
11 changed files with 855 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
**********************************
Hibou - Manually Disperse Payments
**********************************
What happens when a customer 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 Date Behavior** field to pay the 'next due' amount (e.g. to take a vendor terms discount)
* **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. 2021

View File

@@ -0,0 +1,4 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import models
from . import wizard

View File

@@ -0,0 +1,22 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
{
'name': 'Payment Disperse',
'version': '13.0.1.0.0',
'author': 'Hibou Corp.',
'license': 'OPL-1',
'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,4 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import account
from . import account_patch

View File

@@ -0,0 +1,19 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import models, _
class AccountPayment(models.Model):
_inherit = 'account.payment'
def _values_for_payment_invoice_line(self, line, currency_id):
sign = 1.0 if self.payment_type == 'outbound' else -1.0
i_write_off_amount = -line.difference if line.difference and line.close_balance else 0.0
i_amount_currency = (line.amount + i_write_off_amount) if currency_id else 0.0
i_amount = sign * (line.amount + i_write_off_amount)
return {
'amount_currency': i_amount_currency,
'currency_id': currency_id,
'debit': i_amount if i_amount > 0.0 else 0.0,
'credit': -i_amount if i_amount < 0.0 else 0.0,
}

View File

@@ -0,0 +1,352 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo import api, fields, models, _
from odoo.exceptions import UserError, ValidationError
from odoo.tools import float_is_zero
"""
Patched because other modules may extend original functions.
"""
from odoo.addons.account.models import account_payment
def post(self):
""" Create the journal items for the payment and update the payment's state to 'posted'.
A journal entry is created containing an item in the source liquidity account (selected journal's default_debit or default_credit)
and another in the destination reconcilable account (see _compute_destination_account_id).
If invoice_ids is not empty, there will be one reconcilable move line per invoice to reconcile with.
If the payment is a transfer, a second journal entry is created in the destination journal to receive money from the transfer account.
"""
"""
Manual Payment Disperse functionality:
During reconciliation, reconcile specific lines to specific invoices so that the invoices
are paid down in the desired way.
"""
register_wizard = None
if self._context.get('payment_wizard_id'):
register_wizard = self.env['account.payment.register'].browse(self._context.get('payment_wizard_id')).exists()
is_manual_disperse = bool(register_wizard and register_wizard.is_manual_disperse)
AccountMove = self.env['account.move'].with_context(default_type='entry')
for rec in self:
if rec.state != 'draft':
raise UserError(_("Only a draft payment can be posted."))
if any(inv.state != 'posted' for inv in rec.invoice_ids):
raise ValidationError(_("The payment cannot be processed because the invoice is not open!"))
# keep the name in case of a payment reset to draft
if not rec.name:
# Use the right sequence to set the name
if rec.payment_type == 'transfer':
sequence_code = 'account.payment.transfer'
else:
if rec.partner_type == 'customer':
if rec.payment_type == 'inbound':
sequence_code = 'account.payment.customer.invoice'
if rec.payment_type == 'outbound':
sequence_code = 'account.payment.customer.refund'
if rec.partner_type == 'supplier':
if rec.payment_type == 'inbound':
sequence_code = 'account.payment.supplier.refund'
if rec.payment_type == 'outbound':
sequence_code = 'account.payment.supplier.invoice'
rec.name = self.env['ir.sequence'].next_by_code(sequence_code, sequence_date=rec.payment_date)
if not rec.name and rec.payment_type != 'transfer':
raise UserError(_("You have to define a sequence for %s in your company.") % (sequence_code,))
moves = AccountMove.create(rec._prepare_payment_moves())
moves.filtered(lambda move: move.journal_id.post_at != 'bank_rec').post()
# Update the state / move before performing any reconciliation.
move_name = self._get_move_name_transfer_separator().join(moves.mapped('name'))
rec.write({'state': 'posted', 'move_name': move_name})
if rec.payment_type in ('inbound', 'outbound'):
# ==== 'inbound' / 'outbound' ====
if is_manual_disperse:
move = moves[0]
digits_rounding_precision = move.company_id.currency_id.rounding
# pick out lines...
for i in register_wizard.payment_invoice_ids:
i_values = rec._values_for_payment_invoice_line(i, None)
if not any((i_values['debit'], i_values['credit'])):
continue
move_line = move.line_ids.filtered(lambda l: not l.reconciled and l.account_id == rec.destination_account_id and l.account_id != l.payment_id.writeoff_account_id and
float_is_zero(l.debit - i_values['debit'], precision_rounding=digits_rounding_precision) and
float_is_zero(l.credit - i_values['credit'], precision_rounding=digits_rounding_precision))
i_lines = i.invoice_id.line_ids.filtered(lambda l: not l.reconciled and l.account_id == rec.destination_account_id)
if move_line and i_lines:
(move_line + i_lines).reconcile()
else:
if rec.invoice_ids:
(moves[0] + rec.invoice_ids).line_ids \
.filtered(lambda line: not line.reconciled and line.account_id == rec.destination_account_id and not (line.account_id == line.payment_id.writeoff_account_id and line.name == line.payment_id.writeoff_label))\
.reconcile()
elif rec.payment_type == 'transfer':
# ==== 'transfer' ====
moves.mapped('line_ids')\
.filtered(lambda line: line.account_id == rec.company_id.transfer_account_id)\
.reconcile()
return True
def _prepare_payment_moves(self):
''' Prepare the creation of journal entries (account.move) by creating a list of python dictionary to be passed
to the 'create' method.
Example 1: outbound with write-off:
Account | Debit | Credit
---------------------------------------------------------
BANK | 900.0 |
RECEIVABLE | | 1000.0
WRITE-OFF ACCOUNT | 100.0 |
Example 2: internal transfer from BANK to CASH:
Account | Debit | Credit
---------------------------------------------------------
BANK | | 1000.0
TRANSFER | 1000.0 |
CASH | 1000.0 |
TRANSFER | | 1000.0
:return: A list of Python dictionary to be passed to env['account.move'].create.
'''
"""
Manual Payment Disperse functionality:
During journal entry creation, create a single line per invoice for the amount
to be paid on that invoice.
However, all write-offs are accumulated on a single line.
"""
register_wizard = None
if self._context.get('payment_wizard_id'):
register_wizard = self.env['account.payment.register'].browse(
self._context.get('payment_wizard_id')).exists()
is_manual_disperse = bool(register_wizard and register_wizard.is_manual_disperse)
all_move_vals = []
for payment in self:
company_currency = payment.company_id.currency_id
move_names = payment.move_name.split(payment._get_move_name_transfer_separator()) if payment.move_name else None
# Compute amounts.
write_off_amount = payment.payment_difference_handling == 'reconcile' and -payment.payment_difference or 0.0
# Manual Disperse
if is_manual_disperse:
write_off_amount = sum(register_wizard.payment_invoice_ids.filtered(
lambda l: l.close_balance and l.difference and l.invoice_id in payment.invoice_ids).mapped('difference'))
if payment.payment_type == 'outbound':
write_off_amount *= -1.0
if payment.payment_type in ('outbound', 'transfer'):
counterpart_amount = payment.amount
liquidity_line_account = payment.journal_id.default_debit_account_id
else:
counterpart_amount = -payment.amount
liquidity_line_account = payment.journal_id.default_credit_account_id
# Manage currency.
if payment.currency_id == company_currency:
# Single-currency.
balance = counterpart_amount
write_off_balance = write_off_amount
counterpart_amount = write_off_amount = 0.0
currency_id = False
else:
# Multi-currencies.
balance = payment.currency_id._convert(counterpart_amount, company_currency, payment.company_id, payment.payment_date)
write_off_balance = payment.currency_id._convert(write_off_amount, company_currency, payment.company_id, payment.payment_date)
currency_id = payment.currency_id.id
# Manage custom currency on journal for liquidity line.
if payment.journal_id.currency_id and payment.currency_id != payment.journal_id.currency_id:
# Custom currency on journal.
if payment.journal_id.currency_id == company_currency:
# Single-currency
liquidity_line_currency_id = False
else:
liquidity_line_currency_id = payment.journal_id.currency_id.id
liquidity_amount = company_currency._convert(
balance, payment.journal_id.currency_id, payment.company_id, payment.payment_date)
else:
# Use the payment currency.
liquidity_line_currency_id = currency_id
liquidity_amount = counterpart_amount
# Compute 'name' to be used in receivable/payable line.
rec_pay_line_name = ''
if payment.payment_type == 'transfer':
rec_pay_line_name = payment.name
else:
if payment.partner_type == 'customer':
if payment.payment_type == 'inbound':
rec_pay_line_name += _("Customer Payment")
elif payment.payment_type == 'outbound':
rec_pay_line_name += _("Customer Credit Note")
elif payment.partner_type == 'supplier':
if payment.payment_type == 'inbound':
rec_pay_line_name += _("Vendor Credit Note")
elif payment.payment_type == 'outbound':
rec_pay_line_name += _("Vendor Payment")
if payment.invoice_ids:
rec_pay_line_name += ': %s' % ', '.join(payment.invoice_ids.mapped('name'))
# Compute 'name' to be used in liquidity line.
if payment.payment_type == 'transfer':
liquidity_line_name = _('Transfer to %s') % payment.destination_journal_id.name
else:
liquidity_line_name = payment.name
# ==== 'inbound' / 'outbound' ====
move_vals = {
'date': payment.payment_date,
'ref': payment.communication,
'journal_id': payment.journal_id.id,
'currency_id': payment.journal_id.currency_id.id or payment.company_id.currency_id.id,
'partner_id': payment.partner_id.id,
'line_ids': [
# Receivable / Payable / Transfer line.
# (0, 0, {
# 'name': rec_pay_line_name,
# 'amount_currency': counterpart_amount + write_off_amount if currency_id else 0.0,
# 'currency_id': currency_id,
# 'debit': balance + write_off_balance > 0.0 and balance + write_off_balance or 0.0,
# 'credit': balance + write_off_balance < 0.0 and -balance - write_off_balance or 0.0,
# 'date_maturity': payment.payment_date,
# 'partner_id': payment.partner_id.commercial_partner_id.id,
# 'account_id': payment.destination_account_id.id,
# 'payment_id': payment.id,
# }),
# Liquidity line.
(0, 0, {
'name': liquidity_line_name,
'amount_currency': -liquidity_amount if liquidity_line_currency_id else 0.0,
'currency_id': liquidity_line_currency_id,
'debit': balance < 0.0 and -balance or 0.0,
'credit': balance > 0.0 and balance or 0.0,
'date_maturity': payment.payment_date,
'partner_id': payment.partner_id.commercial_partner_id.id,
'account_id': liquidity_line_account.id,
'payment_id': payment.id,
}),
],
}
if is_manual_disperse:
for i in register_wizard.payment_invoice_ids.filtered(lambda l: l.invoice_id in payment.invoice_ids):
i_rec_pay_line_name = rec_pay_line_name
i_values = payment._values_for_payment_invoice_line(i, currency_id)
if not any((i_values['debit'], i_values['credit'])):
# do not make useless lines
continue
move_vals['line_ids'].insert(0, (0, 0, {
'name': i_rec_pay_line_name,
# 'amount_currency': counterpart_amount + write_off_amount if currency_id else 0.0,
# 'amount_currency': i_amount_currency,
# 'currency_id': currency_id,
# # 'debit': balance + write_off_balance > 0.0 and balance + write_off_balance or 0.0,
# 'debit': i_amount if i_amount > 0.0 else 0.0,
# # 'credit': balance + write_off_balance < 0.0 and -balance - write_off_balance or 0.0,
# 'credit': -i_amount if i_amount < 0.0 else 0.0,
'date_maturity': payment.payment_date,
'partner_id': payment.partner_id.commercial_partner_id.id,
'account_id': payment.destination_account_id.id,
'payment_id': payment.id,
**i_values,
}))
else:
# insert because existing tests expect them in a set order.
move_vals['line_ids'].insert(0, (0, 0, {
'name': rec_pay_line_name,
'amount_currency': counterpart_amount + write_off_amount if currency_id else 0.0,
'currency_id': currency_id,
'debit': balance + write_off_balance > 0.0 and balance + write_off_balance or 0.0,
'credit': balance + write_off_balance < 0.0 and -balance - write_off_balance or 0.0,
'date_maturity': payment.payment_date,
'partner_id': payment.partner_id.commercial_partner_id.id,
'account_id': payment.destination_account_id.id,
'payment_id': payment.id,
}))
if write_off_balance:
# Write-off line.
move_vals['line_ids'].append((0, 0, {
'name': payment.writeoff_label,
'amount_currency': -write_off_amount,
'currency_id': currency_id,
'debit': write_off_balance < 0.0 and -write_off_balance or 0.0,
'credit': write_off_balance > 0.0 and write_off_balance or 0.0,
'date_maturity': payment.payment_date,
'partner_id': payment.partner_id.commercial_partner_id.id,
'account_id': payment.writeoff_account_id.id,
'payment_id': payment.id,
}))
if move_names:
move_vals['name'] = move_names[0]
all_move_vals.append(move_vals)
# ==== 'transfer' ====
if payment.payment_type == 'transfer':
journal = payment.destination_journal_id
# Manage custom currency on journal for liquidity line.
if journal.currency_id and payment.currency_id != journal.currency_id:
# Custom currency on journal.
liquidity_line_currency_id = journal.currency_id.id
transfer_amount = company_currency._convert(balance, journal.currency_id, payment.company_id, payment.payment_date)
else:
# Use the payment currency.
liquidity_line_currency_id = currency_id
transfer_amount = counterpart_amount
transfer_move_vals = {
'date': payment.payment_date,
'ref': payment.communication,
'partner_id': payment.partner_id.id,
'journal_id': payment.destination_journal_id.id,
'line_ids': [
# Transfer debit line.
(0, 0, {
'name': payment.name,
'amount_currency': -counterpart_amount if currency_id else 0.0,
'currency_id': currency_id,
'debit': balance < 0.0 and -balance or 0.0,
'credit': balance > 0.0 and balance or 0.0,
'date_maturity': payment.payment_date,
'partner_id': payment.partner_id.commercial_partner_id.id,
'account_id': payment.company_id.transfer_account_id.id,
'payment_id': payment.id,
}),
# Liquidity credit line.
(0, 0, {
'name': _('Transfer from %s') % payment.journal_id.name,
'amount_currency': transfer_amount if liquidity_line_currency_id else 0.0,
'currency_id': liquidity_line_currency_id,
'debit': balance > 0.0 and balance or 0.0,
'credit': balance < 0.0 and -balance or 0.0,
'date_maturity': payment.payment_date,
'partner_id': payment.partner_id.commercial_partner_id.id,
'account_id': payment.destination_journal_id.default_credit_account_id.id,
'payment_id': payment.id,
}),
],
}
if move_names and len(move_names) == 2:
transfer_move_vals['name'] = move_names[1]
all_move_vals.append(transfer_move_vals)
return all_move_vals
account_payment.account_payment.post = post
account_payment.account_payment._prepare_payment_moves = _prepare_payment_moves

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import test_payment_multi

View File

@@ -0,0 +1,163 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from odoo.addons.account.tests.test_payment import TestPayment
from odoo.tests.common import Form
from odoo.tests import tagged
from odoo.exceptions import ValidationError
import time
# Fun fact... if you install enterprise accounting, you'll get errors
# due to required fields being missing...
# The classic fix would be the following @tagged, but for some reason this makes
# odoo.tests.common.Form suddenly not calculate the amount on the register payment wizard
# @tagged('post_install', '-at_install')
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]
payment_register = Form(self.register_payments_model.with_context(active_ids=ids))
payment_register.journal_id = self.bank_journal_euro
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.is_manual_disperse = True
with payment_register.payment_invoice_ids.edit(0) as f:
f.amount = 99.0
f.save()
with payment_register.payment_invoice_ids.edit(1) as f:
f.amount = 300.0
f.save()
self.assertEqual(payment_register.amount, 399.0, 'Amount isn\'t the amount from lines')
# Persist object
payment_register = payment_register.save()
payment_register.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.amount_residual_signed, 1.0)
self.assertEqual(inv_2.amount_residual_signed, 200.0)
payment_register = Form(self.register_payments_model.with_context(active_ids=ids))
payment_register.journal_id = self.bank_journal_euro
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.is_manual_disperse = True
with payment_register.payment_invoice_ids.edit(0) as f:
f.amount = 0.0
f.save()
with payment_register.payment_invoice_ids.edit(1) as f:
f.amount = 200.0
f.save()
payment_register = payment_register.save()
payment_register.create_payments()
self.assertEqual(inv_1.amount_residual_signed, 1.0)
self.assertEqual(inv_2.amount_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]
payment_register = Form(self.register_payments_model.with_context(active_ids=ids))
payment_register.journal_id = self.bank_journal_euro
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.is_manual_disperse = True
payment_register.writeoff_account_id = self.transfer_account
with payment_register.payment_invoice_ids.edit(0) as f:
f.amount = 100.0
f.close_balance = True
f.save()
with payment_register.payment_invoice_ids.edit(1) as f:
f.amount = 300.0
f.close_balance = True
f.save()
self.assertEqual(payment_register.amount, 400.0, 'Amount isn\'t the amount from lines')
payment_register = payment_register.save()
self.assertEqual(sum(payment_register.mapped('payment_invoice_ids.difference')), -200.0)
payment_register.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.amount_residual_signed, 0.0)
self.assertEqual(inv_2.amount_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 (different partners)
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]
payment_register = Form(self.register_payments_model.with_context(active_ids=ids))
payment_register.journal_id = self.bank_journal_euro
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.is_manual_disperse = True
with payment_register.payment_invoice_ids.edit(0) as f:
f.amount = 100.0
f.save()
with payment_register.payment_invoice_ids.edit(1) as f:
f.amount = 300.0
f.save()
self.assertEqual(payment_register.amount, 400.0, 'Amount isn\'t the amount from lines')
payment_register = payment_register.save()
payment_register.create_payments()
payment_ids = self.payment_model.search([('invoice_ids', 'in', ids)], order="id desc")
self.assertEqual(len(payment_ids), 2, 'Need two payments.')
self.assertEqual(sum(payment_ids.mapped('amount')), 400.0)
self.assertEqual(inv_1.amount_residual_signed, 0.0)
self.assertEqual(inv_2.amount_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]
payment_register = Form(self.register_payments_model.with_context(active_ids=ids))
payment_register.journal_id = self.bank_journal_euro
payment_register.payment_date = time.strftime('%Y') + '-07-15'
payment_register.is_manual_disperse = True
with payment_register.payment_invoice_ids.edit(0) as f:
f.amount = 100.0
f.save()
with payment_register.payment_invoice_ids.edit(1) as f:
f.amount = 300.0
f.save()
self.assertEqual(payment_register.amount, 400.0)
# Cannot have close balance in form because it becomes required
payment_register = payment_register.save()
payment_register.action_toggle_close_balance()
with self.assertRaises(ValidationError):
payment_register.create_payments()
# Need the writeoff account
payment_register.writeoff_account_id = self.transfer_account
payment_register.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.amount_residual_signed, 0.0)
self.assertEqual(inv_2.amount_residual_signed, 0.0)

View File

@@ -0,0 +1,3 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from . import register_payment_wizard

View File

@@ -0,0 +1,200 @@
# Part of Hibou Suite Professional. See LICENSE_PROFESSIONAL file for full copyright and licensing details.
from datetime import datetime
from odoo import api, fields, models, _
from odoo.exceptions import ValidationError
from odoo.addons.account.models.account_payment import MAP_INVOICE_TYPE_PARTNER_TYPE
class AccountRegisterPayments(models.TransientModel):
_inherit = 'account.payment.register'
is_manual_disperse = fields.Boolean(string='Disperse Manually')
payment_invoice_ids = fields.One2many('account.payment.register.payment_invoice', 'wizard_id', string='Invoices')
writeoff_account_id = fields.Many2one('account.account', string="Difference Account", domain="[('deprecated', '=', False)]", copy=False)
requires_writeoff_account = fields.Boolean(compute='_compute_requires_writeoff_account')
amount = fields.Float(string='Amount', compute='_compute_amount')
due_date_cutoff = fields.Date(string='Due Date Cutoff', default=fields.Date.today)
due_date_behavior = fields.Selection([
('due', 'Due'),
('next_due', 'Next Due'),
], string='Due Date Behavior', default='due')
@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['payment_invoice_ids'] = [(0, 0, {'invoice_id': i}) for i in invoice_ids]
return rec
def create_payments(self):
for payment in self.filtered(lambda p: not p.amount and p.invoice_ids):
payment._compute_amount()
for payment in self.filtered(lambda p: p.is_manual_disperse):
line_amount = sum(payment.payment_invoice_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_account_id and payment.payment_invoice_ids.filtered(lambda l: l.close_balance and l.difference):
raise ValidationError('Closing balance will require a difference account.')
new_self = self.with_context(payment_wizard_id=self.id)
return super(AccountRegisterPayments, new_self).create_payments()
@api.onchange('is_manual_disperse')
def _ensure_is_manual_disperse(self):
for payment in self:
payment.group_payment = payment.is_manual_disperse
@api.depends('payment_invoice_ids.amount', 'invoice_ids', 'journal_id', 'payment_date', 'is_manual_disperse')
def _compute_amount(self):
for payment in self.filtered(lambda p: p.is_manual_disperse):
payment.amount = sum(payment.mapped('payment_invoice_ids.amount') or [0.0])
for payment in self.filtered(lambda p: not p.is_manual_disperse):
if payment.invoice_ids:
payment.amount = self.env['account.payment']._compute_payment_amount(
payment.invoice_ids,
payment.invoice_ids[0].currency_id,
payment.journal_id,
payment.payment_date)
else:
payment.amount = 0.0
@api.depends('payment_invoice_ids.difference', 'payment_invoice_ids.close_balance')
def _compute_requires_writeoff_account(self):
for payment in self:
if payment.is_manual_disperse:
payment.requires_writeoff_account = bool(payment.payment_invoice_ids.filtered(
lambda l: l.difference and l.close_balance))
else:
payment.requires_writeoff_account = False
def _prepare_payment_vals(self, invoices):
'''Create the payment values.
:param invoices: The invoices/bills to pay. In case of multiple
documents, they need to be grouped by partner, bank, journal and
currency.
:return: The payment values as a dictionary.
'''
if self.is_manual_disperse:
# this will already be positive for both invoice types unlink the below amount (abs)
amount = sum(self.payment_invoice_ids.filtered(lambda l: l.invoice_id in invoices).mapped('amount'))
sign = 1.0
if invoices:
invoice = invoices[0]
sign = 1.0 if invoice.type in ('out_invoice', 'out_refund') else -1.0
else:
amount = self.env['account.payment']._compute_payment_amount(invoices, invoices[0].currency_id,
self.journal_id, self.payment_date)
sign = 1.0 if amount > 0.0 else -1.0
values = {
'journal_id': self.journal_id.id,
'payment_method_id': self.payment_method_id.id,
'payment_date': self.payment_date,
'communication': self._prepare_communication(invoices),
'invoice_ids': [(6, 0, invoices.ids)],
'payment_type': 'inbound' if sign > 0 else 'outbound',
'amount': abs(amount),
'currency_id': invoices[0].currency_id.id,
'partner_id': invoices[0].commercial_partner_id.id,
'partner_type': MAP_INVOICE_TYPE_PARTNER_TYPE[invoices[0].type],
'partner_bank_account_id': invoices[0].invoice_partner_bank_id.id,
'writeoff_account_id': self.writeoff_account_id.id,
}
return values
def action_fill_residual(self):
for payment in self:
for line in payment.payment_invoice_ids:
line.amount = line.residual
return payment._reopen_action()
def action_fill_residual_due(self):
for payment in self:
for line in payment.payment_invoice_ids:
line.amount = line.residual_due
return payment._reopen_action()
def action_toggle_close_balance(self):
for payment in self:
for line in payment.payment_invoice_ids:
line.close_balance = not line.close_balance
return payment._reopen_action()
def _reopen_action(self):
return {
'name': _('Register Payment'),
'res_model': 'account.payment.register',
'view_mode': 'form',
'view_id': self.env.ref('account.view_account_payment_form_multi').id,
'context': self.env.context,
'target': 'new',
'res_id': self.id,
'type': 'ir.actions.act_window',
}
class AccountRegisterPaymentsInvoiceLine(models.TransientModel):
_name = 'account.payment.register.payment_invoice'
wizard_id = fields.Many2one('account.payment.register')
invoice_id = fields.Many2one('account.move', string='Invoice', required=True)
partner_id = fields.Many2one('res.partner', string='Partner', compute='_compute_invoice_balances', compute_sudo=True)
residual = fields.Float(string='Remaining', compute='_compute_invoice_balances', compute_sudo=True)
residual_due = fields.Float(string='Due', compute='_compute_invoice_balances', compute_sudo=True)
difference = fields.Float(string='Difference', compute='_compute_difference')
amount = fields.Float(string='Amount')
close_balance = fields.Boolean(string='Close Balance', help='Write off remaining balance.')
@api.depends('invoice_id', 'wizard_id.due_date_cutoff', 'wizard_id.due_date_behavior', 'invoice_id.partner_id')
def _compute_invoice_balances(self):
dummy_date = datetime(1980, 1, 1)
for line in self:
invoice = line.invoice_id.browse(line.invoice_id.id)
sign = 1.0 if invoice.type in ('out_invoice', 'out_refund') else -1.0
residual = sign * invoice.amount_residual_signed
cutoff_date = line.wizard_id.due_date_cutoff
due_behavior = line.wizard_id.due_date_behavior
total_amount = 0.0
total_reconciled = 0.0
# TODO partial reconcile will need sign check
if due_behavior == 'due':
for move_line in invoice.line_ids.filtered(lambda r: (
not r.reconciled
and r.account_id.internal_type in ('payable', 'receivable')
and (not r.date_maturity or r.date_maturity <= cutoff_date)
)):
amount = 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
else:
move_lines = invoice.line_ids.filtered(lambda r: (
not r.reconciled
and r.account_id.internal_type in ('payable', 'receivable')
)).sorted(key=lambda r: r.date_maturity or dummy_date)
if move_lines:
move_line = move_lines[0]
amount = 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': sign * (total_amount - total_reconciled),
# 'difference': sign * ((line.amount or 0.0) - residual),
'difference': (line.amount or 0.0) - residual,
'partner_id': invoice.partner_id.id,
}
line.update(values)
@api.depends('amount', 'residual', 'residual_due', 'invoice_id')
def _compute_difference(self):
for line in self:
line.difference = line.amount - line.residual

View File

@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_account_payment_form_multi_inherit" model="ir.ui.view">
<field name="name">account.payment.form.multi.inherit</field>
<field name="model">account.payment.register</field>
<field name="inherit_id" ref="account.view_account_payment_form_multi"/>
<field name="arch" type="xml">
<xpath expr="//field[@name='payment_date']" position="after">
<field name="is_manual_disperse"/>
<field name="amount" invisible="1" />
</xpath>
<xpath expr="//form/group[1]" position="after">
<group name="invoice_lines" attrs="{'invisible': [('is_manual_disperse', '=', False)]}">
<field name="payment_invoice_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="close_balance"/>
</tree>
</field>
</group>
<group name="invoice_totals" attrs="{'invisible': [('is_manual_disperse', '=', False)]}">
<group>
<field name="requires_writeoff_account" invisible="1"/>
<field name="writeoff_account_id" attrs="{'required': [('requires_writeoff_account', '=', True)]}" />
<field name="due_date_cutoff"/>
<field name="due_date_behavior"/>
<button type="object" name="action_fill_residual"
string="Fill with Remaining" class="btn-primary"/>
<button type="object" name="action_fill_residual_due"
string="Fill with Due" class="btn-primary"/>
</group>
<group>
<button type="object" name="action_toggle_close_balance"
string="Toggle Close Balance" class="btn-primary"/>
</group>
</group>
</xpath>
</field>
</record>
</odoo>