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
120 changes: 120 additions & 0 deletions purchase_line_sale_history/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
.. image:: https://odoo-community.org/readme-banner-image
:target: https://odoo-community.org/get-involved?utm_source=readme
:alt: Odoo Community Association

==========================
Purchase Line Sale History
==========================

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

.. |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-LGPL--3-blue.png
:target: http://www.gnu.org/licenses/lgpl-3.0-standalone.html
:alt: License: LGPL-3
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fpurchase--workflow-lightgray.png?logo=github
:target: https://github.com/OCA/purchase-workflow/tree/19.0/purchase_line_sale_history
:alt: OCA/purchase-workflow
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
:target: https://translation.odoo-community.org/projects/purchase-workflow-19-0/purchase-workflow-19-0-purchase_line_sale_history
: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/purchase-workflow&target_branch=19.0
:alt: Try me on Runboat

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

This module lets purchasers consult the sales history of a product while
filling a purchase order, without leaving the order form.

Each purchase order line gets a *Sales History* checkbox. When checked,
a pivot table is displayed below the order lines with the quantities of
that product sold per month (columns) and year (rows), based on posted
customer invoices (credit notes are subtracted). Only one line per order
can be active at a time.

The history is computed for the exact product variant of the line, so
different variants of the same product template are never mixed.

**Table of contents**

.. contents::
:local:

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

1. Go to *Purchase > Configuration > Settings*.
2. Set *Sales History Years* to the number of past years to display in
addition to the current one (default: 2, i.e. three year rows in
total).

A sale is any line of a posted customer invoice; quantities from
customer credit notes are subtracted.

Usage
=====

1. Open or create a purchase order.
2. Check the *Sales History* box on the line whose product you want to
analyze. The monthly sales pivot table appears below the order lines.
3. Check the box on another line to switch the table to that product;
the previous checkbox is unchecked automatically.
4. Future months of the current year are displayed as a dash (—),
distinct from months with zero sales.

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

- Add an option to aggregate the history by product template instead of
the exact product variant.
- Consider sourcing the history from sale orders or stock moves as an
alternative to posted customer invoices.

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

Bugs are tracked on `GitHub Issues <https://github.com/OCA/purchase-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/purchase-workflow/issues/new?body=module:%20purchase_line_sale_history%0Aversion:%2019.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
-------

* Jarsa

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

- Jesús Alan Ramos Rodríguez <alan.ramos@jarsa.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.

This module is part of the `OCA/purchase-workflow <https://github.com/OCA/purchase-workflow/tree/19.0/purchase_line_sale_history>`_ project on GitHub.

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.
1 change: 1 addition & 0 deletions purchase_line_sale_history/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from . import models
25 changes: 25 additions & 0 deletions purchase_line_sale_history/__manifest__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copyright 2026 Jarsa
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).
{
"name": "Purchase Line Sale History",
"summary": "Show the sales history of a product while filling a "
"purchase order line",
"version": "19.0.1.0.0",
"category": "Purchases",
"website": "https://github.com/OCA/purchase-workflow",
"author": "Jarsa, Odoo Community Association (OCA)",
"license": "LGPL-3",
"depends": ["purchase"],
"data": [
"views/res_config_settings_views.xml",
"views/purchase_order_views.xml",
],
"assets": {
"web.assets_backend": [
"purchase_line_sale_history/static/src/js/purchase_order_sales_history_field.esm.js",
"purchase_line_sale_history/static/src/xml/purchase_order_sales_history_field.xml",
"purchase_line_sale_history/static/src/scss/purchase_order_sales_history.scss",
],
},
"installable": True,
}
115 changes: 115 additions & 0 deletions purchase_line_sale_history/i18n/es.po
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * purchase_line_sale_history
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 19.0+e\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2026-07-15 00:32+0000\n"
"PO-Revision-Date: 2026-07-15 00:32+0000\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: purchase_line_sale_history
#. odoo-javascript
#: code:addons/purchase_line_sale_history/static/src/xml/purchase_order_sales_history_field.xml:0
msgid ""
"Check the Sales History box on a line to see its product's sales history."
msgstr ""
"Marca la casilla Histórico de ventas en una línea para ver el histórico de "
"ventas de su producto."

#. module: purchase_line_sale_history
#: model:ir.model,name:purchase_line_sale_history.model_res_config_settings
msgid "Config Settings"
msgstr "Ajustes de configuración"

#. module: purchase_line_sale_history
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_purchase_order__display_name
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_purchase_order_line__display_name
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_res_config_settings__display_name
msgid "Display Name"
msgstr "Nombre para mostrar"

#. module: purchase_line_sale_history
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_purchase_order__id
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_purchase_order_line__id
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_res_config_settings__id
msgid "ID"
msgstr "ID"

#. module: purchase_line_sale_history
#: model_terms:ir.ui.view,arch_db:purchase_line_sale_history.res_config_settings_view_form
msgid ""
"Number of past years, in addition to the current one, shown in the sales "
"history of a purchase order line"
msgstr ""
"Número de años anteriores, además del actual, mostrados en el histórico de "
"ventas de una línea de pedido de compra"

#. module: purchase_line_sale_history
#: model:ir.model.fields,help:purchase_line_sale_history.field_res_config_settings__purchase_sales_history_years
msgid ""
"Number of past years, in addition to the current one, shown in the sales "
"history of a purchase order line."
msgstr ""
"Número de años anteriores, además del actual, mostrados en el histórico de "
"ventas de una línea de pedido de compra."

