Initial commit: Odoo 18.0-20251222 extra-addons
Some checks failed
pre-commit / pre-commit (push) Has been cancelled
tests / Detect unreleased dependencies (push) Has been cancelled
tests / test with OCB (push) Has been cancelled
tests / test with Odoo (push) Has been cancelled

This commit is contained in:
tocmo0nlord
2026-03-13 20:43:25 +00:00
parent 36e847a7df
commit adbe430761
9472 changed files with 1265727 additions and 0 deletions

View File

@@ -0,0 +1,137 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association
=================================
Stock account move reset to draft
=================================
..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:60779d5d631e5592fe210325524c179ba0fbec7eda3696e1709bfa23c9d13211
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |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/license-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--invoicing-lightgray.png?logo=github
:target: https://github.com/OCA/account-invoicing/tree/18.0/stock_account_move_reset_to_draft
:alt: OCA/account-invoicing
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/account-invoicing-18-0/account-invoicing-18-0-stock_account_move_reset_to_draft
:alt: Translate me on Weblate
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
:target: https://runboat.odoo-community.org/builds?repo=OCA/account-invoicing&target_branch=18.0
:alt: Try me on Runboat
|badge1| |badge2| |badge3| |badge4| |badge5|
This module allows a vendor bill to be reverted to draft status even
when an SVL (Stock Valuation Layer) record is created due to a price
difference between the receipt and the vendor bill, when inventory from
the receipt has yet to be consumed. When a vendor bill is reset to
draft, as applicable, a new SVL record is generated to offset the
valuation difference that was generated upon the bill's confirmation.
**Table of contents**
.. contents::
:local:
Use Cases / Context
===================
Typically, when a purchase invoice is created at a price different from
the price at which the SVLs of the incoming picking were initially
created and confirmed, SVLs are generated to account for the price
difference. Once this happens, the invoice cannot normally be reverted
to draft due to stock_account restrictions.
Imagine international business settings where exchange rates are updated
daily; this limitation can be problematic. For example, if goods are
received on April 17th and the vendor bill is processed on April 18th
with a different exchange rate, an SVL record is generated, locking the
vendor bill. If a user then realizes a mistake, such as processing the
bill for the incorrect purchase order, they are unable to cancel it.
Usage
=====
1. Create a product category with Costing Method: Average Cost (AVCO) or
First In First Out (FIFO).
2. Create a product linked to the category created before.
3. Create a purchase order and adds a product line with quantity 1 and
price 10.
4. Confirms the purchase order and validates the incoming picking.
5. Creates an invoice from the purchase order.
6. Changes the invoice line price to 12 and confirm the invoice.
7. It is possible to reset the invoice to draft (a new SVL record to
offset the existing SVL difference will be created).
When attempting to reset an invoice to draft for a product that has been
partially or fully consumed, the system will display the following error
message: "The inventory has already been (partially) consumed." In that
case, consider using landed costs to adjust the valuation of the product
as necessary.
Bug Tracker
===========
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-invoicing/issues>`_.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
`feedback <https://github.com/OCA/account-invoicing/issues/new?body=module:%20stock_account_move_reset_to_draft%0Aversion:%2018.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
-------
* Tecnativa
Contributors
------------
- `Tecnativa <https://www.tecnativa.com>`__:
- Víctor Martínez
- Pedro M. Baeza
- ``Quartile <https://www.quartile.co>``\ \_:
- Yoshi Tashiro
- Aung Ko Ko Lin
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-victoralmau| image:: https://github.com/victoralmau.png?size=40px
:target: https://github.com/victoralmau
:alt: victoralmau
Current `maintainer <https://odoo-community.org/page/maintainer-role>`__:
|maintainer-victoralmau|
This module is part of the `OCA/account-invoicing <https://github.com/OCA/account-invoicing/tree/18.0/stock_account_move_reset_to_draft>`_ 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,14 @@
# Copyright 2024 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
{
"name": "Stock account move reset to draft",
"author": "Tecnativa, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/account-invoicing",
"version": "18.0.1.0.0",
# Real dependency is stock_account but we need purchase_stock in tests
"depends": ["purchase_stock"],
"license": "AGPL-3",
"category": "Warehouse Management",
"installable": True,
"maintainers": ["victoralmau"],
}

