[12.0][ADD] account_document_reversal

This commit is contained in:
Kitti U
2019-05-16 10:22:30 +07:00
committed by Jordi Ballester Alomar
parent 3a53ba8f7d
commit 0e44a9de54
21 changed files with 1321 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
=========================
Account Document Reversal
=========================
.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| 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
.. |badge2| image:: https://img.shields.io/badge/github-OCA%2Faccount--financial--tools-lightgray.png?logo=github
:target: https://github.com/OCA/account-financial-tools/tree/12.0/account_document_reversal
:alt: OCA/account-financial-tools
.. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-financial-tools-12-0/account-financial-tools-12-0-account_document_reversal
:alt: Translate me on Weblate
.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png
:target: https://runbot.odoo-community.org/runbot/92/12.0
:alt: Try me on Runbot
|badge1| |badge2| |badge3| |badge4|
By Odoo standard, when an account document is cancelled, its journal entry will be deleted completely.
This module enhance the process, instead of deletion, it will create new reversed journal entry.
This will help preserved the accounting history, which is strictly required by some country.
Following are documented provide this feature,
- Invoice (account.invoice)
- Payment (acccont.payment)
- Bank Statement (account.bank.statement.line)
**Table of contents**
.. contents::
:local:
Configuration
=============
To use document reversal, setup the document's journal as following,
- Allow Cancelling = True
- Cancel method = Reversal (create reversed journal entries)
Usage
=====
After configure document journal to allow cancel with reversal, it is ready to use.
- Cancel document as normally do, system will show new cancel wizard
- User can select cancel date and new journal (if different from the document)
which will be used for the reversed journal entry
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-tools/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-financial-tools/issues/new?body=module:%20account_document_reversal%0Aversion:%2012.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
~~~~~~~
* Ecosoft
Contributors
~~~~~~~~~~~~
* Kitti Upariphutthiphong <kittiu@ecosoft.co.th>
* Jordi Ballester <jordi.ballester@eficent.com.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.
.. |maintainer-kittiu| image:: https://github.com/kittiu.png?size=40px
:target: https://github.com/kittiu
:alt: kittiu
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-kittiu|
This module is part of the `OCA/account-financial-tools <https://github.com/OCA/account-financial-tools/tree/12.0/account_document_reversal>`_ project on GitHub.
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

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 wizard
from . import models

View File

@@ -0,0 +1,22 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
{
'name': 'Account Document Reversal',
'summary': 'Create reversed journal entries when cancel document',
'version': '12.0.1.0.0',
'author': 'Ecosoft,'
'Odoo Community Association (OCA)',
'website': 'https://github.com/OCA/account-financial-tools',
'category': 'Accounting & Finance',
'depends': ['account_cancel'],
'data': [
'wizard/reverse_account_document_wizard.xml',
'views/account_view.xml',
],
'license': 'AGPL-3',
'installable': True,
'application': False,
'development_status': 'beta',
'maintainers': ['kittiu'],
}

View File

@@ -0,0 +1,7 @@
# 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
from . import account_document_reversal
from . import account_invoice
from . import account_payment
from . import account_bank_statement

View File

@@ -0,0 +1,24 @@
# 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
class AccountJournal(models.Model):
_inherit = 'account.journal'
cancel_method = fields.Selection(
[('normal', 'Normal (delete journal entries if exists)'),
('reversal', 'Reversal (create reversed journal entries)')],
string='Cancel Method',
default='normal',
required=True)
is_cancel_reversal = fields.Boolean(
string='Use Cancel Reversal',
compute='_compute_is_cancel_reversal',
help="True, when journal allow cancel entries with method is reversal")
@api.multi
def _compute_is_cancel_reversal(self):
for rec in self:
rec.is_cancel_reversal = \
rec.update_posted and rec.cancel_method == 'reversal'

View File

