pull/537/merge
sonhd 2025-04-17 07:27:00 +02:00 committed by GitHub
commit ef3245b6c0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 1228 additions and 0 deletions

View File

@ -0,0 +1,83 @@
===================================
Account Payment Mode Auto Reconcile
===================================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github
:target: https://github.com/OCA/account-reconcile/tree/16.0/account_payment_mode_auto_reconcile
:alt: OCA/account-reconcile
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-reconcile-16-0/account-reconcile-16-0-account_payment_mode_auto_reconcile
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/webui/builds.html?repo=OCA/account-reconcile&target_branch=16.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module adds a checkbox `auto_reconcile_outstanding_credits` on account
payment modes to allow automatic reconciliation on account invoices if it is
checked.
Automatic reconciliation of outstanding credits will only happen on customer
invoices at validation if the payment mode is set or when the payment mode is
changed on an open invoice. If a payment mode using auto-reconcile is removed
from an open invoice, the existing auto reconciled payments will be removed.
Another option `auto_reconcile_allow_partial` on account payment mode defines
if outstanding credits can be partially used for the auto reconciliation.
**Table of contents**
.. contents::
:local:
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-reconcile/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us smashing it by providing a detailed and welcomed
`feedback <https://github.com/OCA/account-reconcile/issues/new?body=module:%20account_payment_mode_auto_reconcile%0Aversion:%2016.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
Do not contact contributors directly about support or help with technical issues.
Credits
=======
Authors
~~~~~~~
* Camptocamp
Contributors
~~~~~~~~~~~~
* Akim Juillerat <akim.juillerat@camptocamp.com>
Maintainers
~~~~~~~~~~~
This module is maintained by the OCA.
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org
OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.
This module is part of the `OCA/account-reconcile <https://github.com/OCA/account-reconcile/tree/16.0/account_payment_mode_auto_reconcile>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

View File

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

View File