View File

@@ -0,0 +1,36 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * stock_account_move_reset_to_draft
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-10-11 15:02+0000\n"
"PO-Revision-Date: 2024-10-11 15:02+0000\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
#, python-format
msgid "Inventory valuation records are intertwined for %(line_name)s."
msgstr ""
#. module: stock_account_move_reset_to_draft
#: model:ir.model,name:stock_account_move_reset_to_draft.model_account_move
msgid "Journal Entry"
msgstr "Asiento contable"
#. module: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
#, python-format
msgid "The inventory has already been (partially) consumed for %(line_name)s."
msgstr ""

View File

@@ -0,0 +1,37 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * stock_account_move_reset_to_draft
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2024-11-08 11:06+0000\n"
"Last-Translator: mymage <stefano.consolaro@mymage.it>\n"
"Language-Team: none\n"
"Language: it\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.6.2\n"
#. module: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
#, python-format
msgid "Inventory valuation records are intertwined for %(line_name)s."
msgstr ""
"I record di valutazione dell'inventario sono intrecciati per %(line_name)s."
#. module: stock_account_move_reset_to_draft
#: model:ir.model,name:stock_account_move_reset_to_draft.model_account_move
msgid "Journal Entry"
msgstr "Registrazione contabile"
#. module: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
#, python-format
msgid "The inventory has already been (partially) consumed for %(line_name)s."
msgstr "L'inventario è già stato (parzialmente) consumato per %(line_name)s."

View File

@@ -0,0 +1,33 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * stock_account_move_reset_to_draft
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.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: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
msgid ""
"Inventory valuation records are intertwined for "
"%(line_name)s."
msgstr ""
#. module: stock_account_move_reset_to_draft
#: model:ir.model,name:stock_account_move_reset_to_draft.model_account_move
msgid "Journal Entry"
msgstr ""
#. module: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
msgid "The inventory has already been (partially) consumed for %(line_name)s."
msgstr ""

View File

@@ -0,0 +1,38 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * stock_account_move_reset_to_draft
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 18.0\n"
"Report-Msgid-Bugs-To: \n"
"PO-Revision-Date: 2025-09-10 11:42+0000\n"
"Last-Translator: Tamer Sezgin <tamer.sezgin@gmail.com>\n"
"Language-Team: none\n"
"Language: tr\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.10.4\n"
#. module: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
msgid ""
"Inventory valuation records are intertwined for "
"%(line_name)s."
msgstr ""
"Envanter değerleme kayıtları %(line_name)s "
"için iç içe geçmiştir."
#. module: stock_account_move_reset_to_draft
#: model:ir.model,name:stock_account_move_reset_to_draft.model_account_move
msgid "Journal Entry"
msgstr "Yevmiye Kaydı"
#. module: stock_account_move_reset_to_draft
#. odoo-python
#: code:addons/stock_account_move_reset_to_draft/models/account_move.py:0
msgid "The inventory has already been (partially) consumed for %(line_name)s."
msgstr "Envanter %(line_name)s için zaten (kısmen) tüketildi."

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import account_move

View File