@@ -0,0 +1,65 @@
# 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 api, models, _
from odoo.exceptions import UserError
class AccountPayment(models.Model):
_name = 'account.bank.statement.line'
_inherit = ['account.bank.statement.line', 'account.document.reversal']
@api.multi
def button_cancel_reconciliation(self):
""" If cancel method is to reverse, use document reversal wizard """
cancel_reversal = all(self.mapped('journal_entry_ids.move_id.'
'journal_id.is_cancel_reversal'))
states = self.mapped('statement_id.state')
if cancel_reversal:
if not all(st == 'open' for st in states):
raise UserError(
_('Only new bank statement can be cancelled'))
return self.reverse_document_wizard()
return super().button_cancel_reconciliation()
@api.multi
def action_document_reversal(self, date=None, journal_id=None):
""" Reverse all moves related to this statement + delete payment """
# This part is from button_cancel_reconciliation()
aml_to_unbind = self.env['account.move.line']
aml_to_cancel = self.env['account.move.line']
payment_to_unreconcile = self.env['account.payment']
payment_to_cancel = self.env['account.payment']
for st_line in self:
aml_to_unbind |= st_line.journal_entry_ids
for line in st_line.journal_entry_ids:
payment_to_unreconcile |= line.payment_id
if st_line.move_name and \
line.payment_id.payment_reference == st_line.move_name:
# there can be several moves linked to a statement line but
# maximum one created by the line itself
aml_to_cancel |= line
payment_to_cancel |= line.payment_id
aml_to_unbind = aml_to_unbind - aml_to_cancel
if aml_to_unbind:
aml_to_unbind.write({'statement_line_id': False})
payment_to_unreconcile = payment_to_unreconcile - payment_to_cancel
if payment_to_unreconcile:
payment_to_unreconcile.unreconcile()
# --
# Set all moves to unreconciled
aml_to_cancel.filtered(lambda x:
x.account_id.reconcile).remove_move_reconcile()
moves = aml_to_cancel.mapped('move_id')
# Important to remove relation with move.line before reverse
aml_to_cancel.write({'payment_id': False,
'statement_id': False,
'statement_line_id': False})
# Create reverse entries
moves.reverse_moves(date, journal_id)
# Delete related payments
if payment_to_cancel:
payment_to_cancel.unlink()
# Unlink from statement line
self.write({'move_name': False})
return True

View File

@@ -0,0 +1,26 @@
# 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
class AccountDocumentReversal(models.AbstractModel):
_name = 'account.document.reversal'
_description = 'Abstract Module for Document Reversal'
@api.model
def reverse_document_wizard(self):
""" Return Wizard to Cancel Document """
action = self.env.ref('account_document_reversal.'
'action_view_reverse_account_document')
vals = action.read()[0]
return vals
@api.multi
def action_document_reversal(self, date=None, journal_id=None):
""" Reverse with following guildeline,
- Check existing document state / raise warning
- Find all related moves and unreconcile
- Create reversed moves
- Set state to cancel
"""
raise NotImplementedError()

View File

@@ -0,0 +1,52 @@
# 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 api, models, _
from odoo.exceptions import ValidationError, UserError
class AccountInvoice(models.Model):
_name = 'account.invoice'
_inherit = ['account.invoice', 'account.document.reversal']
@api.multi
def action_invoice_cancel(self):
""" If cancel method is to reverse, use document reversal wizard
* Draft invoice, fall back to standard invoice cancel
* Non draft, must be fully open (not even partial reconciled) to cancel
"""
cancel_reversal = all(self.mapped('journal_id.is_cancel_reversal'))
states = self.mapped('state')
if cancel_reversal and 'draft' not in states:
if not all(st == 'open' for st in states) or \
(self.mapped('move_id.line_ids.matched_debit_ids') |
self.mapped('move_id.line_ids.matched_credit_ids')):
raise UserError(
_('Only fully unpaid invoice can be cancelled.\n'
'To cancel this invoice, make sure all payment(s) '
'are also cancelled.'))
return self.reverse_document_wizard()
return super().action_invoice_cancel()
@api.multi
def action_document_reversal(self, date=None, journal_id=None):
""" Reverse all moves related to this invoice + set state to cancel """
# Check document state
if 'cancel' in self.mapped('state'):
raise ValidationError(
_('You are trying to cancel the cancelled document'))
MoveLine = self.env['account.move.line']
move_lines = MoveLine.search([('invoice_id', 'in', self.ids)])
moves = move_lines.mapped('move_id')
# Set all moves to unreconciled
move_lines.filtered(lambda x:
x.account_id.reconcile).remove_move_reconcile()
# Important to remove relation with move.line before reverse
move_lines.write({'invoice_id': False})
# Create reverse entries
moves.reverse_moves(date, journal_id)
# Set state cancelled and unlink with account.move
self.write({'move_id': False,
'move_name': False,
'reference': False,
'state': 'cancel'})
return True

View File