#. module: purchase_line_sale_history
#: model:ir.model.fields,help:purchase_line_sale_history.field_purchase_order__sales_history_line_id
msgid "Order line whose product sales history is displayed."
msgstr "Línea del pedido cuyo histórico de ventas del producto se muestra."

#. module: purchase_line_sale_history
#: model:ir.model,name:purchase_line_sale_history.model_purchase_order
msgid "Purchase Order"
msgstr "Pedido de compra"

#. module: purchase_line_sale_history
#: model:ir.model,name:purchase_line_sale_history.model_purchase_order_line
msgid "Purchase Order Line"
msgstr "Línea de pedido de compra"

#. module: purchase_line_sale_history
#. odoo-javascript
#: code:addons/purchase_line_sale_history/static/src/xml/purchase_order_sales_history_field.xml:0
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_purchase_order_line__show_sales_history
msgid "Sales History"
msgstr "Histórico de ventas"

#. module: purchase_line_sale_history
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_purchase_order__sales_history_line_id
msgid "Sales History Line"
msgstr "Línea del histórico de ventas"

#. module: purchase_line_sale_history
#: model:ir.model.fields,field_description:purchase_line_sale_history.field_res_config_settings__purchase_sales_history_years
msgid "Sales History Years"
msgstr "Años del histórico de ventas"

#. module: purchase_line_sale_history
#: model:ir.model.fields,help:purchase_line_sale_history.field_purchase_order_line__show_sales_history
msgid ""
"Show the sales history of this line's product below the order lines. Only "
"one line per order can be active at a time."
msgstr ""
"Muestra el histórico de ventas del producto de esta línea debajo de las "
"líneas del pedido. Solo una línea por pedido puede estar activa a la vez."

#. module: purchase_line_sale_history
#. odoo-javascript
#: code:addons/purchase_line_sale_history/static/src/xml/purchase_order_sales_history_field.xml:0
msgid "Total"
msgstr "Total"

#. module: purchase_line_sale_history
#. odoo-javascript
#: code:addons/purchase_line_sale_history/static/src/xml/purchase_order_sales_history_field.xml:0
msgid "Year"
msgstr "Año"
3 changes: 3 additions & 0 deletions purchase_line_sale_history/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import purchase_order
from . import purchase_order_line
from . import res_config_settings
100 changes: 100 additions & 0 deletions purchase_line_sale_history/models/purchase_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Copyright 2026 Jarsa
# License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl).

from babel.dates import get_month_names

from odoo import api, fields, models
from odoo.tools.misc import babel_locale_parse, get_lang


class PurchaseOrder(models.Model):
_inherit = "purchase.order"

sales_history_line_id = fields.Many2one(
comodel_name="purchase.order.line",
string="Sales History Line",
copy=False,
help="Order line whose product sales history is displayed.",
)
sales_history_data = fields.Json(
compute="_compute_sales_history_data",
export_string_translation=False,
)

@api.onchange("order_line")
def _onchange_order_line_sales_history(self):
# Line-level onchanges cannot reach sibling lines, so the live
# exclusivity is handled here. sales_history_line_id round-trips
# through the client between onchange calls, so it reliably tells
# which line was active before this change, even before saving.
active = self.order_line.filtered("show_sales_history")
prev = self.sales_history_line_id
# In the onchange environment the lines are NewId records while the
# many2one holds the real id, so compare through _origin.
new = (
active.filtered(lambda line: line != prev and line._origin != prev)[-1:]
or active[:1]
)
(active - new).show_sales_history = False
self.sales_history_line_id = new

@api.depends("sales_history_line_id.product_id", "date_order")
def _compute_sales_history_data(self):
years_back = int(
self.env["ir.config_parameter"]
.sudo()
.get_param("purchase_line_sale_history.years_back", 2)
)
month_names = get_month_names(
"abbreviated", locale=babel_locale_parse(get_lang(self.env).code)
)
months = [month_names[month] for month in range(1, 13)]
for order in self:
line = order.sales_history_line_id
if not line.product_id:
order.sales_history_data = False
continue
order_date = order.date_order or fields.Datetime.now()
current_year = order_date.year
years = [current_year - offset for offset in range(years_back + 1)]
qty_by_month = order._get_sales_history_quantities(
line.product_id, years[-1], current_year
)
data = {}
for year in years:
row = []
for month in range(1, 13):
qty = qty_by_month.get((year, month))
if qty is None:
is_future = year == current_year and month > order_date.month
row.append(None if is_future else 0)
else:
row.append(round(qty, 2))
data[str(year)] = row
order.sales_history_data = {
"product_name": line.product_id.display_name,
"years": years,
"months": months,
"data": data,
}

def _get_sales_history_quantities(self, product, year_from, year_to):
"""Return {(year, month): qty} of posted customer invoice quantities
for ``product``, with credit notes subtracted."""
base_domain = [
("product_id", "=", product.id),
("parent_state", "=", "posted"),
("date", ">=", fields.Date.to_date(f"{year_from}-01-01")),
("date", "<=", fields.Date.to_date(f"{year_to}-12-31")),
]
qty_by_month = {}
for move_type, sign in (("out_invoice", 1), ("out_refund", -1)):
groups = self.env["account.move.line"]._read_group(
base_domain + [("move_id.move_type", "=", move_type)],
groupby=["date:month"],
aggregates=["quantity:sum"],
)
for month_start, qty in groups:
key = (month_start.year, month_start.month)
qty_by_month[key] = qty_by_month.get(key, 0.0) + sign * (qty or 0.0)
return qty_by_month
Loading
Loading