@@ -0,0 +1,74 @@
# Copyright 2024 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import models
from odoo.exceptions import UserError
from odoo.tools.float_utils import float_is_zero
class AccountMove(models.Model):
_inherit = "account.move"
def button_draft(self):
"""If it is a purchase invoice, we will create a new SVL for each line with
the sum of the value in opposite sign.
"""
for rec in self.filtered(lambda rec: rec.is_purchase_document()):
for line in rec.line_ids:
if not line.stock_valuation_layer_ids:
continue
origin_svls = line.stock_valuation_layer_ids.stock_valuation_layer_id
if (
len(
origin_svls.stock_valuation_layer_ids.account_move_line_id.filtered(
lambda x: x.parent_state == "posted"
)
)
> 1
):
raise UserError(
self.env._(
"Inventory valuation records are intertwined for \
%(line_name)s.",
line_name=line.display_name,
)
)
for origin_svl in origin_svls:
if origin_svl.quantity != origin_svl.remaining_qty:
raise UserError(
self.env._(
"The inventory has already been (partially) consumed "
"for %(line_name)s.",
line_name=line.display_name,
)
)
svls = origin_svl.stock_valuation_layer_ids.filtered(
"account_move_line_id"
)
value = sum(svls.mapped("value"))
if not float_is_zero(
value, precision_rounding=line.currency_id.rounding
):
origin_svl.remaining_value -= value
revert_svl = svls[0].copy({"value": -value})
revert_svl._validate_accounting_entries()
product = line.product_id.with_company(line.company_id.id)
if product.cost_method == "average":
product.sudo().with_context(disable_auto_svl=True).write(
{"standard_price": product.value_svl / product.quantity_svl}
)
return super().button_draft()
def _compute_show_reset_to_draft_button(self):
"""Overwrite the value only if it is already posted and with SVLs.
We use the same fields for filtering that account uses for the
show_reset_to_draft_button field.
"""
_self = self.sudo().filtered(
lambda x: not x.restrict_mode_hash_table
and x.state in ("posted", "cancel")
and any(line.stock_valuation_layer_ids for line in x.line_ids)
)
for item in self:
item.show_reset_to_draft_button = True
return super(AccountMove, self - _self)._compute_show_reset_to_draft_button()

View File

@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"

View File

@@ -0,0 +1,12 @@
Typically, when a purchase invoice is created at a price different from
the price at which the SVLs of the incoming picking were initially
created and confirmed, SVLs are generated to account for the price
difference. Once this happens, the invoice cannot normally be reverted
to draft due to stock_account restrictions.
Imagine international business settings where exchange rates are updated
daily; this limitation can be problematic. For example, if goods are
received on April 17th and the vendor bill is processed on April 18th
with a different exchange rate, an SVL record is generated, locking the
vendor bill. If a user then realizes a mistake, such as processing the
bill for the incorrect purchase order, they are unable to cancel it.

View File

