[12.0][ADD] account_payment_netting

This commit is contained in:
Kitti U
2019-05-16 07:41:07 +07:00
committed by kittiu
parent 9d1b7f08b7
commit 0736477dfd
14 changed files with 937 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import models

View File

@@ -0,0 +1,23 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
{
'name': 'Account Payment Netting',
'version': '12.0.1.0.0',
'summary': 'Net Payment on AR/AP invoice from the same partner',
'category': 'Accounting & Finance',
'author': 'Ecosoft, '
'Odoo Community Association (OCA)',
'license': 'AGPL-3',
'website': 'https://github.com/OCA/account-financial-tools/',
'depends': [
'account',
],
'data': [
'views/account_invoice_view.xml',
'views/account_payment_view.xml',
],
'installable': True,
'development_status': 'beta',
'maintainers': ['kittiu'],
}

View File

@@ -0,0 +1,5 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import account_payment
from . import account_invoice

View File

@@ -0,0 +1,109 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import models, api, fields
class AccountInvoice(models.Model):
_inherit = 'account.invoice'
unpaid_move_lines = fields.One2many(
comodel_name='account.move.line',
compute='_compute_unpaid_move_lines',
help="Compute unpaid AR/AP move lines of this invoice",
)
@api.multi
def _compute_unpaid_move_lines(self):
for inv in self:
inv.unpaid_move_lines = inv.move_id.line_ids.filtered(
lambda r: not r.reconciled
and r.account_id.internal_type in ('payable', 'receivable'))
@api.model
def _get_netting_groups(self, account_groups):
debtors = []
creditors = []
total_debtors = 0
total_creditors = 0
for account_group in account_groups:
balance = account_group['debit'] - account_group['credit']
group_vals = {
'account_id': account_group['account_id'][0],
'balance': abs(balance),
}
if balance > 0:
debtors.append(group_vals)
total_debtors += balance
else:
creditors.append(group_vals)
total_creditors += abs(balance)
return (debtors, total_debtors, creditors, total_creditors)
@api.model
def _get_netting_move_lines(self, payment_line, partner,
debtors, total_debtors,
creditors, total_creditors):
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': partner.id,
'name': payment_line.move_id.ref,
'account_id': account_group['account_id'],
'payment_id': payment_line.payment_id.id,
}
move_lines.append((0, 0, move_line_vals))
available_amount -= account_group['balance']
if available_amount <= 0:
break
return move_lines
@api.multi
def register_payment(self, payment_line, writeoff_acc_id=False,
writeoff_journal_id=False):
""" Attempt to reconcile netting first,
and leave the remaining for normal reconcile """
if not payment_line.payment_id.netting:
return super().register_payment(
payment_line, writeoff_acc_id=writeoff_acc_id,
writeoff_journal_id=writeoff_journal_id)
# Case netting payment:
# 1. create netting lines dr/cr
# 2. do initial reconcile
line_to_netting = self.mapped('unpaid_move_lines')
payment_move = payment_line.move_id
# Group amounts by account
account_groups = line_to_netting.read_group(
[('id', 'in', line_to_netting.ids)],
['account_id', 'debit', 'credit'],
['account_id'],
)
(debtors, total_debtors, creditors, total_creditors) = \
self._get_netting_groups(account_groups)
# Create move lines
move_lines = self._get_netting_move_lines(
payment_line, line_to_netting[0].partner_id,
debtors, total_debtors, creditors, total_creditors)
if move_lines:
payment_move.write({'line_ids': move_lines})
# Make reconciliation
for move_line in payment_move.line_ids:
if move_line == payment_line: # Keep this for super()
continue
to_reconcile = move_line + line_to_netting.filtered(
lambda x: x.account_id == move_line.account_id)
to_reconcile.filtered('account_id.reconcile').\
filtered(lambda r: not r.reconciled).reconcile()
return super().register_payment(
payment_line.filtered(lambda l: not l.reconciled),
writeoff_acc_id=writeoff_acc_id,
writeoff_journal_id=writeoff_journal_id)

View File

