Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 145 additions & 0 deletions budget_control_stock/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
=======================
Budget Control on Stock
=======================

..
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
!! source digest: sha256:4555fce4ea6a0cee91c8b799f9e6ff12636e3f539375ebea491e3465d9270356
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png
:target: https://odoo-community.org/page/development-status
:alt: Alpha
.. |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-ecosoft--odoo%2Fbudgeting-lightgray.png?logo=github
:target: https://github.com/ecosoft-odoo/budgeting/tree/18.0/budget_control_stock
:alt: ecosoft-odoo/budgeting

|badge1| |badge2| |badge3|

This module adds budget control on stock operations, so that goods
issued from inventory consume budget independently from the related
purchase or sale documents.

When an outgoing transfer (delivery) of an operation type with
``Commit Budget`` enabled is confirmed, a ``stock.budget.move``
commitment is created per analytic account. The committed amount is the
move quantity valued at the configured price source: the product
standard price (default) or the lot standard price. When the transfer is
validated, the stock journal entry is posted and recorded as budget
*actual* (from the stock valuation value), while the original stock
commitment is released, so the budget consumption moves smoothly from
*commitment* to *actual*. Cancelling or reverting the transfer removes
the commitment.

The committed amount can be reviewed on the *Budget Commitment* tab of
the stock transfer and on the budget monitoring report.

Open stock commitments (transfers confirmed but not yet validated at the
end of a budget period) are carried forward to the next period together
with the other commitment types of the standard *Budget Commit Forward*
document.

.. IMPORTANT::
This is an alpha version, the data model and design can change at any time without warning.
Only for development or testing purpose, do not use in production.
`More details on development status <https://odoo-community.org/page/development-status>`_

**Table of contents**

.. contents::
:local:

Configuration
=============

To configure this module, you need to:

#. Go to *Inventory > Configuration > Operation Types*. #. Open the
operation type that issues goods from inventory (for example *Delivery
Orders*) and enable ``Commit Budget``. Confirming a transfer of this
operation type will then create budget commitments. #. Choose the
``Budget Price Source`` used to value the commitment:

- *Product Standard Price* (default): use ``product.standard_price``.
- *Lot Standard Price*: use ``lot.standard_price`` per reserved lot, for
FIFO / lot-based costing.

#. Go to *Budgeting > Configuration > Budget Periods*, open the relevant
period and tick ``On Stock`` to control the budget against stock
commitments. When ``Control Budget`` is enabled the flag follows it by
default.

Notes:

- Stock moves must carry an analytic distribution (provided by the
*stock_analytic* dependency); commitments are created per analytic
account.
- Recording the related stock journal entry as budget *actual* relies on
the standard journal-entry budget flow, so make sure the stock journal
entries carry the analytic distribution of the moves.

Usage
=====

To use this module, you need to:

#. Make sure the operation type and the budget period are configured
(see *Configuration*) and that the budget control sheet of the analytic
account is in *Controlled* status. #. Create an outgoing transfer
(delivery) whose stock moves carry an analytic distribution. #. Confirm
the transfer. For each analytic account, a budget commitment is created
from the configured price source (product or lot standard price). If the
budget is not sufficient and the period blocks over-budget transactions,
confirmation is refused. #. Review the committed amount on the *Budget
Commitment* tab of the transfer, on the budget control sheet (``Stock``
column) or on the budget monitoring report. #. Validate the transfer.
The stock journal entry is posted and recorded as budget *actual*, and
the matching stock commitment is released, so consumption moves from
*commitment* to *actual*.

Cancelling, reverting to draft or returning the transfer removes the
related commitment.

Bug Tracker
===========