@@ -0,0 +1,40 @@
# 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 api, models, _
from odoo.exceptions import ValidationError
class AccountPayment(models.Model):
_name = 'account.payment'
_inherit = ['account.payment', 'account.document.reversal']
@api.multi
def cancel(self):
""" If cancel method is to reverse, use document reversal wizard """
cancel_reversal = all(
self.mapped('move_line_ids.move_id.journal_id.is_cancel_reversal'))
states = self.mapped('state')
if cancel_reversal and 'draft' not in states:
return self.reverse_document_wizard()
return super().cancel()
@api.multi
def action_document_reversal(self, date=None, journal_id=None):
""" Reverse all moves related to this payment + set state to cancel """
# Check document state
if 'cancelled' in self.mapped('state'):
raise ValidationError(
_('You are trying to cancel the cancelled document'))
move_lines = self.mapped('move_line_ids')
moves = move_lines.mapped('move_id')
# Set all moves to unreconciled
move_lines.filtered(lambda x:
x.account_id.reconcile).remove_move_reconcile()
# Important to remove relation with move.line before reverse
move_lines.write({'payment_id': False})
# Create reverse entries
moves.reverse_moves(date, journal_id)
# Set state cancelled and unlink with account.move
self.write({'move_name': False,
'state': 'cancelled'})
return True

View File

@@ -0,0 +1,4 @@
To use document reversal, setup the document's journal as following,
- Allow Cancelling = True
- Cancel method = Reversal (create reversed journal entries)

View File

@@ -0,0 +1,2 @@
* Kitti Upariphutthiphong <kittiu@ecosoft.co.th>
* Jordi Ballester <jordi.ballester@eficent.com.com>

View File

@@ -0,0 +1,9 @@
By Odoo standard, when an account document is cancelled, its journal entry will be deleted completely.
This module enhance the process, instead of deletion, it will create new reversed journal entry.
This will help preserved the accounting history, which is strictly required by some country.
Following are documented provide this feature,
- Invoice (account.invoice)
- Payment (acccont.payment)
- Bank Statement (account.bank.statement.line)

View File

@@ -0,0 +1,5 @@
After configure document journal to allow cancel with reversal, it is ready to use.
- Cancel document as normally do, system will show new cancel wizard
- User can select cancel date and new journal (if different from the document)
which will be used for the reversed journal entry

View File