@@ -0,0 +1,101 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo import models, fields, api, _
from odoo.exceptions import UserError
class AccountAbstractPayment(models.AbstractModel):
_inherit = 'account.abstract.payment'
netting = fields.Boolean(
string='Netting',
help="Technical field, as user select invoice that are both AR and AP",
)
@api.model
def default_get(self, fields):
rec = super().default_get(fields)
if not rec.get('multi'):
return rec
active_ids = self._context.get('active_ids')
invoices = self.env['account.invoice'].browse(active_ids)
types = invoices.mapped('type')
ap = any(set(['in_invoice', 'in_refund']).intersection(types))
ar = any(set(['out_invoice', 'out_refund']).intersection(types))
if ap and ar: # Both AP and AR -> Netting
rec.update({'netting': True,
'multi': False, # With netting, allow edit amount
'communication': ', '.join(invoices.mapped('number')),
})
return rec
def _compute_journal_domain_and_types(self):
if not self.netting:
return super()._compute_journal_domain_and_types()
# For case netting, it is possible to have net amount = 0.0
# without forcing new journal type and payment diff handling
domain = []
if self.payment_type == 'inbound':
domain.append(('at_least_one_inbound', '=', True))
else:
domain.append(('at_least_one_outbound', '=', True))
return {'domain': domain, 'journal_types': set(['bank', 'cash'])}
class AccountRegisterPayments(models.TransientModel):
_inherit = 'account.register.payments'
@api.multi
def get_payments_vals(self):
""" When doing netting, combine all invoices """
if self.netting:
return [self._prepare_payment_vals(self.invoice_ids)]
return super().get_payments_vals()
@api.multi
def _prepare_payment_vals(self, invoices):
""" When doing netting, partner_type follow payment type """
values = super()._prepare_payment_vals(invoices)
if self.netting:
values['netting'] = self.netting
values['communication'] = self.communication
if self.payment_type == 'inbound':
values['partner_type'] = 'customer'
elif self.payment_type == 'outbound':
values['partner_type'] = 'supplier'
return values
@api.multi
def create_payments(self):
if self.netting:
self._validate_invoice_netting(self.invoice_ids)
return super().create_payments()
@api.model
def _validate_invoice_netting(self, invoices):
""" Ensure valid selection of invoice for netting process """
# All invoice must be of the same partner
if len(invoices.mapped('commercial_partner_id')) > 1:
raise UserError(_('All invoices must belong to same partner'))
# All invoice must have residual
paid_invoices = invoices.filtered(lambda l: not l.residual)
if paid_invoices:
raise UserError(_('Some selected invoices are already paid: %s') %
paid_invoices.mapped('number'))
class AccountPayments(models.Model):
_inherit = 'account.payment'
@api.one
@api.depends('invoice_ids', 'payment_type', 'partner_type', 'partner_id')
def _compute_destination_account_id(self):
super()._compute_destination_account_id()
if self.netting:
if self.partner_type == 'customer':
self.destination_account_id = \
self.partner_id.property_account_receivable_id.id
else:
self.destination_account_id = \
self.partner_id.property_account_payable_id.id

View File

@@ -0,0 +1 @@
* Kitti Upariphutthiphong <kittiu@ecosoft.co.th>

View File

@@ -0,0 +1,7 @@
This module allow net payment on AR/AP invoice from the same business partner.
**NOTE**: This module is influenced by account_netting,
but make it more user friendly when netting invoices.
While account netting require user to select manually the journal items to do netting
(which create netting journal entry), this module has a new menu "Invoices to netting"
allowing user to select both customer/supplier invoice to register payment.

View File

