details:   https://code.tryton.org/tryton/commit/a0b571509e75
branch:    default
user:      Cédric Krier <[email protected]>
date:      Sat Oct 11 10:46:03 2025 +0200
description:
        Add stock inventory and turnover reports
diffstat:

 modules/stock/CHANGELOG                                          |    1 +
 modules/stock/doc/design/index.rst                               |    1 +
 modules/stock/doc/design/reporting.inc.rst                       |   63 +
 modules/stock/move.py                                            |    8 +
 modules/stock/stock.xml                                          |    2 +-
 modules/stock/stock_reporting.py                                 |  526 
++++++++++
 modules/stock/stock_reporting.xml                                |  315 +++++
 modules/stock/tests/scenario_stock_reporting_inventory.json      |    6 +
 modules/stock/tests/scenario_stock_reporting_inventory.rst       |  239 ++++
 modules/stock/tryton.cfg                                         |    8 +
 modules/stock/view/reporting_inventory_context_form.xml          |   14 +
 modules/stock/view/reporting_inventory_daily_form.xml            |   20 +
 modules/stock/view/reporting_inventory_daily_graph.xml           |   11 +
 modules/stock/view/reporting_inventory_daily_list.xml            |   10 +
 modules/stock/view/reporting_inventory_form.xml                  |   10 +
 modules/stock/view/reporting_inventory_list.xml                  |    7 +
 modules/stock/view/reporting_inventory_move_form.xml             |   25 +
 modules/stock/view/reporting_inventory_move_list.xml             |   12 +
 modules/stock/view/reporting_inventory_range_context_form.xml    |   14 +
 modules/stock/view/reporting_inventory_turnover_context_form.xml |   15 +
 modules/stock/view/reporting_inventory_turnover_form.xml         |   15 +
 modules/stock/view/reporting_inventory_turnover_list.xml         |    9 +
 22 files changed, 1330 insertions(+), 1 deletions(-)

diffs (1450 lines):

diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/CHANGELOG
--- a/modules/stock/CHANGELOG   Wed Aug 27 10:00:14 2025 +0200
+++ b/modules/stock/CHANGELOG   Sat Oct 11 10:46:03 2025 +0200
@@ -1,3 +1,4 @@
+* Add inventory and turnover reports
 
 Version 7.6.0 - 2025-04-28
 --------------------------
diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/doc/design/index.rst
--- a/modules/stock/doc/design/index.rst        Wed Aug 27 10:00:14 2025 +0200
+++ b/modules/stock/doc/design/index.rst        Sat Oct 11 10:46:03 2025 +0200
@@ -11,3 +11,4 @@
 .. include:: period.inc.rst
 .. include:: configuration.inc.rst
 .. include:: product.inc.rst
+.. include:: reporting.inc.rst
diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/doc/design/reporting.inc.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/doc/design/reporting.inc.rst        Sat Oct 11 10:46:03 
2025 +0200
@@ -0,0 +1,63 @@
+.. _model-stock.reporting.inventory:
+
+Inventory Reporting
+===================
+
+The *Inventory* report is used to calculate the quantity of `Product
+<product:concept-product>` at a given `Location <model-stock.location>` on a
+specific date.
+
+.. seealso::
+
+   Inventory report can be accessed from the main menu item:
+
+      |Inventory & Stock --> Reporting --> Inventory|__
+
+      .. |Inventory & Stock --> Reporting --> Inventory| replace:: 
:menuselection:`Inventory & Stock --> Reporting --> Inventory`
+      __ https://demo.tryton.org/model/stock.reporting.inventory
+
+.. _model-stock.reporting.inventory.move:
+
+Inventory Moves Reporting
+=========================
+
+The *Inventory Moves* report provides information about how `Stock Moves
+<model-stock.move>` have affected the stock levels in a `Location
+<model-stock.location>` over time.
+
+.. seealso::
+
+   Inventory Moves report can be accessed from the :guilabel:`Stock Inventory
+   Moves` in the product's :guilabel:`Open related records` menu.
+
+.. _model-stock.reporting.inventory.daily:
+
+Inventory Daily Reporting
+=========================
+
+The *Inventory Daily* report provides information about how the stock levels of
+one or more products have varied over time in a `Location
+<model-stock.location>`.
+
+.. seealso::
+
+   Inventory Daily report can be accessed from the :guilabel:`Stock Inventory
+   Daily` in the product's :guilabel:`Open related records` menu.
+
+.. _model-stock.reporting.inventory.turnover:
+
+Inventory Turnover Reporting
+============================
+
+The *Inventory Turnover* report calculates the turnover of each `Product
+<product:concept-product>` of a `Location <model-stock.location>` over a
+period.
+
+.. seealso::
+
+   Inventory Turnover report can be accessed from the main menu item:
+
+      |Inventory & Stock --> Reporting --> Inventory Turnover|__
+
+      .. |Inventory & Stock --> Reporting --> Inventory Turnover| replace:: 
:menuselection:`Inventory & Stock --> Reporting --> Inventory Turnover`
+      __ https://demo.tryton.org/model/stock.reporting.inventory.turnover
diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/move.py
--- a/modules/stock/move.py     Wed Aug 27 10:00:14 2025 +0200
+++ b/modules/stock/move.py     Sat Oct 11 10:46:03 2025 +0200
@@ -443,6 +443,14 @@
                     (t.company, Index.Equality()),
                     (t.state, Index.Equality(cardinality='low')),
                     where=t.state.in_(['staging', 'draft', 'assigned'])),