@@ -0,0 +1,449 @@
<?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 Document Reversal</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-document-reversal">
<h1 class="title">Account Document Reversal</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><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_document_reversal"><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_document_reversal"><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>By Odoo standard, when an account document is cancelled, its journal entry will be deleted completely.
This module enhance the process, instead of deletion, it will create new reversed journal entry.
This will help preserved the accounting history, which is strictly required by some country.</p>
<p>Following are documented provide this feature,</p>
<ul class="simple">
<li>Invoice (account.invoice)</li>
<li>Payment (acccont.payment)</li>
<li>Bank Statement (account.bank.statement.line)</li>
</ul>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#configuration" id="id1">Configuration</a></li>
<li><a class="reference internal" href="#usage" id="id2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="id3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="id4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="id5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="id6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="id7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="configuration">
<h1><a class="toc-backref" href="#id1">Configuration</a></h1>
<p>To use document reversal, setup the documents journal as following,</p>
<ul class="simple">
<li>Allow Cancelling = True</li>
<li>Cancel method = Reversal (create reversed journal entries)</li>
</ul>
</div>
<div class="section" id="usage">
<h1><a class="toc-backref" href="#id2">Usage</a></h1>
<p>After configure document journal to allow cancel with reversal, it is ready to use.</p>
<ul class="simple">
<li>Cancel document as normally do, system will show new cancel wizard</li>
<li>User can select cancel date and new journal (if different from the document)
which will be used for the reversed journal entry</li>
</ul>
</div>
<div class="section" id="bug-tracker">
<h1><a class="toc-backref" href="#id3">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_document_reversal%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="#id4">Credits</a></h1>
<div class="section" id="authors">
<h2><a class="toc-backref" href="#id5">Authors</a></h2>
<ul class="simple">
<li>Ecosoft</li>
</ul>
</div>
<div class="section" id="contributors">
<h2><a class="toc-backref" href="#id6">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>
<li>Jordi Ballester &lt;<a class="reference external" href="mailto:jordi.ballester&#64;eficent.com.com">jordi.ballester&#64;eficent.com.com</a>&gt;</li>
</ul>
</div>
<div class="section" id="maintainers">
<h2><a class="toc-backref" href="#id7">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>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p>
<p><a class="reference external" href="https://github.com/kittiu"><img alt="kittiu" src="https://github.com/kittiu.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-financial-tools/tree/12.0/account_document_reversal">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 (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
from . import test_invoice_reversal
from . import test_payment_reversal

View File

@@ -0,0 +1,78 @@
# 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
class TestInvoiceReversal(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestInvoiceReversal, cls).setUpClass()
cls.partner = cls.env['res.partner'].create({'name': 'Test'})
cls.account_type_receivable = cls.env['account.account.type'].create({
'name': 'Test Receivable',
'type': 'receivable',
})
cls.account_type_regular = cls.env['account.account.type'].create({
'name': 'Test Regular',
'type': 'other',
})
cls.account_receivable = cls.env['account.account'].create({
'name': 'Test Receivable',
'code': 'TEST_AR',
'user_type_id': cls.account_type_receivable.id,
'reconcile': True,
})
cls.account_income = cls.env['account.account'].create({
'name': 'Test Income',
'code': 'TEST_IN',
'user_type_id': cls.account_type_regular.id,
'reconcile': False,
})
cls.sale_journal = cls.env['account.journal'].\
search([('type', '=', 'sale')])[0]
cls.invoice = cls.env['account.invoice'].create({
'name': "Test Customer Invoice",
'journal_id': cls.sale_journal.id,
'partner_id': cls.partner.id,
'account_id': cls.account_receivable.id,
})
cls.invoice_line = cls.env['account.invoice.line']
cls.invoice_line1 = cls.invoice_line.create({
'invoice_id': cls.invoice.id,
'name': 'Line 1',
'price_unit': 200.0,
'account_id': cls.account_income.id,
'quantity': 1,
})
def test_journal_invoice_cancel_reversal(self):
""" Tests cancel with reversal, end result must follow,
- Reversal journal entry is created, and reconciled with original entry
- Status is changed to cancel
"""
# Test journal
self.assertFalse(self.sale_journal.is_cancel_reversal)
self.sale_journal.write({'update_posted': True,
'cancel_method': 'reversal'})
# Open invoice
self.invoice.action_invoice_open()
move = self.invoice.move_id
# Click Cancel will open reverse document wizard
res = self.invoice.action_invoice_cancel()
self.assertEqual(res['res_model'], 'reverse.account.document')
# Cancel invoice
ctx = {'active_model': 'account.invoice',
'active_ids': [self.invoice.id]}
f = Form(self.env[res['res_model']].with_context(ctx))
cancel_wizard = f.save()
cancel_wizard.action_cancel()
reversed_move = move.reverse_entry_id
move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id')
reversed_move_reconcile = \
reversed_move.mapped('line_ids').mapped('full_reconcile_id')
# Check
self.assertTrue(move_reconcile)
self.assertTrue(reversed_move_reconcile)
self.assertEqual(move_reconcile, reversed_move_reconcile)
self.assertEqual(self.invoice.state, 'cancel')

View File

@@ -0,0 +1,343 @@
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
import time
from odoo.tests.common import SavepointCase, Form
from odoo.exceptions import UserError
class TestPaymentReversal(SavepointCase):
@classmethod
def setUpClass(cls):
super(TestPaymentReversal, cls).setUpClass()
# Models
cls.acc_bank_stmt_model = cls.env['account.bank.statement']
cls.acc_bank_stmt_line_model = cls.env['account.bank.statement.line']
cls.partner = cls.env['res.partner'].create({'name': 'Test'})
cls.account_account_type_model = cls.env['account.account.type']
cls.account_account_model = cls.env['account.account']
cls.account_journal_model = cls.env['account.journal']
cls.account_invoice_model = cls.env['account.invoice']
cls.account_move_line_model = cls.env['account.move.line']
cls.invoice_line_model = cls.env['account.invoice.line']
# Records
cls.account_type_bank = cls.account_account_type_model.create({
'name': 'Test Bank',
'type': 'liquidity',
})
cls.account_type_receivable = cls.account_account_type_model.create({
'name': 'Test Receivable',
'type': 'receivable',
})
cls.account_type_regular = cls.account_account_type_model.create({
'name': 'Test Regular',
'type': 'other',
})
cls.account_bank = cls.account_account_model.create({
'name': 'Test Bank',
'code': 'TEST_BANK',
'user_type_id': cls.account_type_bank.id,
'reconcile': False,
})
cls.account_receivable = cls.account_account_model.create({
'name': 'Test Receivable',
'code': 'TEST_AR',
'user_type_id': cls.account_type_receivable.id,
'reconcile': True,
})
cls.account_income = cls.account_account_model.create({
'name': 'Test Income',
'code': 'TEST_IN',
'user_type_id': cls.account_type_regular.id,
'reconcile': False,
})
cls.account_expense = cls.account_account_model.create({
'name': 'Test Expense',
'code': 'TEST_EX',
'user_type_id': cls.account_type_regular.id,
'reconcile': False,
})
cls.bank_journal = cls.account_journal_model.create({
'name': 'Test Bank',
'code': 'TBK',
'type': 'bank'
})
cls.sale_journal = cls.account_journal_model.\
search([('type', '=', 'sale')])[0]
cls.invoice = cls.account_invoice_model.create({
'name': "Test Customer Invoice",
'journal_id': cls.sale_journal.id,
'partner_id': cls.partner.id,
'account_id': cls.account_receivable.id,
})
cls.invoice_line1 = cls.invoice_line_model.create({
'invoice_id': cls.invoice.id,
'name': 'Line 1',
'price_unit': 200.0,
'account_id': cls.account_income.id,
'quantity': 1,
})
def test_payment_cancel_normal(self):
""" Tests that, if I don't use cancel reversal,
I can create an invoice, pay it and then cancel as normal. I expect:
- account move are removed completely
"""
# Test journal with normal cancel
self.bank_journal.write({'update_posted': True,
'cancel_method': 'normal'})
# Open invoice
self.invoice.action_invoice_open()
# Pay invoice
self.invoice.pay_and_reconcile(self.bank_journal, 200.0)
payment = self.invoice.payment_ids[0]
payment.cancel()
move_lines = self.env['account.move.line'].\
search([('payment_id', '=', payment.id)])
# All account moves are removed completely
self.assertFalse(move_lines)
def test_payment_cancel_reversal(self):
""" Tests that if I use cancel reversal, I can create an invoice,
pay it and then cancel the payment. I expect:
- Reversal journal entry is created, and reconciled with original entry
- Status of the payment is changed to cancel
- The invoice is not reconciled with the payment anymore
"""
# Test journal
self.bank_journal.write({'update_posted': True,
'cancel_method': 'reversal'})
# Open invoice
self.invoice.action_invoice_open()
# Pay invoice
self.invoice.pay_and_reconcile(self.bank_journal, 200.0)
payment = self.invoice.payment_ids[0]
move = self.env['account.move.line'].search(
[('payment_id', '=', payment.id)], limit=1).move_id
res = payment.cancel()
# Cancel payment
ctx = {'active_model': 'account.payment',
'active_ids': [payment.id]}
f = Form(self.env[res['res_model']].with_context(ctx))
self.assertEqual(res['res_model'], 'reverse.account.document')
cancel_wizard = f.save()
cancel_wizard.action_cancel()
payment_moves = self.env['account.move.line'].search(
[('payment_id', '=', payment.id)])
self.assertFalse(payment_moves)
reversed_move = move.reverse_entry_id
move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id')
reversed_move_reconcile = \
reversed_move.mapped('line_ids').mapped('full_reconcile_id')
# Check
self.assertTrue(move_reconcile)
self.assertTrue(reversed_move_reconcile)
self.assertEqual(move_reconcile, reversed_move_reconcile)
self.assertEqual(payment.state, 'cancelled')
self.assertEqual(self.invoice.state, 'open')
def test_bank_statement_cancel_normal(self):
""" Tests that, if I don't use cancel reversal,
I can create an invoice, pay it via a bank statement
line and then cancel the bank statement line as normal. I expect:
- account move are removed completely
"""
# Test journal with normal cancel
self.bank_journal.write({'update_posted': True,
'cancel_method': 'normal'})
# Open invoice
self.invoice.action_invoice_open()
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': self.bank_journal.id,
'date': time.strftime('%Y') + '-07-15',
'name': 'payment' + self.invoice.name
})
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner.id,
'amount': 200,
'date': time.strftime('%Y') + '-07-15', })
line_id = self.account_move_line_model
# reconcile the payment with the invoice
for l in self.invoice.move_id.line_ids:
if l.account_id.id == self.account_receivable.id:
line_id = l
break
bank_stmt_line.process_reconciliation(counterpart_aml_dicts=[{
'move_line': line_id,
'account_id': self.account_income.id,
'debit': 0.0,
'credit': 200.0,
'name': 'test_reconciliation',
}])
self.assertTrue(bank_stmt_line.journal_entry_ids)
original_move_lines = bank_stmt_line.journal_entry_ids
self.assertTrue(original_move_lines.mapped('statement_id'))
# Cancel the statement line
bank_stmt_line.button_cancel_reconciliation()
move_lines = self.env['account.move.line'].\
search([('statement_id', '=', bank_stmt.id)])
# All account moves are removed completely
self.assertFalse(move_lines)
def test_bank_statement_cancel_reversal_01(self):
""" Tests that I can create an invoice, pay it via a bank statement
line and then reverse the bank statement line. I expect:
- Reversal journal entry is created, and reconciled with original entry
- Payment is deleted
- The invoice is not reconciled with the payment anymore
- The line in the statement is ready to reconcile again
"""
# Test journal
self.bank_journal.write({'update_posted': True,
'cancel_method': 'reversal'})
# Open invoice
self.invoice.action_invoice_open()
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': self.bank_journal.id,
'date': time.strftime('%Y') + '-07-15',
'name': 'payment' + self.invoice.name
})
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner.id,
'amount': 200,
'date': time.strftime('%Y') + '-07-15', })
line_id = self.account_move_line_model
# reconcile the payment with the invoice
for l in self.invoice.move_id.line_ids:
if l.account_id.id == self.account_receivable.id:
line_id = l
break
bank_stmt_line.process_reconciliation(counterpart_aml_dicts=[{
'move_line': line_id,
'account_id': self.account_income.id,
'debit': 0.0,
'credit': 200.0,
'name': 'test_reconciliation',
}])
self.assertTrue(bank_stmt_line.journal_entry_ids)
original_move_lines = bank_stmt_line.journal_entry_ids
original_payment_id = original_move_lines.mapped('payment_id').id
self.assertTrue(original_move_lines.mapped('statement_id'))
# Cancel the statement line
res = bank_stmt_line.button_cancel_reconciliation()
ctx = {'active_model': 'account.bank.statement.line',
'active_ids': [bank_stmt_line.id]}
f = Form(self.env[res['res_model']].with_context(ctx))
self.assertEqual(res['res_model'], 'reverse.account.document')
cancel_wizard = f.save()
cancel_wizard.action_cancel()
self.assertFalse(bank_stmt_line.journal_entry_ids)
payment = self.env['account.payment'].search(
[('id', '=', original_payment_id)],
limit=1)
self.assertFalse(payment)
self.assertFalse(original_move_lines.mapped('statement_id'))
move = original_move_lines[0].move_id
reversed_move = move.reverse_entry_id
move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id')
reversed_move_reconcile = \
reversed_move.mapped('line_ids').mapped('full_reconcile_id')
# Check
self.assertTrue(move_reconcile)
self.assertTrue(reversed_move_reconcile)
self.assertEqual(move_reconcile, reversed_move_reconcile)
def test_bank_statement_cancel_reversal_02(self):
""" Tests that I can create a bank statement line and reconcile it
to an expense account, and then reverse the reconciliation of the
statement line. I expect:
- Reversal journal entry is created, and reconciled with original entry
- Payment is deleted
- The line in the statement is ready to reconcile again
"""
# Test journal
self.bank_journal.write({'update_posted': True,
'cancel_method': 'reversal'})
# Create a bank statement
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': self.bank_journal.id,
'date': time.strftime('%Y') + '-07-15',
'name': 'payment' + self.invoice.name
})
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner.id,
'amount': 200,
'date': time.strftime('%Y') + '-07-15', })
line_id = self.account_move_line_model
bank_stmt_line.process_reconciliation(new_aml_dicts=[{
'move_line': line_id,
'account_id': self.account_expense.id,
'debit': 200.0,
'name': 'test_expense_reconciliation',
}])
self.assertTrue(bank_stmt_line.journal_entry_ids)
original_move_lines = bank_stmt_line.journal_entry_ids
original_payment_id = original_move_lines.mapped('payment_id').id
self.assertTrue(original_move_lines.mapped('statement_id'))
# Cancel the statement line
res = bank_stmt_line.button_cancel_reconciliation()
ctx = {'active_model': 'account.bank.statement.line',
'active_ids': [bank_stmt_line.id]}
f = Form(self.env[res['res_model']].with_context(ctx))
self.assertEqual(res['res_model'], 'reverse.account.document')
cancel_wizard = f.save()
cancel_wizard.action_cancel()
self.assertFalse(bank_stmt_line.journal_entry_ids)
payment = self.env['account.payment'].search(
[('id', '=', original_payment_id)],
limit=1)
self.assertFalse(payment)
self.assertFalse(original_move_lines.mapped('statement_id'))
move = original_move_lines[0].move_id
reversed_move = move.reverse_entry_id
move_reconcile = move.mapped('line_ids').mapped('full_reconcile_id')
reversed_move_reconcile = \
reversed_move.mapped('line_ids').mapped('full_reconcile_id')
# Check
self.assertTrue(move_reconcile)
self.assertTrue(reversed_move_reconcile)
self.assertEqual(move_reconcile, reversed_move_reconcile)
def test_bank_statement_cancel_exception(self):
""" Tests on exception case, if statement is already validated, but
user cancel statement line. I expect:
- UserError will show
"""
# Test journal
self.bank_journal.write({'update_posted': True,
'cancel_method': 'reversal'})
# Create a bank statement
bank_stmt = self.acc_bank_stmt_model.create({
'journal_id': self.bank_journal.id,
'date': time.strftime('%Y') + '-07-15',
'name': 'payment' + self.invoice.name
})
bank_stmt_line = self.acc_bank_stmt_line_model.create(
{'name': 'payment',
'statement_id': bank_stmt.id,
'partner_id': self.partner.id,
'amount': 200,
'date': time.strftime('%Y') + '-07-15', })
line_id = self.account_move_line_model
bank_stmt_line.process_reconciliation(new_aml_dicts=[{
'move_line': line_id,
'account_id': self.account_expense.id,
'debit': 200.0,
'name': 'test_expense_reconciliation',
}])
bank_stmt.balance_end_real = 200.00
bank_stmt.check_confirm_bank()
self.assertEqual(bank_stmt.state, 'confirm')
with self.assertRaises(UserError):
bank_stmt_line.button_cancel_reconciliation()

