Skip to content
Merged
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
135 changes: 135 additions & 0 deletions sale_semaphore/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
==============
Sale Semaphore
==============

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

.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
:target: https://odoo-community.org/page/development-status
:alt: Beta
.. |badge2| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fsale--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/sale-workflow/tree/18.0/sale_semaphore
:alt: OCA/sale-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/sale-workflow-18-0/sale-workflow-18-0-sale_semaphore
: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/sale-workflow&target_branch=18.0
:alt: Try me on Runboat

|badge1| |badge2| |badge3| |badge4| |badge5|

This module adds a commercial semaphore to help sales teams assess
whether a quoted price stays within the discount policy defined for a
product or product category.

For each product you can define three discount thresholds, expressed as
a percentage over the public sale price:

- **Green**: price is within the preferred commercial margin.
- **Yellow**: price is still acceptable, but already close to the limit.
- **Red**: price is below the warning threshold and should be reviewed.

The semaphore is displayed directly on sales order lines and propagated
to customer invoice lines and reporting models, so the same visual
indicator is available during the full sales flow.

When no product-specific configuration exists, the module can also reuse
the thresholds defined on the product category.

**Table of contents**

.. contents::
:local:

Usage
=====

To use this module:

1. Go to **Sales > Products > Products** and open a product.
2. In the **Sales** tab, enable **Semaphore**.
3. Define the allowed discount percentages for the three thresholds:

- **Discount Success**: upper band for a green result.
- **Discount Warning**: upper band for a yellow result.
- **Discount Danger**: final accepted limit.

4. Optionally, configure the same values on the product category to
provide default thresholds for all products in that category.

Once configured, the semaphore is evaluated automatically on each sales
order line according to the effective unit price:

- **Green** when the price stays above the success threshold.
- **Yellow** when the price falls below the success threshold but
remains above the warning threshold.
- **Red** when the price falls below the warning threshold.

Additional behavior:

- The line is highlighted when the price goes below the **danger**
threshold.
- The semaphore value is copied to invoice lines.
- Non-manager users are prevented from confirming or updating confirmed
sales orders when the effective price is below both the configured
semaphore limit and the applicable pricelist price.
- Product-level settings take precedence over category-level settings.

Known issues / Roadmap
======================

- Propagate category semaphore recursively to child categories
- Use pricelist price as the base for calculations

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/sale-workflow/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/sale-workflow/issues/new?body=module:%20sale_semaphore%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>`__:

- Carlos Dauden
- Carlos Roca

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

This module is maintained by the OCA.

.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://odoo-community.org

OCA, or the Odoo Community Association, is a nonprofit organization whose
mission is to support the collaborative development of Odoo features and
promote its widespread use.

This module is part of the `OCA/sale-workflow <https://github.com/OCA/sale-workflow/tree/18.0/sale_semaphore>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
2 changes: 2 additions & 0 deletions sale_semaphore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
from . import models
from .hook import pre_init_hook
29 changes: 29 additions & 0 deletions sale_semaphore/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Copyright 2025 Tecnativa - Carlos Roca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

{
"name": "Sale Semaphore",
"summary": "Adds a semaphore for commercial purposes",
"version": "18.0.1.0.0",
"category": "Sale",
"author": "Tecnativa, Odoo Community Association (OCA)",
"website": "https://github.com/OCA/sale-workflow",
"license": "AGPL-3",
"depends": ["sale"],
"data": [
"views/account_invoice_report_views.xml",
"views/account_move_views.xml",
"views/product_category_views.xml",
"views/product_views.xml",
"views/sale_order_views.xml",
"views/sale_report_views.xml",
],
"installable": True,
"auto_install": False,
"assets": {
"web.assets_backend": [
"sale_semaphore/static/src/semaphore/*",
],
},
"pre_init_hook": "pre_init_hook",
}
17 changes: 17 additions & 0 deletions sale_semaphore/hook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Copyright 2025 Tecnativa - Carlos Roca
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).


def pre_init_hook(env):
env.cr.execute(
"""
ALTER TABLE sale_order_line
ADD COLUMN IF NOT EXISTS semaphore VARCHAR DEFAULT '';
"""
)
env.cr.execute(
"""
ALTER TABLE sale_order_line
ADD COLUMN IF NOT EXISTS semaphore_active BOOLEAN DEFAULT FALSE;
"""
)
172 changes: 172 additions & 0 deletions sale_semaphore/i18n/es.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * sale_semaphore
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 15.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-11-06 15:17+0000\n"
"PO-Revision-Date: 2025-11-06 16:24+0100\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"X-Generator: Poedit 3.4.2\n"