@ -0,0 +1,22 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
{
"name": "Account Payment Mode Auto Reconcile",
"summary": "Reconcile outstanding credits according to payment mode",
"version": "16.0.1.0.0",
"category": "Banking addons",
"website": "https://github.com/OCA/account-reconcile",
"author": "Camptocamp, Odoo Community Association (OCA)",
"license": "AGPL-3",
"installable": True,
"depends": [
"account_payment_partner",
],
"data": [
"views/account_invoice.xml",
"views/account_payment_mode.xml",
],
"demo": [
"demo/account_payment_mode.xml",
],
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record
id="account_payment_mode.payment_mode_inbound_dd1"
model="account.payment.mode"
>
<field name="auto_reconcile_outstanding_credits" eval="True" />
</record>
</odoo>

View File

@ -0,0 +1,98 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * account_payment_mode_auto_reconcile
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 10.0\n"
"Report-Msgid-Bugs-To: \n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_allow_partial
msgid "Allow partial"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,help:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_allow_partial
msgid "Allows automatic partial reconciliation of outstanding credits"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_outstanding_credits
msgid "Auto reconcile"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.ui.view,arch_db:account_payment_mode_auto_reconcile.account_payment_mode_form_inherit
msgid "Auto reconcile outstanding credits"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: code:addons/account_payment_mode_auto_reconcile/models/account_invoice.py:160
#, python-format
msgid "Changing payment mode will reconcile outstanding credits."
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: code:addons/account_payment_mode_auto_reconcile/models/account_invoice.py:150
#, python-format
msgid "Changing payment mode will unreconcile existing auto reconciled payments."
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_invoice_display_payment_mode_warning
msgid "Display payment mode warning"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model,name:account_payment_mode_auto_reconcile.model_account_invoice
msgid "Invoice"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,help:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_same_journal
msgid "Only reconcile payment in the same journal than the invoice"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_same_journal
msgid "Only same journal"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model,name:account_payment_mode_auto_reconcile.model_account_partial_reconcile
msgid "Partial Reconcile"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model,name:account_payment_mode_auto_reconcile.model_account_payment_mode
msgid "Payment Modes"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_partial_reconcile_payment_mode_auto_reconcile
msgid "Payment mode auto reconcile"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,field_description:account_payment_mode_auto_reconcile.field_account_invoice_payment_mode_warning
msgid "Payment mode warning"
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: model:ir.model.fields,help:account_payment_mode_auto_reconcile.field_account_payment_mode_auto_reconcile_outstanding_credits
msgid "Reconcile automatically outstanding credits when an invoice using this payment mode is validated, or when this payment mode is defined on an open invoice."
msgstr ""
#. module: account_payment_mode_auto_reconcile
#: code:addons/account_payment_mode_auto_reconcile/models/account_invoice.py:140
#, python-format
msgid "Validating invoices with this payment mode will reconcile any outstanding credits."
msgstr ""

View File

@ -0,0 +1,3 @@
from . import account_move
from . import account_partial_reconcile
from . import account_payment_mode

View File

@ -0,0 +1,174 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from operator import itemgetter
from odoo import _, api, fields, models
class AccountMove(models.Model):
_inherit = "account.move"
# Allow changing payment mode in open state
# TODO: Check if must be done in account_payment_partner instead
payment_mode_id = fields.Many2one(
states={"draft": [("readonly", False)], "posted": [("readonly", False)]}
)
payment_mode_warning = fields.Char(
compute="_compute_payment_mode_warning",
)
display_payment_mode_warning = fields.Boolean(
compute="_compute_payment_mode_warning",
)
def action_post(self):
res = super(AccountMove, self).action_post()
for invoice in self:
if invoice.move_type != "out_invoice":
continue
if not invoice.payment_mode_id.auto_reconcile_outstanding_credits:
continue
partial = invoice.payment_mode_id.auto_reconcile_allow_partial
invoice.with_context(
_payment_mode_auto_reconcile=True
).auto_reconcile_credits(partial_allowed=partial)
return res
def write(self, vals):
res = super(AccountMove, self).write(vals)
if "payment_mode_id" in vals or "state" in vals:
for invoice in self:
# Do not auto reconcile anything else than open customer inv
if invoice.state != "posted" or invoice.move_type != "out_invoice":
continue
invoice_lines = invoice.line_ids.filtered(
lambda line: line.account_type == "asset_receivable"
)
# Auto reconcile if payment mode sets it
payment_mode = invoice.payment_mode_id
if payment_mode and payment_mode.auto_reconcile_outstanding_credits:
partial = payment_mode.auto_reconcile_allow_partial
invoice.with_context(
_payment_mode_auto_reconcile=True
).auto_reconcile_credits(partial_allowed=partial)
# If the payment mode is not using auto reconcile we remove
# the existing reconciliations
elif any(
[
invoice_lines.mapped("matched_credit_ids"),
invoice_lines.mapped("matched_debit_ids"),
]
):
invoice.auto_unreconcile_credits()
return res
def auto_reconcile_credits(self, partial_allowed=True):
for invoice in self:
invoice._compute_payments_widget_to_reconcile_info()
if not invoice.invoice_has_outstanding:
continue
credits_info = invoice.invoice_outstanding_credits_debits_widget
# Get outstanding credits in chronological order
# (using reverse because aml is sorted by date desc as default)
credits_dict = credits_info.get("content", False)
if invoice.payment_mode_id.auto_reconcile_same_journal:
credits_dict = invoice._filter_payment_same_journal(credits_dict)
sorted_credits = self._sort_credits_dict(credits_dict)
for credit in sorted_credits:
if (
not partial_allowed
and credit.get("amount") > invoice.amount_residual
):
continue
invoice.js_assign_outstanding_line(credit.get("id"))
@api.model
def _sort_credits_dict(self, credits_dict):
"""Sort credits dict according to their id (oldest recs first)"""
return sorted(credits_dict, key=itemgetter("id"))
def _filter_payment_same_journal(self, credits_dict):
"""Keep only credits on the same journal than the invoice."""
self.ensure_one()
line_ids = [credit["id"] for credit in credits_dict]
lines = self.env["account.move.line"].search(
[("id", "in", line_ids), ("journal_id", "=", self.journal_id.id)]
)
return [credit for credit in credits_dict if credit["id"] in lines.ids]
def auto_unreconcile_credits(self):
for invoice in self:
payments_info = invoice.invoice_payments_widget
for payment in payments_info.get("content", []):
payment_aml = (
self.env["account.payment"]
.browse(payment.get("account_payment_id"))
.line_ids
)
aml = payment_aml.filtered(lambda l: l.matched_debit_ids)
for apr in aml.matched_debit_ids:
if apr.amount != payment.get("amount"):
continue
if (
apr.payment_mode_auto_reconcile
and apr.debit_move_id.move_id == invoice
):
aml.remove_move_reconcile()
@api.depends(
"move_type", "payment_mode_id", "payment_id", "state", "invoice_has_outstanding"
)
def _compute_payment_mode_warning(self):
# TODO Improve me but watch out
for invoice in self:
existed_reconciliations = any(
[
invoice.line_ids.mapped("matched_credit_ids"),
invoice.line_ids.mapped("matched_debit_ids"),
]
)
if invoice.move_type != "out_invoice" or (
invoice.state == "posted" and invoice.payment_state != "paid"
):
invoice.payment_mode_warning = ""
invoice.display_payment_mode_warning = False
continue
invoice.display_payment_mode_warning = True
if (
invoice.state != "posted"
and invoice.payment_mode_id
and invoice.payment_mode_id.auto_reconcile_outstanding_credits
):
invoice.payment_mode_warning = _(
"Validating invoices with this payment mode will reconcile"
" any outstanding credits."
)
elif (
invoice.state == "posted"
and invoice.payment_state != "paid"
and existed_reconciliations
and (
not invoice.payment_mode_id
or not invoice.payment_mode_id.auto_reconcile_outstanding_credits
)
):
invoice.payment_mode_warning = _(
"Changing payment mode will unreconcile existing auto "
"reconciled payments."
)
elif (
invoice.state == "posted"
and invoice.payment_state != "paid"
and not existed_reconciliations
and invoice.payment_mode_id
and invoice.payment_mode_id.auto_reconcile_outstanding_credits
and invoice.invoice_has_outstanding
):
invoice.payment_mode_warning = _(
"Changing payment mode will reconcile outstanding credits."
)
else:
invoice.payment_mode_warning = ""
invoice.display_payment_mode_warning = False

View File

@ -0,0 +1,16 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import fields, models
class AccountPartialReconcile(models.Model):
_inherit = "account.partial.reconcile"
payment_mode_auto_reconcile = fields.Boolean()
def create(self, vals):
if self.env.context.get("_payment_mode_auto_reconcile"):
for val in vals:
val["payment_mode_auto_reconcile"] = True
return super(AccountPartialReconcile, self).create(vals)

View File

@ -0,0 +1,25 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from odoo import fields, models
class AccountPaymentMode(models.Model):
_inherit = "account.payment.mode"
auto_reconcile_outstanding_credits = fields.Boolean(
string="Auto reconcile",
help="Reconcile automatically outstanding credits when an invoice "
"using this payment mode is validated, or when this payment mode "
"is defined on an open invoice.",
)
auto_reconcile_allow_partial = fields.Boolean(
default=True,
string="Allow partial",
help="Allows automatic partial reconciliation of outstanding credits",
)
auto_reconcile_same_journal = fields.Boolean(
default=False,
string="Only same journal",
help="Only reconcile payment in the same journal than the invoice",
)

View File

@ -0,0 +1 @@
* Akim Juillerat <akim.juillerat@camptocamp.com>

View File

@ -0,0 +1,11 @@
This module adds a checkbox `auto_reconcile_outstanding_credits` on account
payment modes to allow automatic reconciliation on account invoices if it is
checked.
Automatic reconciliation of outstanding credits will only happen on customer
invoices at validation if the payment mode is set or when the payment mode is
changed on an open invoice. If a payment mode using auto-reconcile is removed
from an open invoice, the existing auto reconciled payments will be removed.
Another option `auto_reconcile_allow_partial` on account payment mode defines
if outstanding credits can be partially used for the auto reconciliation.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@ -0,0 +1,427 @@
<?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: http://docutils.sourceforge.net/" />
<title>Account Payment Mode Auto Reconcile</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-mode-auto-reconcile">
<h1 class="title">Account Payment Mode Auto Reconcile</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-reconcile/tree/16.0/account_payment_mode_auto_reconcile"><img alt="OCA/account-reconcile" src="https://img.shields.io/badge/github-OCA%2Faccount--reconcile-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/account-reconcile-16-0/account-reconcile-16-0-account_payment_mode_auto_reconcile"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runboat.odoo-community.org/webui/builds.html?repo=OCA/account-reconcile&amp;target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module adds a checkbox <cite>auto_reconcile_outstanding_credits</cite> on account
payment modes to allow automatic reconciliation on account invoices if it is
checked.</p>
<p>Automatic reconciliation of outstanding credits will only happen on customer
invoices at validation if the payment mode is set or when the payment mode is
changed on an open invoice. If a payment mode using auto-reconcile is removed
from an open invoice, the existing auto reconciled payments will be removed.</p>
<p>Another option <cite>auto_reconcile_allow_partial</cite> on account payment mode defines
if outstanding credits can be partially used for the auto reconciliation.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#bug-tracker" id="id1">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id2">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id3">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id4">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id5">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id1">Bug Tracker</a></h1>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-reconcile/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-reconcile/issues/new?body=module:%20account_payment_mode_auto_reconcile%0Aversion:%2016.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="#id2">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id3">Authors</a></h2>
<ul class="simple">
<li>Camptocamp</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id4">Contributors</a></h2>
<ul class="simple">
<li>Akim Juillerat &lt;<a class="reference external" href="mailto:akim.juillerat&#64;camptocamp.com">akim.juillerat&#64;camptocamp.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id5">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-reconcile/tree/16.0/account_payment_mode_auto_reconcile">OCA/account-reconcile</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 @@
from . import test_partner_auto_reconcile

View File

@ -0,0 +1,299 @@
# Copyright 2019 Camptocamp SA
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl)
from datetime import date, timedelta
from odoo.tests import TransactionCase
from odoo.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
class TestPartnerAutoReconcile(TransactionCase):
@classmethod
def setUpClass(cls):
super(TestPartnerAutoReconcile, cls).setUpClass()
cls.env = cls.env(context=dict(cls.env.context, tracking_disable=True))
cls.acc_rec = cls.env["account.account"].create(
{
"name": "Receivable",
"code": "RECEIVE",
"account_type": "asset_receivable",
"company_id": cls.env.ref("base.main_company").id,
}
)
cls.acc_pay = cls.env["account.account"].create(
{
"name": "Payable",
"code": "PAYABLE",
"account_type": "liability_payable",
"company_id": cls.env.ref("base.main_company").id,
}
)
cls.acc_rev = cls.env["account.account"].create(
{
"name": "Income",
"code": "INCOME",
"account_type": "income",
"company_id": cls.env.ref("base.main_company").id,
}
)
cls.partner = cls.env["res.partner"].create(
{
"name": "Test partner",
"customer_rank": 1,
"property_account_receivable_id": cls.acc_rec.id,
"property_account_payable_id": cls.acc_pay.id,
}
)
cls.payment_mode = cls.env.ref("account_payment_mode.payment_mode_inbound_dd1")
# TODO check why it's not set from demo data
cls.payment_mode.auto_reconcile_outstanding_credits = True
cls.product = cls.env.ref("product.consu_delivery_02")
cls.journal = cls.env["account.journal"].create(
{
"name": "BANK",
"code": "BANK-TEST",
"company_id": cls.env.ref("base.main_company").id,
"type": "bank",
}
)
cls.sale_journal = cls.env["account.journal"].create(
{
"name": "SALE-TEST",
"code": "SALE",
"company_id": cls.env.ref("base.main_company").id,
"type": "sale",
}
)
cls.invoice = cls.env["account.move"].create(
{
"partner_id": cls.partner.id,
"move_type": "out_invoice",
"invoice_payment_term_id": cls.env.ref(
"account.account_payment_term_immediate"
).id,
"invoice_line_ids": [
(
0,
0,
{
"product_id": cls.product.id,
"name": cls.product.name,
"price_unit": 1000.0,
"quantity": 1,
"account_id": cls.acc_rev.id,
},
)
],
}
)
cls.invoice.action_post()
cls.refund_wiz = (
cls.env["account.move.reversal"]
.with_context(active_ids=cls.invoice.ids)
.create(
{
"refund_method": "refund",
"move_ids": [cls.invoice.id],
"journal_id": cls.sale_journal.id,
}
)
)
refund_id = cls.refund_wiz.reverse_moves().get("res_id")
cls.refund = cls.env["account.move"].browse(refund_id)
cls.payment = cls.env["account.payment"].create(
{
"amount": 500.0,
"partner_id": cls.partner.id,
}
)
cls.payment.action_post()
cls.invoice_copy = cls.invoice.copy()
cls.invoice_copy.write(
{
"invoice_line_ids": [
(
0,
0,
{
"product_id": cls.product.id,
"name": cls.product.name,
"price_unit": 500.0,
"quantity": 1,
"account_id": cls.acc_rev.id,
},
)
]
}
)
def test_invoice_validate_auto_reconcile(self):
auto_rec_invoice = self.invoice.copy(
{
"payment_mode_id": self.payment_mode.id,
}
)
auto_rec_invoice.action_post()
self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits)
self.assertEqual(self.invoice_copy.amount_residual, 1725.0)
self.assertEqual(auto_rec_invoice.amount_residual, 650.0)
def test_invoice_change_auto_reconcile(self):
self.assertEqual(self.invoice_copy.amount_residual, 1725.0)
self.invoice_copy.write({"payment_mode_id": self.payment_mode.id})
self.invoice_copy.action_post()
# Reconcile 500 from payment
self.assertEqual(self.invoice_copy.amount_residual, 1225.0)
self.invoice_copy.button_draft()
self.invoice_copy.write({"payment_mode_id": False})
self.invoice_copy.action_post()
self.assertEqual(self.invoice_copy.amount_residual, 1725.0)
# Copy the refund so there's more outstanding credit than invoice total
new_refund = self.refund.copy()
new_refund.date = (date.today() + timedelta(days=1)).strftime(DATE_FORMAT)
new_refund.invoice_line_ids.write({"price_unit": 1200})
new_refund.action_post()
# Set reconcile partial to False
self.payment_mode.auto_reconcile_allow_partial = False
self.assertFalse(self.payment_mode.auto_reconcile_allow_partial)
self.invoice_copy.write({"payment_mode_id": self.payment_mode.id})
# Only the older move is used as payment
self.assertEqual(self.invoice_copy.amount_residual, 1225.0)
self.invoice_copy.write({"payment_mode_id": False})
self.assertEqual(self.invoice_copy.amount_residual, 1725.0)
# Set allow partial will reconcile both moves
self.payment_mode.auto_reconcile_allow_partial = True
self.invoice_copy.write({"payment_mode_id": self.payment_mode.id})
self.assertEqual(self.invoice_copy.state, "posted")
self.assertEqual(self.invoice_copy.amount_residual, 0)
def test_invoice_auto_unreconcile(self):
# Copy the refund so there's more outstanding credit than invoice total
new_refund = self.refund.copy()
new_refund.date = (date.today() + timedelta(days=1)).strftime(DATE_FORMAT)
new_refund.invoice_line_ids.write({"price_unit": 1200})
new_refund.action_post()
auto_rec_invoice = self.invoice.copy(
{
"payment_mode_id": self.payment_mode.id,
}
)
auto_rec_invoice.invoice_line_ids.write({"price_unit": 800})
auto_rec_invoice.action_post()
self.assertEqual(auto_rec_invoice.state, "posted")
self.assertEqual(auto_rec_invoice.amount_residual, 0)
# As we had 1880 (500 for payment and 1200 + 15% of new_fund) of
# outstanding credits and 920 was assigned, there's 960 left
self.assertTrue(self.payment_mode.auto_reconcile_allow_partial)
self.invoice_copy.write({"payment_mode_id": self.payment_mode.id})
self.invoice_copy.action_post()
self.assertEqual(self.invoice_copy.amount_residual, 765.0)
# Unreconcile of an invoice doesn't change the reconciliation of the
# other invoice
self.invoice_copy.button_draft()
self.invoice_copy.write({"payment_mode_id": False})
self.assertEqual(self.invoice_copy.amount_residual, 1725.0)
self.assertEqual(auto_rec_invoice.state, "posted")
self.assertEqual(auto_rec_invoice.amount_residual, 0)
def test_invoice_auto_unreconcile_only_auto_reconcile(self):
refund = self.refund.copy()
refund.invoice_line_ids.write({"price_unit": 100})
refund.action_post()
new_invoice = self.invoice_copy.copy()
new_invoice.action_post()
# Only reconcile 1000 refund manually
new_invoice_credits = new_invoice.invoice_outstanding_credits_debits_widget.get(
"content"
)
for cred in new_invoice_credits:
if cred.get("amount") == 115.0:
new_invoice.js_assign_outstanding_line(cred.get("id"))
self.assertEqual(new_invoice.amount_residual, 1610.0)
# Assign payment mode adds the outstanding credit of 500
new_invoice.write({"payment_mode_id": self.payment_mode.id})
self.assertEqual(round(new_invoice.amount_residual, 2), 1110.0)
# Remove payment mode only removes automatically added credit
new_invoice.write({"payment_mode_id": False})
self.assertEqual(new_invoice.amount_residual, 1610.0)
# use the same payment partially on different invoices.
other_invoice = self.invoice.copy()
other_invoice.invoice_line_ids.write(
{
"price_unit": 200,
}
)
other_invoice.write(
{
"payment_mode_id": self.payment_mode.id,
}
)
other_invoice.action_post()
self.assertEqual(other_invoice.state, "posted")
# since 230 (200 + 15% VAT) were assigned on other invoice adding
# auto-rec payment mode on new_invoice will reconcile 270
# and residual will be 1340.0
new_invoice.write({"payment_mode_id": self.payment_mode.id})
self.assertEqual(new_invoice.amount_residual, 1340.0)
# Removing the payment mode should not remove the partial payment on
# the other invoice
new_invoice.write({"payment_mode_id": False})
self.assertEqual(new_invoice.amount_residual, 1610.0)
self.assertEqual(other_invoice.state, "posted")
def test_invoice_auto_reconcile_same_journal(self):
"""Check reconciling credits on same journal."""
self.payment_mode.auto_reconcile_same_journal = True
auto_rec_invoice = self.invoice.copy(
{
"payment_mode_id": self.payment_mode.id,
}
)
payment_method_line = self.env["account.payment.method.line"].search(
[
("code", "=", "manual"),
("payment_type", "=", "inbound"),
("journal_id", "=", self.journal.id),
]
)
self.invoice.journal_id.inbound_payment_method_line_ids = [
payment_method_line.id
]
payment_same_journal = self.env["account.payment"].create(
{
"amount": 500.0,
"partner_id": self.partner.id,
"journal_id": auto_rec_invoice.journal_id.id,
}
)
payment_same_journal.action_post()
self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits)
self.assertEqual(self.invoice_copy.amount_residual, 1725.0)
auto_rec_invoice.action_post()
self.assertEqual(auto_rec_invoice.amount_residual, 650)
def test_invoice_auto_reconcile_different_journal(self):
"""Check not reconciling credits on different journal."""
self.payment_mode.auto_reconcile_same_journal = True
auto_rec_invoice = self.invoice.copy(
{
"payment_mode_id": self.payment_mode.id,
"journal_id": self.sale_journal.id,
}
)
payment_different_journal = self.env["account.payment"].create(
{
"amount": 500.0,
"partner_id": self.partner.id,
}
)
payment_different_journal.action_post()
auto_rec_invoice.action_post()
self.assertTrue(self.payment_mode.auto_reconcile_outstanding_credits)
self.assertEqual(self.invoice_copy.amount_residual, 1725.0)
self.assertEqual(auto_rec_invoice.amount_residual, 1150.0)