@@ -0,0 +1,8 @@
- [Tecnativa](https://www.tecnativa.com):
- Víctor Martínez
- Pedro M. Baeza
- `Quartile <https://www.quartile.co>`_:
- Yoshi Tashiro
- Aung Ko Ko Lin

View File

@@ -0,0 +1,6 @@
This module allows a vendor bill to be reverted to draft status even
when an SVL (Stock Valuation Layer) record is created due to a price
difference between the receipt and the vendor bill, when inventory from
the receipt has yet to be consumed. When a vendor bill is reset to
draft, as applicable, a new SVL record is generated to offset the
valuation difference that was generated upon the bill's confirmation.

View File

@@ -0,0 +1,16 @@
1. Create a product category with Costing Method: Average Cost (AVCO)
or First In First Out (FIFO).
2. Create a product linked to the category created before.
3. Create a purchase order and adds a product line with quantity 1 and
price 10.
4. Confirms the purchase order and validates the incoming picking.
5. Creates an invoice from the purchase order.
6. Changes the invoice line price to 12 and confirm the invoice.
7. It is possible to reset the invoice to draft (a new SVL record to
offset the existing SVL difference will be created).
When attempting to reset an invoice to draft for a product that has been
partially or fully consumed, the system will display the following error
message: "The inventory has already been (partially) consumed." In that
case, consider using landed costs to adjust the valuation of the product
as necessary.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

View File

@@ -0,0 +1,481 @@
<!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: https://docutils.sourceforge.io/" />
<title>README.rst</title>
<style type="text/css">
/*
:Author: David Goodger (goodger@python.org)
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
:Copyright: This stylesheet has been placed in the public domain.
Default cascading style sheet for the HTML output of Docutils.
Despite the name, some widely supported CSS2 features are used.
See https://docutils.sourceforge.io/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: gray; } /* 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, pre.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">
<a class="reference external image-reference" href="https://odoo-community.org/get-involved?utm_source=readme">
<img alt="Odoo Community Association" src="https://odoo-community.org/readme-banner-image" />
</a>
<div class="section" id="stock-account-move-reset-to-draft">
<h1>Stock account move reset to draft</h1>
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:60779d5d631e5592fe210325524c179ba0fbec7eda3696e1709bfa23c9d13211
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
<p><a class="reference external image-reference" 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 image-reference" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/license-AGPL--3-blue.png" /></a> <a class="reference external image-reference" href="https://github.com/OCA/account-invoicing/tree/18.0/stock_account_move_reset_to_draft"><img alt="OCA/account-invoicing" src="https://img.shields.io/badge/github-OCA%2Faccount--invoicing-lightgray.png?logo=github" /></a> <a class="reference external image-reference" href="https://translation.odoo-community.org/projects/account-invoicing-18-0/account-invoicing-18-0-stock_account_move_reset_to_draft"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/account-invoicing&amp;target_branch=18.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-Try%20me-875A7B.png" /></a></p>
<p>This module allows a vendor bill to be reverted to draft status even
when an SVL (Stock Valuation Layer) record is created due to a price
difference between the receipt and the vendor bill, when inventory from
the receipt has yet to be consumed. When a vendor bill is reset to
draft, as applicable, a new SVL record is generated to offset the
valuation difference that was generated upon the bills confirmation.</p>
<p><strong>Table of contents</strong></p>
<div class="contents local topic" id="contents">
<ul class="simple">
<li><a class="reference internal" href="#use-cases-context" id="toc-entry-1">Use Cases / Context</a></li>
<li><a class="reference internal" href="#usage" id="toc-entry-2">Usage</a></li>
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-3">Bug Tracker</a></li>
<li><a class="reference internal" href="#credits" id="toc-entry-4">Credits</a><ul>
<li><a class="reference internal" href="#authors" id="toc-entry-5">Authors</a></li>
<li><a class="reference internal" href="#contributors" id="toc-entry-6">Contributors</a></li>
<li><a class="reference internal" href="#maintainers" id="toc-entry-7">Maintainers</a></li>
</ul>
</li>
</ul>
</div>
<div class="section" id="use-cases-context">
<h2><a class="toc-backref" href="#toc-entry-1">Use Cases / Context</a></h2>
<p>Typically, when a purchase invoice is created at a price different from
the price at which the SVLs of the incoming picking were initially
created and confirmed, SVLs are generated to account for the price
difference. Once this happens, the invoice cannot normally be reverted
to draft due to stock_account restrictions.</p>
<p>Imagine international business settings where exchange rates are updated
daily; this limitation can be problematic. For example, if goods are
received on April 17th and the vendor bill is processed on April 18th
with a different exchange rate, an SVL record is generated, locking the
vendor bill. If a user then realizes a mistake, such as processing the
bill for the incorrect purchase order, they are unable to cancel it.</p>
</div>
<div class="section" id="usage">
<h2><a class="toc-backref" href="#toc-entry-2">Usage</a></h2>
<ol class="arabic simple">
<li>Create a product category with Costing Method: Average Cost (AVCO) or
First In First Out (FIFO).</li>
<li>Create a product linked to the category created before.</li>
<li>Create a purchase order and adds a product line with quantity 1 and
price 10.</li>
<li>Confirms the purchase order and validates the incoming picking.</li>
<li>Creates an invoice from the purchase order.</li>
<li>Changes the invoice line price to 12 and confirm the invoice.</li>
<li>It is possible to reset the invoice to draft (a new SVL record to
offset the existing SVL difference will be created).</li>
</ol>
<p>When attempting to reset an invoice to draft for a product that has been
partially or fully consumed, the system will display the following error
message: “The inventory has already been (partially) consumed.” In that
case, consider using landed costs to adjust the valuation of the product
as necessary.</p>
</div>
<div class="section" id="bug-tracker">
<h2><a class="toc-backref" href="#toc-entry-3">Bug Tracker</a></h2>
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/account-invoicing/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 to smash it by providing a detailed and welcomed
<a class="reference external" href="https://github.com/OCA/account-invoicing/issues/new?body=module:%20stock_account_move_reset_to_draft%0Aversion:%2018.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">
<h2><a class="toc-backref" href="#toc-entry-4">Credits</a></h2>
<div class="section" id="authors">
<h3><a class="toc-backref" href="#toc-entry-5">Authors</a></h3>
<ul class="simple">
<li>Tecnativa</li>
</ul>
</div>
<div class="section" id="contributors">
<h3><a class="toc-backref" href="#toc-entry-6">Contributors</a></h3>
<ul class="simple">
<li><a class="reference external" href="https://www.tecnativa.com">Tecnativa</a>:<ul>
<li>Víctor Martínez</li>
<li>Pedro M. Baeza</li>
</ul>
</li>
<li><tt class="docutils literal">Quartile <span class="pre">&lt;https://www.quartile.co&gt;</span></tt>_:<ul>
<li>Yoshi Tashiro</li>
<li>Aung Ko Ko Lin</li>
</ul>
</li>
</ul>
</div>
<div class="section" id="maintainers">
<h3><a class="toc-backref" href="#toc-entry-7">Maintainers</a></h3>
<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 image-reference" href="https://github.com/victoralmau"><img alt="victoralmau" src="https://github.com/victoralmau.png?size=40px" /></a></p>
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/account-invoicing/tree/18.0/stock_account_move_reset_to_draft">OCA/account-invoicing</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>
</div>
</body>
</html>

View File

@@ -0,0 +1,3 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).
from . import test_stock_account_move_reset_to_draft

View File

@@ -0,0 +1,191 @@
# Copyright 2024 Tecnativa - Víctor Martínez
# Copyright 2025 Quartile (https://www.quartile.co)
# License AGPL-3 - See https://www.gnu.org/licenses/agpl-3.0.html
from odoo import Command
from odoo.exceptions import UserError
from odoo.tests import tagged
from odoo.tools import mute_logger
from odoo.addons.base.tests.common import BaseCommon
@tagged("-at_install", "post_install")
class TestStockAccountMoveResetToDraft(BaseCommon):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.category = cls.env["product.category"].create(
{
"name": "Test product",
"property_cost_method": "average",
}
)
cls.product = cls.env["product.product"].create(
{
"name": "Test product",
"categ_id": cls.category.id,
}
)
cls.partner = cls.env["res.partner"].create({"name": "Test partner"})
def create_and_confirm_order(self, price=10, qty=1):
order = self.env["purchase.order"].create(
{
"partner_id": self.partner.id,
"order_line": [
Command.create(
{
"product_id": self.product.id,
"product_qty": qty,
"price_unit": price,
"taxes_id": False,
}
)
],
}
)
order.button_confirm()
return order
def process_picking(self, picking, qty_done=None):
if qty_done is not None:
picking.move_ids_without_package.quantity = qty_done
res = picking.button_validate()
if res is not True:
wizard = (
self.env[res["res_model"]].with_context(**res["context"]).create({})
)
wizard.process()
self.assertEqual(picking.state, "done")
def create_and_post_invoice(self, order, price=10, qty=1):
res_invoice = order.action_create_invoice()
invoice = self.env[res_invoice["res_model"]].browse(res_invoice["res_id"])
self.assertEqual(invoice.state, "draft")
invoice.invoice_date = order.date_approve
invoice.invoice_line_ids.write({"quantity": qty, "price_unit": price})
invoice.action_post()
self.assertEqual(invoice.state, "posted")
return invoice
@mute_logger("odoo.models.unlink")
def test_purchase_order_flow_01(self):
order = self.create_and_confirm_order(price=10, qty=1)
self.process_picking(order.picking_ids)
invoice = self.create_and_post_invoice(order, price=12, qty=1)
# Upon confirmation, a SVL will be created for the difference (2=12-10)
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 1)
svl_1 = invoice.invoice_line_ids.stock_valuation_layer_ids
self.assertEqual(svl_1.value, 2)
self.assertEqual(
sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 2
)
self.assertTrue(invoice.show_reset_to_draft_button)
# Switch to draft, a SVL will be created for the difference
invoice.button_draft()
self.assertEqual(invoice.state, "draft")
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2)
self.assertEqual(svl_1.value, 2)
svl_1_negative = invoice.invoice_line_ids.stock_valuation_layer_ids - svl_1
self.assertEqual(svl_1_negative.value, -2)
self.assertEqual(
sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 0
)
# Confirm again, no new SVLs are generated
invoice.action_post()
self.assertEqual(invoice.state, "posted")
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2)
self.assertEqual(svl_1.value, 2)
self.assertEqual(svl_1_negative.value, -2)
self.assertTrue(invoice.show_reset_to_draft_button)
self.assertEqual(
sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 0
)
# Change to draft and change the price to 10 so that SVL is not generated
invoice.button_draft()
self.assertEqual(invoice.state, "draft")
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2)
self.assertEqual(svl_1.value, 2)
self.assertEqual(svl_1_negative.value, -2)
invoice.invoice_line_ids.price_unit = 10
invoice.action_post()
self.assertEqual(invoice.state, "posted")
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2)
self.assertEqual(svl_1.value, 2)
self.assertEqual(svl_1_negative.value, -2)
self.assertTrue(invoice.show_reset_to_draft_button)
self.assertEqual(
sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 0
)
@mute_logger("odoo.models.unlink")
def test_purchase_order_flow_02(self):
# PO for a product: 2 pcs at EUR10
order = self.create_and_confirm_order(price=10, qty=2)
# Receive 1 pc and create a backorder
picking = order.picking_ids
self.process_picking(picking, qty_done=1)
extra_picking = order.picking_ids - picking
extra_picking.button_validate()
invoice = self.create_and_post_invoice(order, price=12, qty=2)
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 2)
svls_1 = invoice.invoice_line_ids.stock_valuation_layer_ids
self.assertEqual(sum(svls_1.mapped("value")), 4)
self.assertTrue(invoice.show_reset_to_draft_button)
# Reset the bill to draft
invoice.button_draft()
self.assertEqual(invoice.state, "draft")
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 4)
svls_1_negative = invoice.invoice_line_ids.stock_valuation_layer_ids - svls_1
self.assertEqual(sum(svls_1.mapped("value")), 4)
self.assertEqual(sum(svls_1_negative.mapped("value")), -4)
# Change the bill content to 1 pc at EUR15 and post
invoice.invoice_line_ids.write({"quantity": 1, "price_unit": 15})
invoice.action_post()
self.assertEqual(invoice.state, "posted")
self.assertEqual(len(invoice.invoice_line_ids.stock_valuation_layer_ids), 4)
# Create another bill for 1 pc at EUR8 and post
invoice_extra = self.create_and_post_invoice(order, price=8, qty=1)
self.assertEqual(
len(invoice_extra.invoice_line_ids.stock_valuation_layer_ids), 1
)
self.assertEqual(
invoice_extra.invoice_line_ids.stock_valuation_layer_ids.value, -2
)
self.assertTrue(invoice.show_reset_to_draft_button)
# Reset the first bill to draft -> User error to prevent valuation
# inconsistencies
with self.assertRaisesRegex(
UserError, "Inventory valuation records are intertwined"
):
invoice.button_draft()
@mute_logger("odoo.models.unlink")
def test_purchase_order_flow_03(self):
order = self.create_and_confirm_order(price=10, qty=1)
self.process_picking(order.picking_ids)
invoice = self.create_and_post_invoice(order, price=12, qty=1)
self.assertEqual(
sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 2
)
# Manually create an SVL linked to the original SVL.
# This simulates cases such as applying a landed cost to a receipt.
svl = order.picking_ids.move_ids.stock_valuation_layer_ids
self.env["stock.valuation.layer"].create(
{
"product_id": self.product.id,
"value": 5,
"quantity": 1,
"description": "Manual SVL",
"stock_move_id": False,
"stock_valuation_layer_id": svl.id,
"company_id": self.env.company.id,
}
)
invoice.button_draft()
self.assertEqual(invoice.state, "draft")
self.assertEqual(
sum(invoice.invoice_line_ids.mapped("stock_valuation_layer_ids.value")), 0
)