Bugs are tracked on `GitHub Issues <https://github.com/ecosoft-odoo/budgeting/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/ecosoft-odoo/budgeting/issues/new?body=module:%20budget_control_stock%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
-------

* Ecosoft

Contributors
------------

- Saran Lim. <saranl@ecosoft.co.th>

Maintainers
-----------

.. |maintainer-Saran440| image:: https://github.com/Saran440.png?size=40px
:target: https://github.com/Saran440
:alt: Saran440

Current maintainer:

|maintainer-Saran440|

This module is part of the `ecosoft-odoo/budgeting <https://github.com/ecosoft-odoo/budgeting/tree/18.0/budget_control_stock>`_ project on GitHub.

You are welcome to contribute.
4 changes: 4 additions & 0 deletions budget_control_stock/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from . import models
from . import report
23 changes: 23 additions & 0 deletions budget_control_stock/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Copyright 2026 Ecosoft Co., Ltd. (http://ecosoft.co.th)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Budget Control on Stock",
"version": "18.0.1.0.0",
"license": "AGPL-3",
"author": "Ecosoft, Odoo Community Association (OCA)",
"website": "https://github.com/ecosoft-odoo/budgeting",
"depends": ["budget_control", "stock_account", "stock_analytic"],
"data": [
"security/ir.model.access.csv",
"views/stock_budget_move_view.xml",
"views/stock_picking_type_views.xml",
"views/stock_picking_view.xml",
"views/budget_period_view.xml",
"views/budget_control_view.xml",
# "views/budget_commit_forward_view.xml",
],
"installable": True,
"maintainers": ["Saran440"],
"development_status": "Alpha",
}
19 changes: 19 additions & 0 deletions budget_control_stock/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

# Master Data
from . import budget_period

# Operation Data
from . import budget_control
from . import budget_commit_forward

# Account Module
from . import account_budget_move
from . import account_move
from . import account_move_line