View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_account_journal_form_inherit" model="ir.ui.view">
<field name="name">account.journal.form</field>
<field name="model">account.journal</field>
<field name="inherit_id" ref="account_cancel.view_account_journal_form_inherit"/>
<field name="arch" type="xml">
<field name="update_posted" position="after">
<field name="cancel_method" groups="base.group_no_one" widget="radio"
attrs="{'invisible': [('update_posted', '=', False)]}"/>
</field>
</field>
</record>
</data>
</odoo>

View File

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

View File

@@ -0,0 +1,28 @@
# 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
class ReverseAccountDocument(models.TransientModel):
"""
Document reversal wizard, it cancel by reverse document journal entries
"""
_name = 'reverse.account.document'
_description = 'Account Document Reversal'
date = fields.Date(
string='Reversal date',
default=fields.Date.context_today,
required=True)
journal_id = fields.Many2one(
'account.journal',
string='Use Specific Journal',
help='If empty, uses the journal of the journal entry to be reversed.')
@api.multi
def action_cancel(self):
model = self._context.get('active_model')
active_ids = self._context.get('active_ids')
documents = self.env[model].browse(active_ids)
documents.action_document_reversal(self.date, self.journal_id)
return {'type': 'ir.actions.act_window_close'}

View File

@@ -0,0 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_reverse_account_document" model="ir.ui.view">
<field name="name">reverse.account.document.form</field>
<field name="model">reverse.account.document</field>
<field name="arch" type="xml">
<form string="Document Cancel">
<group>
<group>
<field name="date"/>
</group>
<group>
<field name="journal_id"/>
</group>
</group>
<footer>
<button string="Cancel" name="action_cancel" type="object" class="btn-primary"/>
<button string="Discard" class="btn-secondary" special="cancel"/>
</footer>
</form>
</field>
</record>
<record id="action_view_reverse_account_document" model="ir.actions.act_window">
<field name="name">Document Cancel</field>
<field name="res_model">reverse.account.document</field>
<field name="view_type">form</field>
<field name="view_mode">tree,form</field>
<field name="view_id" ref="view_reverse_account_document"/>
<field name="target">new</field>
</record>
</data>
</odoo>