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
83 changes: 83 additions & 0 deletions budget_control_sale_stock/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
=================================
Budget Control on Sale with Stock
=================================

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

.. |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_sale_stock
:alt: ecosoft-odoo/budgeting

|badge1| |badge2| |badge3|

This module is a bridge between budget_control_stock and sale_stock.

When a Sale Order is confirmed, the system auto-creates a Delivery Order
(DO). At this point, the Budget Control may not yet be confirmed (the
user needs to set KPIs first). This module bypasses the stock budget
commit check during SO confirmation so the DO can be created without a
budget error.

After SO confirmation, all subsequent DO operations (validate,
unreserve, etc.) enforce the budget check normally, requiring the user
to confirm the Budget Control before the DO can be processed.

.. 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:

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_sale_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_sale_stock>`_ project on GitHub.

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

from . import models
21 changes: 21 additions & 0 deletions budget_control_sale_stock/__manifest__.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).

{
"name": "Budget Control on Sale with 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",
"sale_stock",
"sale_project",
"sale_margin",
"sale_stock_analytic",
],
"data": ["views/budget_control_views.xml", "views/sale_order_views.xml"],
"installable": True,
"maintainers": ["Saran440"],
"development_status": "Alpha",
}
4 changes: 4 additions & 0 deletions budget_control_sale_stock/models/__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 budget_control
from . import sale_order
56 changes: 56 additions & 0 deletions budget_control_sale_stock/models/budget_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright 2026 Ecosoft Co., Ltd. (<http://ecosoft.co.th>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

from odoo import api, fields, models


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

sale_order_ids = fields.Many2many(
comodel_name="sale.order",
relation="budget_control_sale_order_rel",
column1="budget_control_id",
column2="sale_order_id",
string="Sale Orders",
copy=False,
)
sale_order_count = fields.Integer(compute="_compute_sale_order_count")
sale_price = fields.Float(
compute="_compute_sale_fields",
store=True,
)
gross_profit = fields.Float(
compute="_compute_sale_fields",
store=True,
)
gross_profit_percent = fields.Float(
compute="_compute_sale_fields",
store=True,
)

@api.depends("sale_order_ids")
def _compute_sale_order_count(self):
for rec in self:
rec.sale_order_count = len(rec.sale_order_ids)

@api.depends("sale_order_ids.amount_total", "allocated_amount")
def _compute_sale_fields(self):
for rec in self:
sale_price = sum(rec.sale_order_ids.mapped("amount_total"))
profit = sale_price - rec.allocated_amount
rec.sale_price = sale_price
rec.gross_profit = profit
rec.gross_profit_percent = (
(profit / sale_price * 100) if sale_price else 0.0
)

def action_open_sale_order(self):
self.ensure_one()
return {
"name": self.env._("Sale Orders"),
"type": "ir.actions.act_window",
"res_model": "sale.order",
"view_mode": "list,form",
"domain": [("id", "in", self.sale_order_ids.ids)],
}
116 changes: 116 additions & 0 deletions budget_control_sale_stock/models/sale_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# 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
from odoo.exceptions import UserError
from odoo.fields import Command


class SaleOrder(models.Model):
_inherit = "sale.order"

budget_control_id = fields.Many2one(
comodel_name="budget.control",
index=True,
copy=False,
)

def action_confirm(self):
"""Bypass stock budget commit check during SO confirmation.

When SO is confirmed, the system auto-creates a Delivery Order (DO).
Budget Control may not yet be confirmed at this point (user needs to
set KPIs first). The bypass context prevents the budget check from
failing during DO creation. Subsequent DO operations (validate, etc.)
will enforce the budget check normally, forcing the user to confirm
the Budget Control before proceeding.
"""
return super(
SaleOrder, self.with_context(skip_budget_commit=True)
).action_confirm()

def action_create_budget_control(self):
"""Manual create budget control"""
self.ensure_one()
self._create_project_budget_control()
return True

def _action_confirm(self):
res = super()._action_confirm()
self.filtered(
lambda rec: not rec.budget_control_id
)._create_project_budget_control()
return res

def _create_project_budget_control(self):
BudgetPeriod = self.env["budget.period"]
BudgetControl = self.env["budget.control"]
for order in self:
project = order.project_id
if not project or not project.account_id:
continue
date = order.date_order.date() if order.date_order else fields.Date.today()
budget_period = BudgetPeriod.search(
[("bm_date_from", "<=", date), ("bm_date_to", ">=", date)],
limit=1,
)
if not budget_period:
raise UserError(
self.env._(
f"No budget period found for date {date} "
f"on sale order {order.name}."
)
)
existing = BudgetControl.search(
[
("analytic_account_id", "=", project.account_id.id),
("budget_period_id", "=", budget_period.id),
("state", "!=", "cancel"),
],
limit=1,
)
if existing:
# New SO on same analytic+period: add to existing budget.
# Same SO re-confirmed (reset to draft): skip to avoid
# doubling the allocated amount.
if order not in existing.sale_order_ids:
add_amount = sum(
line.purchase_price * line.product_uom_qty
for line in order.order_line
)
existing.write(
{
"allocated_amount": existing.allocated_amount + add_amount,
"sale_order_ids": [Command.link(order.id)],
}
)
existing.action_draft()
order.budget_control_id = existing.id
else:
vals = order._prepare_budget_control_vals(project, budget_period)
budget_control = BudgetControl.create(vals)
order.budget_control_id = budget_control.id

def _prepare_budget_control_vals(self, project, budget_period):
self.ensure_one()
allocated_amount = sum(
line.purchase_price * line.product_uom_qty for line in self.order_line
)
return {
"name": project.name,
"analytic_account_id": project.account_id.id,
"budget_period_id": budget_period.id,
"plan_date_range_type_id": budget_period.plan_date_range_type_id.id,
"currency_id": self.company_id.currency_id.id,
"allocated_amount": allocated_amount,
"sale_order_ids": [Command.link(self.id)],
}

def action_open_budget_control(self):
self.ensure_one()
return {
"type": "ir.actions.act_window",
"res_model": "budget.control",
"view_mode": "form",
"res_id": self.budget_control_id.id,
}
3 changes: 3 additions & 0 deletions budget_control_sale_stock/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[build-system]
requires = ["whool"]
build-backend = "whool.buildapi"
1 change: 1 addition & 0 deletions budget_control_sale_stock/readme/CONTRIBUTORS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Saran Lim <saranl@ecosoft.co.th>
10 changes: 10 additions & 0 deletions budget_control_sale_stock/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This module is a bridge between budget_control_stock and sale_stock.

When a Sale Order is confirmed, the system auto-creates a Delivery Order (DO).
At this point, the Budget Control may not yet be confirmed (the user needs to
set KPIs first). This module bypasses the stock budget commit check during SO
confirmation so the DO can be created without a budget error.

After SO confirmation, all subsequent DO operations (validate, unreserve, etc.)
enforce the budget check normally, requiring the user to confirm the Budget
Control before the DO can be processed.
Loading
Loading