+                Index(
+                    t,
+                    (t.company, Index.Equality()),
+                    (Coalesce(t.effective_date, t.planned_date),
+                        Index.Range()),
+                    (t.state, Index.Equality(cardinality='low')),
+                    (t.product, Index.Range()),
+                    where=t.state.in_(['done', 'assigned', 'draft'])),
                 })
         cls._order[0] = ('id', 'DESC')
         cls._transitions |= set((
diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/stock.xml
--- a/modules/stock/stock.xml   Wed Aug 27 10:00:14 2025 +0200
+++ b/modules/stock/stock.xml   Sat Oct 11 10:46:03 2025 +0200
@@ -71,7 +71,7 @@
             parent="menu_stock"
             sequence="100"
             id="menu_reporting"
-            active="0"/>
+            active="1"/>
 
     </data>
 </tryton>
diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/stock_reporting.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/stock_reporting.py  Sat Oct 11 10:46:03 2025 +0200
@@ -0,0 +1,526 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+
+import datetime as dt
+
+from dateutil.relativedelta import relativedelta
+from sql import Literal, Null, Window
+from sql.aggregate import Min, Sum
+from sql.conditionals import Case, Coalesce, Greatest, Least, NullIf
+from sql.functions import Function, NthValue, Round
+from sql.operators import Concat
+
+from trytond import backend
+from trytond.model import ModelSQL, ModelView, fields
+from trytond.pool import Pool
+from trytond.pyson import Eval
+from trytond.transaction import Transaction
+
+
+class _SQLite_JulianDay(Function):
+    __slots__ = ()
+    _function = 'JULIANDAY'
+
+
+class _InventoryContextMixin(ModelView):
+
+    company = fields.Many2One('company.company', "Company", required=True)
+    location = fields.Many2One(
+        'stock.location', "Location", required=True,
+        domain=[
+            ('type', 'in', ['warehouse', 'storage', 'view']),
+            ])
+
+    @classmethod
+    def default_company(cls):
+        return Transaction().context.get('company')
+
+    @classmethod
+    def default_location(cls):
+        pool = Pool()
+        Location = pool.get('stock.location')
+        return Transaction().context.get(
+            'location', Location.get_default_warehouse())
+
+
+class _InventoryMixin:
+    __slots__ = ()
+
+    company = fields.Many2One('company.company', "Company")
+    product = fields.Reference("Product", [
+            ('product.product', "Variant"),
+            ('product.template', "Product"),
+            ])
+    unit = fields.Function(
+        fields.Many2One('product.uom', "Unit"),
+        'on_change_with_unit')
+
+    input_quantity = fields.Float("Input Quantity")
+    output_quantity = fields.Float("Output Quantity")
+    quantity = fields.Float("Quantity")
+
+    @classmethod
+    def table_query(cls):
+        pool = Pool()
+        Location = pool.get('stock.location')
+        Move = pool.get('stock.move')
+        Period = pool.get('stock.period')
+        PeriodCache = pool.get('stock.period.cache')
+        Product = pool.get('product.product')
+
+        transaction = Transaction()
+        context = transaction.context
+        move = Move.__table__()
+        from_location = Location.__table__()
+        to_location = Location.__table__()
+        period_cache = PeriodCache.__table__()
+        product_table = Product.__table__()
+
+        if location := context.get('location'):
+            location = Location(location)
+            left, right = location.left, location.right
+        else:
+            left = right = -1
+        if date := context.get('date'):
+            from_date = to_date = date
+        else:
+            from_date = context.get('from_date') or dt.date.min
+            to_date = context.get('to_date') or dt.date.max
+        company = context.get('company')
+
+        if periods := Period.search([
+                    ('company', '=', company),
+                    ('date', '<=', from_date),
+                    ('state', '=', 'closed'),
+                    ],
+                order=[('date', 'DESC')],
+                limit=1):
+            period, = periods
+        else:
+            period = None
+
+        quantities = (
+            move
+            .join(from_location,
+                condition=move.from_location == from_location.id)
+            .join(to_location,
+                condition=move.to_location == to_location.id)
+            )
+
+        if context.get('product_type') == 'product.product':
+            product = Concat('product.product,', move.product)
+        else:
+            product = Concat('product.template,', product_table.template)
+            quantities = (
+                quantities
+                .join(product_table,
+                    condition=move.product == product_table.id)
+                )
+
+        move_date = Coalesce(move.effective_date, move.planned_date)
+        input_clause = (
+            (to_location.left >= left) & (to_location.right <= right))
+        output_clause = (
+            (from_location.left >= left) & (from_location.right <= right))
+        start_quantity = Sum(
+            move.quantity * Case((output_clause, -1), else_=1),
+            filter_=(move_date < from_date))
+        input_quantity = Sum(
+            move.quantity,
+            filter_=input_clause & (move_date >= from_date))
+        output_quantity = Sum(
+            move.quantity,
+            filter_=output_clause & (move_date >= from_date))
+
+        date_column = Greatest(move_date, from_date)
+        if context.get('product_type') == 'product.product':
+            partition = [move.product]
+        else:
+            partition = [product_table.template]
+
+        # Use NthValue instead of LastValue to get NULL for the last row
+        next_date_column = NthValue(
+            date_column, 2, window=Window(
+                partition,
+                order_by=[date_column.asc],
+                frame='ROWS', start=0, end=1))
+
+        state_clause = cls._state_clause(move, to_date)
+
+        quantities = quantities.select(
+            Min(move.id).as_('id'),
+            product.as_('product'),
+            date_column.as_('date'),
+            next_date_column.as_('next_date'),
+            start_quantity.as_('start_quantity'),
+            input_quantity.as_('input_quantity'),
+            output_quantity.as_('output_quantity'),
+            *cls._quantities_columns(move, product_table),
+            where=(((input_clause & ~output_clause)
+                    | (output_clause & ~input_clause))
+                & (move_date <= to_date)
+                & state_clause
+                & (move.company == company)),
+            group_by=[
+                *partition,
+                date_column,
+                *cls._quantities_group_by(move, product_table),
+                ],
+            )
+
+        quantity = Sum(
+            Coalesce(quantities.start_quantity, 0)
+            + Coalesce(quantities.input_quantity, 0)
+            - Coalesce(quantities.output_quantity, 0),
+            window=Window(
+                [quantities.product],
+                order_by=[*cls._quantities_order_by(quantities)]))
+
+        if period:
+            quantities.where &= move_date > period.date
+            if context.get('product_type') != 'product.product':
+                period_cache_product = Product.__table__()
+                cache = (
+                    period_cache
+                    .join(period_cache_product,
+                        condition=period_cache.product
+                        == period_cache_product.id)
+                    .select(
+                        period_cache_product.template.as_('product'),
+                        Sum(period_cache.internal_quantity
+                            ).as_('quantity'),
+                        group_by=[period_cache_product.template]))
+            else:
+                cache = (
+                    period_cache
+                    .select(
+                        period_cache.product.as_('product'),
+                        Sum(period_cache.internal_quantity).as_('quantity'),
+                        group_by=[period_cache.product]))
+            location_cache = Location.__table__()
+            cache.where = (
+                (period_cache.period == period.id)
+                & period_cache.location.in_(
+                    location_cache.select(
+                        location_cache.id,
+                        where=(location_cache.left >= left)
+                        & (location_cache.right <= right))))
+            query = quantities.join(cache, 'LEFT',
+                condition=(
+                    cache.product
+                    == cls.product.sql_id(quantities.product, cls)))
+            quantity += Coalesce(cache.quantity, 0)
+        else:
+            query = quantities
+
+        return (query
+            .select(
+                quantities.id.as_('id'),
+                Literal(company).as_('company'),
+                quantities.product.as_('product'),
+                quantities.input_quantity.as_('input_quantity'),
+                quantities.output_quantity.as_('output_quantity'),
+                quantity.as_('quantity'),
+                *cls._columns(quantities),
+                where=(
+                    (Coalesce(quantities.next_date, dt.date.max) >= from_date)
+                    | (quantities.date >= from_date))))
+
+    @classmethod
+    def _quantities_columns(cls, move, product):
+        yield from []
+
+    @classmethod
+    def _quantities_group_by(cls, move, product):
+        yield from []
+
+    @classmethod
+    def _quantities_order_by(cls, quantities):
+        yield quantities.date.asc
+
+    @classmethod
+    def _columns(cls, quantities):
+        yield from []
+
+    @classmethod
+    def _where(cls, quantities):
+        return Literal(True)
+
+    @classmethod
+    def _state_clause(cls, move, date):
+        pool = Pool()
+        Date = pool.get('ir.date')
+        today = Date.today()
+        forcast = date > today
+
+        move_date = Coalesce(move.effective_date, move.planned_date)
+        state_clause = (
+            (move_date <= today) & (move.state == 'done'))
+        state_clause |= (
+            ((move_date >= today) if forcast else (move_date > today))
+            & (move.state.in_(['done', 'assigned', 'draft'])))
+        return state_clause
+
+    @fields.depends('product')
+    def on_change_with_unit(self, name=None):
+        if self.product:
+            return self.product.default_uom
+
+
+class InventoryContext(_InventoryContextMixin):
+    __name__ = 'stock.reporting.inventory.context'
+
+    date = fields.Date("Date", required=True)
+    product_type = fields.Selection([
+            ('product.product', "Variant"),
+            ('product.template', "Product"),
+            ], "Product Type", required=True)
+
+    @classmethod
+    def default_date(cls):
+        return Pool().get('ir.date').today()
+
+    @classmethod
+    def default_product_type(cls):
+        return Transaction().context.get('product_type') or 'product.template'
+
+
+class Inventory(_InventoryMixin, ModelSQL, ModelView):
+    __name__ = 'stock.reporting.inventory'
+
+
+class _InventoryRangeContextMixin(_InventoryContextMixin):
+
+    from_date = fields.Date(
+        "From Date", required=True,
+        domain=[
+            ('from_date', '<=', Eval('to_date')),
+            ])
+    to_date = fields.Date(
+        "To Date", required=True,
+        domain=[
+            ('to_date', '>=', Eval('from_date')),
+            ])
+
+    @classmethod
+    def default_from_date(cls):
+        pool = Pool()
+        Date = pool.get('ir.date')
+        context = Transaction().context
+        if 'from_date' in context:
+            return context['from_date']
+        return Date.today() - relativedelta(day=1, months=6)
+
+    @classmethod
+    def default_to_date(cls):
+        pool = Pool()
+        Date = pool.get('ir.date')
+        context = Transaction().context
+        if 'to_date' in context:
+            return context['to_date']
+        return Date.today() + relativedelta(day=1, months=6)
+
+
+class InventoryRangeContext(_InventoryRangeContextMixin):
+    __name__ = 'stock.reporting.inventory.range.context'
+
+
+class InventoryMove(_InventoryMixin, ModelSQL, ModelView):
+    __name__ = 'stock.reporting.inventory.move'
+
+    date = fields.Date("Date")
+    move = fields.Many2One('stock.move', "Move")
+    origin = fields.Reference("Origin", selection='get_origin')
+    document = fields.Function(
+        fields.Reference("Document", selection='get_documents'),
+        'get_document')
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order = [
+            ('date', 'DESC'),
+            ('move', 'DESC NULLS LAST'),
+            ('id', 'DESC'),
+            ]
+
+    @classmethod
+    def _quantities_columns(cls, move, product):
+        from_date = Transaction().context.get('from_date') or dt.date.min
+        move_date = Coalesce(move.effective_date, move.planned_date)
+        yield from super()._quantities_columns(move, product)
+        yield Case(
+            (move_date < from_date, Null),
+            else_=move.id).as_('move')
+        yield Case(
+            (move_date < from_date, Null),
+            else_=move.origin).as_('origin')
+
+    @classmethod
+    def _quantities_order_by(cls, quantities):
+        yield from super()._quantities_order_by(quantities)
+        yield quantities.move.asc.nulls_first
+
+    @classmethod
+    def _quantities_group_by(cls, move, product):
+        from_date = Transaction().context.get('from_date') or dt.date.min
+        move_date = Coalesce(move.effective_date, move.planned_date)
+        yield from super()._quantities_group_by(move, product)
+        yield Case(
+            (move_date < from_date, Null),
+            else_=move.id)
+        yield Case(
+            (move_date < from_date, Null),
+            else_=move.origin)
+
+    @classmethod
+    def _columns(cls, quantities):
+        yield from super()._columns(quantities)
+        yield quantities.date.as_('date')
+        yield quantities.move.as_('move')
+        yield quantities.origin.as_('origin')
+
+    @classmethod
+    def get_origin(cls):
+        pool = Pool()
+        Move = pool.get('stock.move')
+        return Move.get_origin()
+
+    @classmethod
+    def _get_document_models(cls):
+        pool = Pool()
+        Move = pool.get('stock.move')
+        return [m for m, _ in Move.get_shipment() if m]
+
+    @classmethod
+    def get_documents(cls):
+        pool = Pool()
+        Model = pool.get('ir.model')
+        get_name = Model.get_name
+        models = cls._get_document_models()
+        return [(None, '')] + [(m, get_name(m)) for m in models]
+
+    def get_document(self, name):
+        if self.move and self.move.shipment:
+            return str(self.move.shipment)
+
+    def get_rec_name(self, name):
+        name = super().get_rec_name(name)
+        if self.move:
+            name = self.move.rec_name
+        return name
+
+
+class InventoryDaily(_InventoryMixin, ModelSQL, ModelView):
+    __name__ = 'stock.reporting.inventory.daily'
+
+    from_date = fields.Date("From Date")
+    to_date = fields.Date("To Date")
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order = [
+            ('from_date', 'DESC'),
+            ('id', 'DESC'),
+            ]
+
+    @classmethod
+    def _quantities_order_by(cls, quantities):
+        yield from []
+        yield quantities.date.asc
+
+    @classmethod
+    def _columns(cls, quantities):
+        transaction = Transaction()
+        context = transaction.context
+        to_date = context.get('to_date') or dt.date.max
+
+        yield from super()._columns(quantities)
+
+        yield quantities.date.as_('from_date')
+        yield Least(quantities.next_date, to_date).as_('to_date')
+
+
+class InventoryTurnoverContext(_InventoryRangeContextMixin):
+    __name__ = 'stock.reporting.inventory.turnover.context'
+
+    product_type = fields.Selection([
+            ('product.product', "Variant"),
+            ('product.template', "Product"),
+            ], "Product Type", required=True)
+
+    @classmethod
+    def default_product_type(cls):
+        return Transaction().context.get('product_type') or 'product.template'
+
+
+class InventoryTurnover(ModelSQL, ModelView):
+    __name__ = 'stock.reporting.inventory.turnover'
+
+    company = fields.Many2One('company.company', "Company")
+    product = fields.Reference("Product", [
+            ('product.product', "Variant"),
+            ('product.template', "Product"),
+            ])
+    unit = fields.Function(
+        fields.Many2One('product.uom', "Unit"),
+        'on_change_with_unit')
+
+    output_quantity = fields.Float("Output Quantity", digits=(None, 3))
+    average_quantity = fields.Float("Average Quantity", digits=(None, 3))
+    turnover = fields.Float("Turnover", digits=(None, 3))
+
+    @classmethod
+    def table_query(cls):
+        pool = Pool()
+        Inventory = pool.get('stock.reporting.inventory.daily')
+
+        transaction = Transaction()
+        context = transaction.context
+        inventory = Inventory.__table__()
+
+        from_date = context.get('from_date') or dt.date.min
+        to_date = context.get('to_date') or dt.date.max
+        company = context.get('company')
+
+        days = (to_date - from_date).days + 1
+
+        output_quantity = Sum(inventory.output_quantity) / days
+        inventory_from_date = inventory.from_date
+        inventory_to_date = inventory.to_date
+        if backend.name == 'sqlite':
+            inventory_from_date = _SQLite_JulianDay(inventory_from_date)
+            inventory_to_date = _SQLite_JulianDay(inventory_to_date)
+        average_quantity = (
+            Sum(Case(
+                    (inventory.quantity >= 0, inventory.quantity),
+                    else_=0)
+                * (inventory_to_date - inventory_from_date
+                    + Case((inventory.to_date == to_date, 1), else_=0)))
+            / days)
+
+        def round_sql(expression, digits=2):
+            factor = 10 ** digits
+            return Round(expression * factor) / factor
+
+        return (inventory
+            .select(
+                cls.product.sql_id(inventory.product, cls).as_('id'),
+                Literal(company).as_('company'),
+                inventory.product.as_('product'),
+                round_sql(
+                    output_quantity,
+                    cls.output_quantity.digits[1]).as_('output_quantity'),
+                round_sql(
+                    average_quantity,
+                    cls.average_quantity.digits[1]).as_('average_quantity'),
+                round_sql(
+                    output_quantity / NullIf(average_quantity, 0),
+                    cls.turnover.digits[1]).as_('turnover'),
+                group_by=[inventory.product]))
+
+    @fields.depends('product')
+    def on_change_with_unit(self, name=None):
+        if self.product:
+            return self.product.default_uom
diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/stock_reporting.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/stock_reporting.xml Sat Oct 11 10:46:03 2025 +0200
@@ -0,0 +1,315 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tryton>
+    <data>
+        <record model="ir.ui.view" id="reporting_inventory_context_view_form">
+            <field name="model">stock.reporting.inventory.context</field>
+            <field name="type">form</field>
+            <field name="name">reporting_inventory_context_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_view_list">
+            <field name="model">stock.reporting.inventory</field>
+            <field name="type">tree</field>
+            <field name="name">reporting_inventory_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_view_form">
+            <field name="model">stock.reporting.inventory</field>
+            <field name="type">form</field>
+            <field name="name">reporting_inventory_form</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_inventory">
+            <field name="name">Inventory</field>
+            <field name="res_model">stock.reporting.inventory</field>
+            <field 
name="context_model">stock.reporting.inventory.context</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_inventory_view_list"/>
+            <field name="act_window" ref="act_reporting_inventory"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="reporting_inventory_view_form"/>
+            <field name="act_window" ref="act_reporting_inventory"/>
+        </record>
+
+        <menuitem
+            parent="menu_reporting"
+            action="act_reporting_inventory"
+            sequence="20"
+            id="menu_reporting_inventory"/>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_inventory_companies">
+            <field name="name">User in companies</field>
+            <field name="model">stock.reporting.inventory</field>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_inventory_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_inventory_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_inventory">
+            <field name="model">stock.reporting.inventory</field>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_inventory_group_stock">
+            <field name="model">stock.reporting.inventory</field>
+            <field name="group" ref="group_stock"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_inventory_range_context_view_form">
+            <field name="model">stock.reporting.inventory.range.context</field>
+            <field name="type">form</field>
+            <field name="name">reporting_inventory_range_context_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_move_view_list">
+            <field name="model">stock.reporting.inventory.move</field>
+            <field name="type">tree</field>
+            <field name="name">reporting_inventory_move_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_move_view_form">
+            <field name="model">stock.reporting.inventory.move</field>
+            <field name="type">form</field>
+            <field name="name">reporting_inventory_move_form</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_inventory_move">
+            <field name="name">Stock Inventory Moves</field>
+            <field name="res_model">stock.reporting.inventory.move</field>
+            <field 
name="context_model">stock.reporting.inventory.range.context</field>
+            <field
+                name="domain"
+                eval="[('product', '=', (Eval('active_model', 
'product.template'), Eval('active_id', -1)))]"
+                pyson="1"/>
+            <field
+                name="context"
+                eval="{'product_type': Eval('active_model', 
'product.template'), 'date': None}"
+                pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_move_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_inventory_move_view_list"/>
+            <field name="act_window" ref="act_reporting_inventory_move"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_move_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="reporting_inventory_move_view_form"/>
+            <field name="act_window" ref="act_reporting_inventory_move"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_inventory_move_keyword_product">
+            <field name="keyword">form_relate</field>
+            <field name="model">product.product,-1</field>
+            <field name="action" ref="act_reporting_inventory_move"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_inventory_move_keyword_template">
+            <field name="keyword">form_relate</field>
+            <field name="model">product.template,-1</field>
+            <field name="action" ref="act_reporting_inventory_move"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_inventory_move_companies">
+            <field name="name">User in companies</field>
+            <field name="model">stock.reporting.inventory.move</field>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_inventory_move_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_inventory_move_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_inventory_move">
+            <field name="model">stock.reporting.inventory.move</field>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_inventory_move_group_stock">
+            <field name="model">stock.reporting.inventory.move</field>
+            <field name="group" ref="group_stock"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_daily_view_list">
+            <field name="model">stock.reporting.inventory.daily</field>
+            <field name="type">tree</field>
+            <field name="name">reporting_inventory_daily_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_daily_view_form">
+            <field name="model">stock.reporting.inventory.daily</field>
+            <field name="type">form</field>
+            <field name="name">reporting_inventory_daily_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_daily_view_graph">
+            <field name="model">stock.reporting.inventory.daily</field>
+            <field name="type">graph</field>
+            <field name="name">reporting_inventory_daily_graph</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_inventory_daily">
+            <field name="name">Stock Inventory Daily</field>
+            <field name="res_model">stock.reporting.inventory.daily</field>
+            <field 
name="context_model">stock.reporting.inventory.range.context</field>
+            <field
+                name="domain"
+                eval="[('product', '=', (Eval('active_model', 
'product.template'), Eval('active_id', -1)))]"
+                pyson="1"/>
+            <field
+                name="context"
+                eval="{'product_type': Eval('active_model', 
'product.template'), 'date': None}"
+                pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_daily_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_inventory_daily_view_list"/>
+            <field name="act_window" ref="act_reporting_inventory_daily"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_daily_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="reporting_inventory_daily_view_graph"/>
+            <field name="act_window" ref="act_reporting_inventory_daily"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_daily_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" ref="reporting_inventory_daily_view_form"/>
+            <field name="act_window" ref="act_reporting_inventory_daily"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_inventory_daily_keyword_product">
+            <field name="keyword">form_relate</field>
+            <field name="model">product.product,-1</field>
+            <field name="action" ref="act_reporting_inventory_daily"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_inventory_daily_keyword_template">
+            <field name="keyword">form_relate</field>
+            <field name="model">product.template,-1</field>
+            <field name="action" ref="act_reporting_inventory_daily"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_inventory_daily_companies">
+            <field name="name">User in companies</field>
+            <field name="model">stock.reporting.inventory.daily</field>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_inventory_daily_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_inventory_daily_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_inventory_daily">
+            <field name="model">stock.reporting.inventory.daily</field>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_inventory_daily_group_stock">
+            <field name="model">stock.reporting.inventory.daily</field>
+            <field name="group" ref="group_stock"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_inventory_turnover_context_view_form">
+            <field 
name="model">stock.reporting.inventory.turnover.context</field>
+            <field name="type">form</field>
+            <field 
name="name">reporting_inventory_turnover_context_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_turnover_view_list">
+            <field name="model">stock.reporting.inventory.turnover</field>
+            <field name="type">tree</field>
+            <field name="name">reporting_inventory_turnover_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_inventory_turnover_view_form">
+            <field name="model">stock.reporting.inventory.turnover</field>
+            <field name="type">form</field>
+            <field name="name">reporting_inventory_turnover_form</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_inventory_turnover">
+            <field name="name">Inventory Turnover</field>
+            <field name="res_model">stock.reporting.inventory.turnover</field>
+            <field 
name="context_model">stock.reporting.inventory.turnover.context</field>
+            <field
+                name="context"
+                eval="{'from_date': Date(delta_years=-1), 'to_date': Date()}"
+                pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_turnover_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_inventory_turnover_view_list"/>
+            <field name="act_window" ref="act_reporting_inventory_turnover"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_inventory_turnover_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="reporting_inventory_turnover_view_form"/>
+            <field name="act_window" ref="act_reporting_inventory_turnover"/>
+        </record>
+
+        <menuitem
+            parent="menu_reporting"
+            action="act_reporting_inventory_turnover"
+            sequence="30"
+            id="menu_reporting_inventory_turnover"/>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_inventory_turnover_companies">
+            <field name="name">User in companies</field>
+            <field name="model">stock.reporting.inventory.turnover</field>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" 
id="rule_reporting_inventory_turnover_companies">
+            <field
+                name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_inventory_turnover_companies"/>
+        </record>
+
+        <record model="ir.model.access" 
id="access_reporting_inventory_turnover">
+            <field name="model">stock.reporting.inventory.turnover</field>
+            <field name="perm_read" eval="False"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+        <record model="ir.model.access" 
id="access_reporting_inventory_turnover_group_stock">
+            <field name="model">stock.reporting.inventory.turnover</field>
+            <field name="group" ref="group_stock"/>
+            <field name="perm_read" eval="True"/>
+            <field name="perm_write" eval="False"/>
+            <field name="perm_create" eval="False"/>
+            <field name="perm_delete" eval="False"/>
+        </record>
+    </data>
+</tryton>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/tests/scenario_stock_reporting_inventory.json
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/tests/scenario_stock_reporting_inventory.json       Sat Oct 
11 10:46:03 2025 +0200
@@ -0,0 +1,6 @@
+[
+    {"period_close": false, "product_type": "product.template"}
+    ,{"period_close": false, "product_type": "product.product"}
+    ,{"period_close": true, "product_type": "product.template"}
+    ,{"period_close": true, "product_type": "product.product"}
+]
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/tests/scenario_stock_reporting_inventory.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/tests/scenario_stock_reporting_inventory.rst        Sat Oct 
11 10:46:03 2025 +0200
@@ -0,0 +1,239 @@
+==================================
+Stock Reporting Inventory Scenario
+==================================
+
+Imports::
+
+    >>> import datetime as dt
+    >>> from decimal import Decimal
+    >>> from unittest.mock import patch
+
+    >>> from proteus import Model
+    >>> from trytond.ir.date import Date
+    >>> from trytond.modules.company.tests.tools import create_company
+    >>> from trytond.modules.currency.tests.tools import get_currency
+    >>> from trytond.tests.tools import activate_modules, assertEqual
+
+    >>> period_close = globals().get('period_close', False)
+    >>> product_type = globals().get('product_type', 'product.template')
+
+Patch today::
+
+    >>> mock = patch.object(Date, 'today', return_value=dt.date(2025, 1, 15))
+    >>> _ = mock.start()
+
+Activate modules::
+
+    >>> config = activate_modules('stock', create_company)
+
+    >>> Inventory = Model.get('stock.reporting.inventory')
+    >>> InventoryMove = Model.get('stock.reporting.inventory.move')
+    >>> InventoryDaily = Model.get('stock.reporting.inventory.daily')
+    >>> InventoryTurnover = Model.get('stock.reporting.inventory.turnover')
+    >>> Location = Model.get('stock.location')
+    >>> Move = Model.get('stock.move')
+    >>> Period = Model.get('stock.period')
+    >>> ProductTemplate = Model.get('product.template')
+    >>> ProductUom = Model.get('product.uom')
+
+Get currency::
+
+    >>> currency = get_currency()
+
+Create product::
+
+    >>> unit, = ProductUom.find([('name', '=', 'Unit')])
+
+    >>> template = ProductTemplate()
+    >>> template.name = 'Product'
+    >>> template.default_uom = unit
+    >>> template.type = 'goods'
+    >>> template.save()
+    >>> product, = template.products
+
+Get stock locations::
+
+    >>> warehouse_loc, = Location.find([('code', '=', 'WH')])
+    >>> supplier_loc, = Location.find([('code', '=', 'SUP')])
+    >>> customer_loc, = Location.find([('code', '=', 'CUS')])
+    >>> storage_loc, = Location.find([('code', '=', 'STO')])
+
+Fill warehouse::
+
+    >>> move = Move()
+    >>> move.product = product
+    >>> move.from_location = supplier_loc
+    >>> move.to_location = storage_loc
+    >>> move.quantity = 10
+    >>> move.effective_date = dt.date(2025, 1, 1)
+    >>> move.unit_price = Decimal('10.0000')
+    >>> move.currency = currency
+    >>> move.click('do')
+    >>> move.state
+    'done'
+
+    >>> move = Move()
+    >>> move.product = product
+    >>> move.from_location = storage_loc
+    >>> move.to_location = customer_loc
+    >>> move.unit = unit
+    >>> move.quantity = 1
+    >>> move.effective_date = dt.date(2025, 1, 1)
+    >>> move.unit_price = Decimal('20.0000')
+    >>> move.currency = currency
+    >>> move.click('do')
+    >>> move.state
+    'done'
+
+    >>> move = Move()
+    >>> move.product = product
+    >>> move.from_location = storage_loc
+    >>> move.to_location = customer_loc
+    >>> move.unit = unit
+    >>> move.quantity = 2
+    >>> move.effective_date = dt.date(2025, 1, 10)
+    >>> move.unit_price = Decimal('20.0000')
+    >>> move.currency = currency
+    >>> move.click('do')
+    >>> move.state
+    'done'
+
+Forecast some moves::
+
+    >>> move = Move()
+    >>> move.product = product
+    >>> move.from_location = supplier_loc
+    >>> move.to_location = storage_loc
+    >>> move.quantity = 10
+    >>> move.effective_date = dt.date(2025, 1, 5)
+    >>> move.unit_price = Decimal('10.0000')
+    >>> move.currency = currency
+    >>> move.save()
+    >>> move.state
+    'draft'
+
+    >>> move = Move()
+    >>> move.product = product
+    >>> move.from_location = storage_loc
+    >>> move.to_location = customer_loc
+    >>> move.unit = unit
+    >>> move.quantity = 3
+    >>> move.planned_date = dt.date(2025, 1, 20)
+    >>> move.unit_price = Decimal('20.0000')
+    >>> move.currency = currency
+    >>> move.save()
+    >>> move.state
+    'draft'
+
+Close period::
+
+    >>> period = Period(date=dt.date(2025, 1, 5))
+    >>> if period_close:
+    ...     period.click('close')
+
+
+Check inventory::
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         product_type=product_type,
+    ...         date=dt.date(2024, 1, 1)):
+    ...     Inventory.find([])
+    []
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         product_type=product_type,
+    ...         date=dt.date(2025, 1, 1)):
+    ...     inventory, = Inventory.find([])
+    >>> inventory.quantity
+    9.0
+    >>> assertEqual(inventory.product.__class__.__name__, product_type)
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         product_type=product_type,
+    ...         date=dt.date(2025, 1, 15)):
+    ...     inventory, = Inventory.find([])
+    >>> inventory.quantity
+    7.0
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         product_type=product_type,
+    ...         date=dt.date(2025, 1, 20)):
+    ...     inventory, = Inventory.find([])
+    >>> inventory.quantity
+    4.0
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         product_type=product_type):
+    ...     inventory_moves = InventoryMove.find([])
+    ...     inventories_daily = InventoryDaily.find([])
+
+    >>> [i.quantity for i in inventory_moves]
+    [4.0, 7.0, 9.0, 10.0]
+    >>> [(i.input_quantity, i.output_quantity) for i in inventory_moves]
+    [(None, 3.0), (None, 2.0), (None, 1.0), (10.0, None)]
+
+    >>> [i.quantity for i in inventories_daily]
+    [4.0, 7.0, 9.0]
+    >>> [(i.input_quantity, i.output_quantity) for i in inventories_daily]
+    [(None, 3.0), (None, 2.0), (10.0, 1.0)]
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         from_date=dt.date(2025, 1, 15),
+    ...         to_date=dt.date(2025, 1, 29),
+    ...         product_type=product_type):
+    ...     inventory_moves = InventoryMove.find([])
+    ...     inventories_daily = InventoryDaily.find([])
+
+    >>> [i.quantity for i in inventory_moves]
+    [4.0, 7.0]
+    >>> [(i.input_quantity, i.output_quantity) for i in inventory_moves]
+    [(None, 3.0), (None, None)]
+
+    >>> [i.quantity for i in inventories_daily]
+    [4.0, 7.0]
+    >>> [(i.input_quantity, i.output_quantity) for i in inventories_daily]
+    [(None, 3.0), (None, None)]
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         from_date=dt.date(2025, 1, 10),
+    ...         to_date=dt.date(2025, 1, 29),
+    ...         product_type=product_type):
+    ...     inventory_moves = InventoryMove.find([])
+    ...     inventories_daily = InventoryDaily.find([])
+
+    >>> assertEqual(
+    ...     [i.quantity for i in inventory_moves],
+    ...     [4.0, 7.0] if period_close else [4.0, 7.0, 9.0])
+    >>> assertEqual(
+    ...     [(i.input_quantity, i.output_quantity) for i in inventory_moves],
+    ...     [(None, 3.0), (None, 2.0)] if period_close
+    ...     else [(None, 3.0), (None, 2.0), (None, None)])
+
+    >>> [i.quantity for i in inventories_daily]
+    [4.0, 7.0]
+    >>> [(i.input_quantity, i.output_quantity) for i in inventories_daily]
+    [(None, 3.0), (None, 2.0)]
+
+Check Inventory turnover::
+
+    >>> with config.set_context(
+    ...         location=warehouse_loc.id,
+    ...         from_date=dt.date(2025, 1, 10),
+    ...         to_date=dt.date(2025, 1, 29),
+    ...         product_type=product_type):
+    ...     turnover, = InventoryTurnover.find([])
+
+    >>> turnover.output_quantity
+    0.25
+    >>> turnover.average_quantity
+    5.5
+    >>> turnover.turnover
+    0.045
+    >>> assertEqual(turnover.product.__class__.__name__, product_type)
diff -r 60a4baa7c9ca -r a0b571509e75 modules/stock/tryton.cfg
--- a/modules/stock/tryton.cfg  Wed Aug 27 10:00:14 2025 +0200
+++ b/modules/stock/tryton.cfg  Sat Oct 11 10:46:03 2025 +0200
@@ -19,6 +19,7 @@
     period.xml
     message.xml
     res.xml