@@ -0,0 +1,9 @@
Given there are open invoices both receivable and payable,
and user decide to make payment on the diff.
- Open menu Accounting > Invoices to Netting
- Select multiple open invoices from the same partner
- Click on action "Register Payment", the wizard will show the diff amount
- Make payment as normal
This create Customer Payment if AR > AP, Supplier Payment otherwise.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,437 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" />
<title>Account Payment Netting</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to
customize this style sheet.
*/
/* used to remove borders from tables and images */
.borderless, table.borderless td, table.borderless th {
border: 0 }
table.borderless td, table.borderless th {
/* Override padding for "table.docutils td" with "! important".
The right padding separates the table cells. */
padding: 0 0.5em 0 0 ! important }
.first {
/* Override more specific margin styles with "! important". */
margin-top: 0 ! important }
.last, .with-subtitle {
margin-bottom: 0 ! important }
.hidden {
display: none }
.subscript {
vertical-align: sub;
font-size: smaller }
.superscript {
vertical-align: super;
font-size: smaller }
a.toc-backref {
text-decoration: none ;
color: black }
blockquote.epigraph {
margin: 2em 5em ; }
dl.docutils dd {
margin-bottom: 0.5em }
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] {
overflow: hidden;
}
/* Uncomment (and remove this text!) to get bold-faced definition list terms
dl.docutils dt {
font-weight: bold }
*/
div.abstract {
margin: 2em 5em }
div.abstract p.topic-title {
font-weight: bold ;
text-align: center }
div.admonition, div.attention, div.caution, div.danger, div.error,
div.hint, div.important, div.note, div.tip, div.warning {
margin: 2em ;
border: medium outset ;
padding: 1em }
div.admonition p.admonition-title, div.hint p.admonition-title,
div.important p.admonition-title, div.note p.admonition-title,
div.tip p.admonition-title {
font-weight: bold ;
font-family: sans-serif }
div.attention p.admonition-title, div.caution p.admonition-title,
div.danger p.admonition-title, div.error p.admonition-title,
div.warning p.admonition-title, .code .error {
color: red ;
font-weight: bold ;
font-family: sans-serif }
/* Uncomment (and remove this text!) to get reduced vertical space in
compound paragraphs.
div.compound .compound-first, div.compound .compound-middle {
margin-bottom: 0.5em }
div.compound .compound-last, div.compound .compound-middle {
margin-top: 0.5em }
*/
div.dedication {
margin: 2em 5em ;
text-align: center ;
font-style: italic }
div.dedication p.topic-title {
font-weight: bold ;
font-style: normal }
div.figure {
margin-left: 2em ;
margin-right: 2em }
div.footer, div.header {
clear: both;
font-size: smaller }
div.line-block {
display: block ;
margin-top: 1em ;
margin-bottom: 1em }
div.line-block div.line-block {
margin-top: 0 ;
margin-bottom: 0 ;
margin-left: 1.5em }
div.sidebar {
margin: 0 0 0.5em 1em ;
border: medium outset ;
padding: 1em ;
background-color: #ffffee ;
width: 40% ;
float: right ;
clear: right }
div.sidebar p.rubric {
font-family: sans-serif ;
font-size: medium }
div.system-messages {
margin: 5em }
div.system-messages h1 {
color: red }
div.system-message {
border: medium outset ;
padding: 1em }
div.system-message p.system-message-title {
color: red ;
font-weight: bold }
div.topic {
margin: 2em }
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle,
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle {
margin-top: 0.4em }
h1.title {
text-align: center }
h2.subtitle {
text-align: center }
hr.docutils {
width: 75% }
img.align-left, .figure.align-left, object.align-left, table.align-left {
clear: left ;
float: left ;
margin-right: 1em }
img.align-right, .figure.align-right, object.align-right, table.align-right {
clear: right ;
float: right ;
margin-left: 1em }
img.align-center, .figure.align-center, object.align-center {
display: block;
margin-left: auto;
margin-right: auto;
}
table.align-center {
margin-left: auto;
margin-right: auto;
}
.align-left {
text-align: left }
.align-center {
clear: both ;
text-align: center }
.align-right {
text-align: right }
/* reset inner alignment in figures */
div.align-right {
text-align: inherit }
/* div.align-center * { */
/* text-align: left } */
.align-top {
vertical-align: top }
.align-middle {
vertical-align: middle }
.align-bottom {
vertical-align: bottom }
ol.simple, ul.simple {
margin-bottom: 1em }
ol.arabic {
list-style: decimal }
ol.loweralpha {
list-style: lower-alpha }
ol.upperalpha {
list-style: upper-alpha }
ol.lowerroman {
list-style: lower-roman }
ol.upperroman {
list-style: upper-roman }
p.attribution {
text-align: right ;
margin-left: 50% }
p.caption {
font-style: italic }
p.credits {
font-style: italic ;
font-size: smaller }
p.label {
white-space: nowrap }
p.rubric {
font-weight: bold ;
font-size: larger ;
color: maroon ;
text-align: center }
p.sidebar-title {
font-family: sans-serif ;
font-weight: bold ;
font-size: larger }
p.sidebar-subtitle {
font-family: sans-serif ;
font-weight: bold }
p.topic-title {
font-weight: bold }
pre.address {
margin-bottom: 0 ;
margin-top: 0 ;
font: inherit }
pre.literal-block, pre.doctest-block, pre.math, pre.code {
margin-left: 2em ;
margin-right: 2em }
pre.code .ln { color: grey; } /* line numbers */
pre.code, code { background-color: #eeeeee }
pre.code .comment, code .comment { color: #5C6576 }
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold }
pre.code .literal.string, code .literal.string { color: #0C5404 }
pre.code .name.builtin, code .name.builtin { color: #352B84 }
pre.code .deleted, code .deleted { background-color: #DEB0A1}
pre.code .inserted, code .inserted { background-color: #A3D289}
span.classifier {
font-family: sans-serif ;
font-style: oblique }
span.classifier-delimiter {
font-family: sans-serif ;
font-weight: bold }
span.interpreted {
font-family: sans-serif }
span.option {
white-space: nowrap }
span.pre {
white-space: pre }
span.problematic {
color: red }
span.section-subtitle {
/* font-size relative to parent (h1..h6 element) */
font-size: 80% }
table.citation {
border-left: solid 1px gray;
margin-left: 1px }
table.docinfo {
margin: 2em 4em }
table.docutils {
margin-top: 0.5em ;
margin-bottom: 0.5em }
table.footnote {
border-left: solid 1px black;
margin-left: 1px }
table.docutils td, table.docutils th,
table.docinfo td, table.docinfo th {
padding-left: 0.5em ;
padding-right: 0.5em ;
vertical-align: top }
table.docutils th.field-name, table.docinfo th.docinfo-name {
font-weight: bold ;
text-align: left ;
white-space: nowrap ;
padding-left: 0 }
/* "booktabs" style (no vertical lines) */
table.docutils.booktabs {
border: 0px;
border-top: 2px solid;
border-bottom: 2px solid;
border-collapse: collapse;
}
table.docutils.booktabs * {
border: 0px;
}
table.docutils.booktabs th {
border-bottom: thin solid;
text-align: left;
}
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils,
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils {
font-size: 100% }
ul.auto-toc {
list-style-type: none }
</style>
</head>
<body>
<div class="document" id="account-payment-netting">
<h1 class="title">Account Payment Netting</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/12.0/account_payment_netting"><img alt="OCA/account-financial-tools" src="https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/account-financial-tools-12-0/account-financial-tools-12-0-account_payment_netting"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/92/12.0"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p>
<p>This module allow net payment on AR/AP invoice from the same business partner.</p>
<p><strong>NOTE</strong>: This module is influenced by account_netting,
but make it more user friendly when netting invoices.
While account netting require user to select manually the journal items to do netting
(which create netting journal entry), this module has a new menu “Invoices to netting”
allowing user to select both customer/supplier invoice to register payment.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#usage" id="id1">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id2">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id3">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id4">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id5">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id6">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id1">Usage</a></h1>
<p>Given there are open invoices both receivable and payable,
and user decide to make payment on the diff.</p>
<ul class="simple">
<li>Open menu Accounting &gt; Invoices to Netting</li>
<li>Select multiple open invoices from the same partner</li>
<li>Click on action “Register Payment”, the wizard will show the diff amount</li>
<li>Make payment as normal</li>
</ul>
<p>This create Customer Payment if AR &gt; AP, Supplier Payment otherwise.</p>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id2">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-financial-tools/issues">GitHub Issues</a>.
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
<a class="reference external" href="https://github.com/OCA/account-financial-tools/issues/new?body=module:%20account_payment_netting%0Aversion:%2012.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p>
<p>Do not contact contributors directly about support or help with technical issues.</p>
</div>
<div class="section" id="credits">
<h1><a class="toc-backref" href="#id3">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id4">Authors</a></h2>
<ul class="simple">
<li>Ecosoft</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id5">Contributors</a></h2>
<ul class="simple">
<li>Kitti Upariphutthiphong &lt;<a class="reference external" href="mailto:kittiu&#64;ecosoft.co.th">kittiu&#64;ecosoft.co.th</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id6">Maintainers</a></h2>
<p>This module is maintained by the OCA.</p>
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a>
<p>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.</p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/12.0/account_payment_netting">OCA/account-financial-tools</a> project on GitHub.</p>
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p>
</div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,4 @@
# Copyright 2019 Ecosoft Co., Ltd.
# License AGPL-3 - See http://www.gnu.org/licenses/agpl-3.0.html
from . import test_account_payment_netting

View File

@@ -0,0 +1,179 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from odoo.tests.common import SavepointCase, Form
from odoo.exceptions import UserError
class TestAccountNetting(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestAccountNetting, cls).setUpClass()
cls.invoice_model = cls.env['account.invoice']
cls.payment_model = cls.env['account.payment']
cls.register_payment_model = cls.env['account.register.payments']
cls.account_receivable = cls.env['account.account'].create({
'code': 'AR',
'name': 'Account Receivable',
'user_type_id': cls.env.ref(
'account.data_account_type_receivable').id,
'reconcile': True,
})
cls.account_payable = cls.env['account.account'].create({
'code': 'AP',
'name': 'Account Payable',
'user_type_id': cls.env.ref(
'account.data_account_type_payable').id,
'reconcile': True,
})
cls.account_revenue = cls.env['account.account'].search([
('user_type_id', '=', cls.env.ref(
'account.data_account_type_revenue').id)
], limit=1)
cls.account_expense = cls.env['account.account'].search([
('user_type_id', '=', cls.env.ref(
'account.data_account_type_expenses').id)
], limit=1)
cls.partner1 = cls.env['res.partner'].create({
'supplier': True,
'customer': True,
'name': 'Supplier/Customer 1',
'property_account_receivable_id': cls.account_receivable.id,
'property_account_payable_id': cls.account_payable.id,
})
cls.partner2 = cls.env['res.partner'].create({
'supplier': True,
'customer': True,
'name': 'Supplier/Customer 2',
'property_account_receivable_id': cls.account_receivable.id,
'property_account_payable_id': cls.account_payable.id,
})
cls.sale_journal = cls.env['account.journal'].create({
'name': 'Test sale journal',
'type': 'sale',
'code': 'INV',
})
cls.purchase_journal = cls.env['account.journal'].create({
'name': 'Test expense journal',
'type': 'purchase',
'code': 'BIL',
})
cls.bank_journal = cls.env['account.journal'].create({
'name': 'Test bank journal',
'type': 'bank',
'code': 'BNK',
})
cls.bank_journal.inbound_payment_method_ids |= cls.env.ref(
'account.account_payment_method_manual_in')
cls.bank_journal.outbound_payment_method_ids |= cls.env.ref(
'account.account_payment_method_manual_out')
def create_invoice(self, inv_type, partner, amount):
""" Returns an open invoice """
journal = inv_type == 'in_invoice' and \
self.purchase_journal or self.sale_journal
arap_account = inv_type == 'in_invoice' and \
self.account_payable or self.account_receivable
account = inv_type == 'in_invoice' and \
self.account_expense or self.account_revenue
invoice = self.invoice_model.create({
'journal_id': journal.id,
'type': inv_type,
'partner_id': partner.id,
'account_id': arap_account.id,
'invoice_line_ids': [(0, 0, {
'name': 'Test',
'price_unit': amount,
'account_id': account.id,
})],
})
return invoice
def do_test_register_payment(self, invoices, expected_type, expected_diff):
""" Test create customer/supplier invoices. Then, select all invoices
and make neting payment. I expect:
- Payment Type (inbound or outbound) = expected_type
- Payment amont = expected_diff
- Payment can link to all invoices
- All 4 invoices are in paid status """
# Select all invoices, and register payment
ctx = {'active_ids': invoices.ids,
'active_model': 'account.invoice'}
view_id = 'account_payment_netting.view_account_payment_from_invoices'
with Form(self.register_payment_model.with_context(ctx),
view=view_id) as f:
f.journal_id = self.bank_journal
payment_wizard = f.save()
# Diff amount = expected_diff, payment_type = expected_type
self.assertEqual(payment_wizard.amount, expected_diff)
self.assertEqual(payment_wizard.payment_type, expected_type)
# Create payments
res = payment_wizard.create_payments()
payment = self.payment_model.browse(res['res_id'])
# Payment can link to all invoices
self.assertEqual(set(payment.invoice_ids.ids), set(invoices.ids))
invoices = self.invoice_model.browse(invoices.ids)
# Test that all 4 invoices are paid
self.assertEqual(list(set(invoices.mapped('state'))), ['paid'])
def test_1_payment_netting_neutral(self):
""" Test AR = AP """
# Create 2 AR Invoice, total amount = 200.0
ar_inv_p1_1 = self.create_invoice('out_invoice', self.partner1, 100.0)
ar_inv_p1_2 = self.create_invoice('out_invoice', self.partner1, 100.0)
# Create 2 AP Invoice, total amount = 200.0
ap_inv_p1_1 = self.create_invoice('in_invoice', self.partner1, 100.0)
ap_inv_p1_2 = self.create_invoice('in_invoice', self.partner1, 100.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
invoices.action_invoice_open()
self.do_test_register_payment(invoices, 'outbound', 0.0)
def test_2_payment_netting_inbound(self):
""" Test AR > AP """
# Create 2 AR Invoice, total amount = 200.0
ar_inv_p1_1 = self.create_invoice('out_invoice', self.partner1, 100.0)
ar_inv_p1_2 = self.create_invoice('out_invoice', self.partner1, 100.0)
# Create 2 AP Invoice, total amount = 160.0
ap_inv_p1_1 = self.create_invoice('in_invoice', self.partner1, 80.0)
ap_inv_p1_2 = self.create_invoice('in_invoice', self.partner1, 80.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
invoices.action_invoice_open()
self.do_test_register_payment(invoices, 'inbound', 40.0)
def test_3_payment_netting_outbound(self):
""" Test AR < AP """
# Create 2 AR Invoice, total amount = 160.0
ar_inv_p1_1 = self.create_invoice('out_invoice', self.partner1, 80.0)
ar_inv_p1_2 = self.create_invoice('out_invoice', self.partner1, 80.0)
# Create 2 AP Invoice, total amount = 200.0
ap_inv_p1_1 = self.create_invoice('in_invoice', self.partner1, 100.0)
ap_inv_p1_2 = self.create_invoice('in_invoice', self.partner1, 100.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p1_1 | ap_inv_p1_2
invoices.action_invoice_open()
self.do_test_register_payment(invoices, 'outbound', 40.0)
def test_4_payment_netting_for_one_invoice(self):
""" Test only 1 customer invoice, should also pass test """
invoices = self.create_invoice('out_invoice', self.partner1, 80.0)
invoices.action_invoice_open()
self.do_test_register_payment(invoices, 'inbound', 80.0)
def test_5_payment_netting_wrong_partner_exception(self):
""" Test when not invoices on same partner, show warning """
# Create 2 AR Invoice, total amount = 160.0
ar_inv_p1_1 = self.create_invoice('out_invoice', self.partner1, 80.0)
ar_inv_p1_2 = self.create_invoice('out_invoice', self.partner1, 80.0)
# Create 1 AP Invoice, amount = 200.0, using different partner 2
ap_inv_p2 = self.create_invoice('in_invoice', self.partner2, 200.0)
# Test Register Payment
invoices = ar_inv_p1_1 | ar_inv_p1_2 | ap_inv_p2
invoices.action_invoice_open()
with self.assertRaises(UserError) as e:
self.do_test_register_payment(invoices, 'outbound', 40.0)
self.assertEqual(e.exception.name,
'All invoices must belong to same partner')

View File

@@ -0,0 +1,38 @@
<!-- Copyright 2019 Ecosoft
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="action_invoice_all_tree" model="ir.actions.act_window">
<field name="name">Invoices for Netting</field>
<field name="res_model">account.invoice</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field eval="False" name="view_id"/>
<field name="domain">[('state', '=', 'open')]</field>
<field name="context">{'type':'out_invoice', 'journal_type': 'sale'}</field>
<field name="search_view_id" ref="account.view_account_invoice_filter"/>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
Create a customer invoice
</p><p>
Create invoices, register payments and keep track of the discussions with your customers.
</p>
</field>
</record>
<record id="action_invoice_all_view1" model="ir.actions.act_window.view">
<field eval="1" name="sequence"/>
<field name="view_mode">tree</field>
<field name="view_id" ref="account.invoice_tree_with_onboarding"/>
<field name="act_window_id" ref="action_invoice_all_tree"/>
</record>
<record id="action_invoice_all_view2" model="ir.actions.act_window.view">
<field eval="2" name="sequence"/>
<field name="view_mode">form</field>
<field name="view_id" ref="account.invoice_form"/>
<field name="act_window_id" ref="action_invoice_all_tree"/>
</record>
<menuitem action="action_invoice_all_tree" id="menu_action_invoice_all_tree" parent="account.menu_finance_entries" sequence="50"/>
</odoo>

View File

@@ -0,0 +1,20 @@
<!-- Copyright 2019 Ecosoft
License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). -->
<odoo>
<record id="view_account_payment_from_invoices" model="ir.ui.view">
<field name="name">view.account.payment.from.invoices</field>
<field name="model">account.register.payments</field>
<field name="inherit_id" ref="account.view_account_payment_from_invoices"/>
<field name="arch" type="xml">
<field name="multi" position="after">
<field name="netting" invisible="1"/>
</field>
<field name="journal_id" position="attributes">
<attribute name="attrs">{'invisible': [('amount', '=', 0), ('netting', '=', False)]}</attribute>
</field>
<field name="group_invoices" position="attributes">
<attribute name="attrs">{'invisible': [('netting', '=', True)]}</attribute>
</field>
</field>
</record>
</odoo>