# Stock Module
from . import stock_budget_move
from . import stock_picking_type
from . import stock_picking
from . import stock_move
21 changes: 21 additions & 0 deletions budget_control_stock/models/account_budget_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Copyright 2026 Ecosoft Co., Ltd. (http://ecosoft.co.th)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import api, models


class AccountBudgetMove(models.Model):
_inherit = "account.budget.move"

@api.depends("move_id")
def _compute_source_document(self):
res = super()._compute_source_document()
for rec in self.filtered(lambda r: r.move_line_id.stock_valuation_layer_ids):
if rec.source_document:
continue
stock_moves = rec.move_line_id.stock_valuation_layer_ids.mapped(
"stock_move_id"
)
if stock_moves and stock_moves[0].picking_id:
rec.source_document = stock_moves[0].picking_id.display_name
return res
20 changes: 20 additions & 0 deletions budget_control_stock/models/account_move.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2026 Ecosoft Co., Ltd. (http://ecosoft.co.th)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import models


class AccountMove(models.Model):
_inherit = "account.move"

def write(self, vals):
"""Recompute stock commit when JE state changes.

recompute_budget_move handles both re-commit and uncommit
(via _recommit_via_valuation_lines) in one pass.
"""
res = super().write(vals)
if vals.get("state") in ("draft", "posted", "cancel"):
stock_moves = self.mapped("stock_valuation_layer_ids.stock_move_id")
stock_moves.recompute_budget_move()
return res
84 changes: 84 additions & 0 deletions budget_control_stock/models/account_move_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# Copyright 2026 Ecosoft Co., Ltd. (http://ecosoft.co.th)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import models


class AccountMoveLine(models.Model):
_inherit = "account.move.line"

def _condition_skip_uncommit_stock(self, move):
"""Skip if this account move line is not related to a stock valuation."""
return not move.stock_valuation_layer_ids

def _get_lot_uncommit_context(self, move):
"""Return context for lot-based uncommit.

Each stock valuation JE links to exactly one SVL (one per serial/lot).
Always scope the uncommit to the per-SVL qty so cancelling one JE
restores only that lot's commitment, not the full move qty.
For lot_price source, also override the unit price from the lot.
"""
svls = move.stock_valuation_layer_ids
if not svls:
return {}
svl = svls[0]
if not svl.lot_id:
return {}
stock_move = svl.stock_move_id
ctx = {"product_qty": abs(svl.quantity)}
if stock_move.picking_id.picking_type_id.budget_price_source == "lot_price":
ctx["budget_lot_price"] = svl.lot_id.standard_price
return ctx

def uncommit_stock_budget(self):
"""Uncommit the budget for related stock moves
when the stock valuation entry is in a valid state."""
AnalyticAccount = self.env["account.analytic.account"]

for ml in self:
move = ml.move_id
# Not related to stock valuation
if self._condition_skip_uncommit_stock(move):
continue

# Get the stock.move from the valuation layer
stock_moves = move.stock_valuation_layer_ids.mapped("stock_move_id")
if not stock_moves:
continue

if move.state == "posted":
stock_moves = stock_moves.filtered(
lambda m: m.amount_commit
and any(v > 0 for v in m.amount_commit.values())
)
if not stock_moves:
# 2-step delivery: SVL is on OUT move (no budget_commit),
# but the commit is on the upstream PICK move.
svl_moves = move.stock_valuation_layer_ids.mapped("stock_move_id")
stock_moves = svl_moves.mapped("move_orig_ids").filtered(
lambda m: m.amount_commit
and any(v > 0 for v in m.amount_commit.values())
)
if not stock_moves:
continue

if ml.analytic_distribution:
analytic_accounts = {
int(aid): AnalyticAccount.browse(int(aid))
for aid in ml.analytic_distribution
}
lot_ctx = self._get_lot_uncommit_context(move)
for analytic_id, _ in ml.analytic_distribution.items():
for stock_move in stock_moves:
stock_move.with_context(**lot_ctx).commit_budget(
reverse=True,
account_move_line_id=ml.id,
date=ml.date_commit,
analytic_account_id=analytic_accounts[int(analytic_id)],
)
else: # Cancel or draft, not commitment line
StockMove = self.env["stock.move"]
self.env[StockMove._budget_model()].search(
[("account_move_line_id", "=", ml.id)]
).unlink()
53 changes: 53 additions & 0 deletions budget_control_stock/models/budget_commit_forward.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2026 Ecosoft Co., Ltd. (http://ecosoft.co.th)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import fields, models


class BudgetCommitForward(models.Model):
_inherit = "budget.commit.forward"

stock = fields.Boolean(
default=True,
help="If checked, click review budget commitment will pull stock commitment",
)
forward_stock_ids = fields.One2many(
comodel_name="budget.commit.forward.line",
inverse_name="forward_id",
string="Stock Moves",
domain=[("res_model", "=", "stock.move")],
)

def _get_budget_docline_model(self):
res = super()._get_budget_docline_model()
if self.stock:
res.append("stock.move")
return res

def _get_document_number(self, doc):
if doc._name == "stock.move":
return f"{doc.picking_id._name},{doc.picking_id.id}"
return super()._get_document_number(doc)

def _get_base_domain_extension(self, res_model):
"""For module extension"""
if res_model == "stock.move":
return " AND a.state NOT IN ('cancel', 'draft')"
return super()._get_base_domain_extension(res_model)


class BudgetCommitForwardLine(models.Model):
_inherit = "budget.commit.forward.line"

res_model = fields.Selection(
selection_add=[("stock.move", "Stock Move")],
ondelete={"stock.move": "cascade"},
)
document_id = fields.Reference(
selection_add=[("stock.move", "Stock Move")],
ondelete={"stock.move": "cascade"},
)
document_number = fields.Reference(
selection_add=[("stock.picking", "Stock Picking")],
ondelete={"stock.picking": "cascade"},
)
14 changes: 14 additions & 0 deletions budget_control_stock/models/budget_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2026 Ecosoft Co., Ltd. (http://ecosoft.co.th)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo import fields, models


class BudgetControl(models.Model):
_inherit = "budget.control"

amount_stock = fields.Monetary(
string="Stock",
compute="_compute_budget_info",
help="Sum of stock amount",
)
Loading
Loading