+    stock_reporting.xml
     stock_reporting_margin.xml
 
 [register]
@@ -60,6 +61,13 @@
     inventory.InventoryLine
     inventory.CountSearch
     inventory.CountQuantity
+    stock_reporting.InventoryContext
+    stock_reporting.Inventory
+    stock_reporting.InventoryRangeContext
+    stock_reporting.InventoryMove
+    stock_reporting.InventoryDaily
+    stock_reporting.InventoryTurnoverContext
+    stock_reporting.InventoryTurnover
     stock_reporting_margin.Context
     stock_reporting_margin.Main
     stock_reporting_margin.MainTimeseries
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_context_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_context_form.xml   Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="date"/>
+    <field name="date"/>
+    <label name="product_type"/>
+    <field name="product_type"/>
+
+    <label name="location"/>
+    <field name="location"/>
+    <label name="company"/>
+    <field name="company"/>
+</form>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_daily_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_daily_form.xml     Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="product"/>
+    <field name="product" colspan="3"/>
+
+    <label name="from_date"/>
+    <field name="from_date"/>
+    <label name="to_date"/>
+    <field name="to_date"/>
+
+    <label name="input_quantity"/>
+    <field name="input_quantity" symbol="unit"/>
+    <label name="output_quantity"/>
+    <field name="output_quantity" symbol="unit"/>
+
+    <label name="quantity"/>
+    <field name="quantity" symbol="unit"/>
+</form>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_daily_graph.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_daily_graph.xml    Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<graph type="line" legend="0">
+    <x>
+        <field name="from_date"/>
+    </x>
+    <y>
+        <field name="quantity" empty="0" interpolation="constant-left"/>
+    </y>
+</graph>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_daily_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_daily_list.xml     Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="product" expand="2"/>
+    <field name="from_date" string="Date"/>
+    <field name="input_quantity" symbol="unit" optional="1"/>
+    <field name="output_quantity" symbol="unit" optional="1"/>
+    <field name="quantity" symbol="unit"/>
+</tree>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_form.xml   Sat Oct 11 10:46:03 
2025 +0200
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="product"/>
+    <field name="product" colspan="3"/>
+
+    <label name="quantity"/>
+    <field name="quantity" symbol="unit"/>
+</form>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_list.xml   Sat Oct 11 10:46:03 
2025 +0200
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="product" expand="2"/>
+    <field name="quantity" symbol="unit"/>
+</tree>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_move_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_move_form.xml      Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="product"/>
+    <field name="product" colspan="3"/>
+
+    <label name="date"/>
+    <field name="date"/>
+    <label name="move"/>
+    <field name="move"/>
+
+    <label name="origin"/>
+    <field name="origin"/>
+    <label name="document"/>
+    <field name="document"/>
+
+    <label name="input_quantity"/>
+    <field name="input_quantity" symbol="unit"/>
+    <label name="output_quantity"/>
+    <field name="output_quantity" symbol="unit"/>
+
+    <label name="quantity"/>
+    <field name="quantity" symbol="unit"/>
+</form>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_move_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_move_list.xml      Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,12 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="product" expand="2"/>
+    <field name="date"/>
+    <field name="origin" expand="1" optional="1"/>
+    <field name="document" expand="1" optional="0"/>
+    <field name="input_quantity" symbol="unit" optional="1"/>
+    <field name="output_quantity" symbol="unit" optional="1"/>
+    <field name="quantity" symbol="unit"/>
+</tree>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_range_context_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_range_context_form.xml     Sat Oct 
11 10:46:03 2025 +0200
@@ -0,0 +1,14 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="from_date"/>
+    <field name="from_date"/>
+    <label name="to_date"/>
+    <field name="to_date"/>
+
+    <label name="location"/>
+    <field name="location"/>
+    <label name="company"/>
+    <field name="company"/>
+</form>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_turnover_context_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_turnover_context_form.xml  Sat Oct 
11 10:46:03 2025 +0200
@@ -0,0 +1,15 @@
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form col="6">
+    <label name="from_date"/>
+    <field name="from_date"/>
+    <label name="to_date"/>
+    <field name="to_date"/>
+    <label name="product_type"/>
+    <field name="product_type"/>
+
+    <label name="location"/>
+    <field name="location" colspan="3"/>
+    <label name="company"/>
+    <field name="company"/>
+</form>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_turnover_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_turnover_form.xml  Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,15 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<form>
+    <label name="product"/>
+    <field name="product" colspan="3"/>
+
+    <label name="output_quantity"/>
+    <field name="output_quantity" symbol="unit"/>
+    <label name="average_quantity"/>
+    <field name="average_quantity" symbol="unit"/>
+
+    <label name="turnover"/>
+    <field name="turnover"/>
+</form>
diff -r 60a4baa7c9ca -r a0b571509e75 
modules/stock/view/reporting_inventory_turnover_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/modules/stock/view/reporting_inventory_turnover_list.xml  Sat Oct 11 
10:46:03 2025 +0200
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<!-- This file is part of Tryton.  The COPYRIGHT file at the top level of
+this repository contains the full copyright notices and license terms. -->
+<tree>
+    <field name="product" expand="2"/>
+    <field name="output_quantity" symbol="unit" optional="0"/>
+    <field name="average_quantity" symbol="unit" optional="0"/>
+    <field name="turnover"/>
+</tree>

Reply via email to