View File

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="view_move_form_inherit" model="ir.ui.view">
<field name="name">account_payment_partner_view_move_form_inherit</field>
<field name="model">account.move</field>
<field name="inherit_id" ref="account_payment_partner.view_move_form" />
<field name="arch" type="xml">
<field name="payment_mode_id" position="before">
<field name="display_payment_mode_warning" invisible="True" />
</field>
<field name="payment_mode_id" position="after">
<label for="payment_mode_warning" invisible="True" />
<div
colspan="2"
style="padding-top:0px; padding-left:0px"
class="alert oe_edit_only"
attrs="{'invisible': [('display_payment_mode_warning', '!=', True)]}"
role="alert"
>
<field name="payment_mode_warning" nolabel="1" />
</div>
</field>
</field>
</record>
</odoo>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" ?>
<odoo>
<record id="account_payment_mode_form_inherit" model="ir.ui.view">
<field name="name">account.payment.mode.form.inherit</field>
<field name="model">account.payment.mode</field>
<field name="inherit_id" ref="account_payment_mode.account_payment_mode_form" />
<field name="arch" type="xml">
<group name="note" position="before">
<group
string="Auto reconcile outstanding credits"
name="auto_reconcile"
>
<field name="auto_reconcile_outstanding_credits" />
<field
name="auto_reconcile_allow_partial"
attrs="{'invisible': [('auto_reconcile_outstanding_credits', '=', False)]}"
/>
<field
name="auto_reconcile_same_journal"
attrs="{'invisible': [('auto_reconcile_outstanding_credits', '=', False)]}"
/>
</group>
</group>
</field>
</record>
</odoo>

View File

@ -0,0 +1 @@
../../../../account_payment_mode_auto_reconcile

View File

@ -0,0 +1,6 @@
import setuptools
setuptools.setup(
setup_requires=['setuptools-odoo'],
odoo_addon=True,
)