#. module: sale_semaphore
#: model_terms:ir.ui.view,arch_db:sale_semaphore.product_category_form_view
#: model_terms:ir.ui.view,arch_db:sale_semaphore.product_template_form_view
msgid "Active"
msgstr "Activo"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_product_category__semaphore_discount_danger
#: model:ir.model.fields,field_description:sale_semaphore.field_product_product__semaphore_discount_danger
#: model:ir.model.fields,field_description:sale_semaphore.field_product_template__semaphore_discount_danger
msgid "Discount Danger"
msgstr "Descuento rojo"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_product_category__semaphore_discount_success
#: model:ir.model.fields,field_description:sale_semaphore.field_product_product__semaphore_discount_success
#: model:ir.model.fields,field_description:sale_semaphore.field_product_template__semaphore_discount_success
msgid "Discount Success"
msgstr "Descuento verde"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_product_category__semaphore_discount_warning
#: model:ir.model.fields,field_description:sale_semaphore.field_product_product__semaphore_discount_warning
#: model:ir.model.fields,field_description:sale_semaphore.field_product_template__semaphore_discount_warning
msgid "Discount Warning"
msgstr "Decuento ámbar"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_account_invoice_report
msgid "Invoices Statistics"
msgstr "Estadísticas de facturas"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_account_move_line
msgid "Journal Item"
msgstr "Apunte contable"

#. module: sale_semaphore
#. openerp-web
#: code:addons/sale_semaphore/static/src/semaphore/semaphore.esm.js:0
#, python-format
msgid "Please save to be able to reset the price."
msgstr "Por favor, guarda para poder resetear el precio"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_order__price_below_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_order_line__price_below_semaphore
msgid "Price Below Semaphore"
msgstr "Precio por debajo del semáforo"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_product_product
msgid "Product"
msgstr "Producto"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_product_category
msgid "Product Category"
msgstr "Categoría de producto"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_product_template
msgid "Product Template"
msgstr "Plantilla de producto"

#. module: sale_semaphore
#. openerp-web
#: code:addons/sale_semaphore/static/src/semaphore/semaphore.xml:0
#, python-format
msgid "Reset"
msgstr "Resetear"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_sale_report
msgid "Sales Analysis Report"
msgstr "Informe de análisis de ventas"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_sale_order
msgid "Sales Order"
msgstr "Orden de venta"

#. module: sale_semaphore
#: model:ir.model,name:sale_semaphore.model_sale_order_line
msgid "Sales Order Line"
msgstr "Linea de la orden de venta"

#. module: sale_semaphore
#. openerp-web
#: code:addons/sale_semaphore/static/src/semaphore/semaphore.esm.js:0
#: model:ir.model.fields,field_description:sale_semaphore.field_account_invoice_report__semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_account_move_line__semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_order_line__semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_report__semaphore
#: model_terms:ir.ui.view,arch_db:sale_semaphore.product_category_form_view
#: model_terms:ir.ui.view,arch_db:sale_semaphore.product_template_form_view
#: model_terms:ir.ui.view,arch_db:sale_semaphore.view_account_invoice_report_search
#: model_terms:ir.ui.view,arch_db:sale_semaphore.view_order_product_search
#, python-format
msgid "Semaphore"
msgstr "Semáforo"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_product_category__semaphore_active
#: model:ir.model.fields,field_description:sale_semaphore.field_product_product__semaphore_active
#: model:ir.model.fields,field_description:sale_semaphore.field_product_template__semaphore_active
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_order_line__semaphore_active
msgid "Semaphore Active"
msgstr "Semáforo activo"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_order_line__semaphore_max_price_danger
msgid "Semaphore Max Price Danger"
msgstr "Precio máximo semáforo rojo"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_order_line__semaphore_max_price_success
msgid "Semaphore Max Price Success"
msgstr "Precio máximo semáforo verde"

#. module: sale_semaphore
#: model:ir.model.fields,field_description:sale_semaphore.field_sale_order_line__semaphore_max_price_warning
msgid "Semaphore Max Price Warning"
msgstr "Precio máximo semáforo amarillo"

#. module: sale_semaphore
#. openerp-web
#: code:addons/sale_semaphore/static/src/semaphore/semaphore.esm.js:0
#, python-format
msgid "The product or its category has not the semaphore activated."
msgstr "El producto o su categoría no tiene el semáforo activo"

#. module: sale_semaphore
#. openerp-web
#: code:addons/sale_semaphore/static/src/semaphore/semaphore.esm.js:0
#, python-format
msgid "The semaphore assignation does not work in this model."
msgstr "La asignación de semáforo no funciona en este modelo."

#. module: sale_semaphore
#: code:addons/sale_semaphore/models/sale_order.py:0
#: code:addons/sale_semaphore/models/sale_order_line.py:0
#, python-format
msgid ""
"There's a line with price below semaphore's accepted.\n"
"\n"
"Please set the prices in a way that they are accepted by the semaphore, or "
"contact the purchasing administrators."
msgstr ""
"Hay líneas con precio inferior al semáforo.\n"
"\n"
"Por favor, establece los precios de forma que estén aceptados por el "
"semáforo o contacta con algún administrador de ventas."
Loading
Loading