changeset 49a612454113 in modules/purchase:default
details: https://hg.tryton.org/modules/purchase?cmd=changeset&node=49a612454113
description:
        Add reporting

        issue9831
        review312881002
diffstat:

 CHANGELOG                                                  |    2 +
 __init__.py                                                |   11 +-
 doc/design.rst                                             |   48 +
 message.xml                                                |   24 +
 purchase.xml                                               |    2 +-
 purchase_reporting.py                                      |  377 ++++++++++
 purchase_reporting.xml                                     |  456 +++++++++++++
 setup.py                                                   |    1 +
 tests/scenario_purchase_reporting.rst                      |  154 ++++
 tryton.cfg                                                 |    1 +
 view/purchase_reporting_context_form.xml                   |   17 +
 view/purchase_reporting_main_graph_expense.xml             |   11 +
 view/purchase_reporting_main_graph_number.xml              |   11 +
 view/purchase_reporting_main_list.xml                      |    8 +
 view/purchase_reporting_main_time_series_graph_expense.xml |   11 +
 view/purchase_reporting_main_time_series_graph_number.xml  |   11 +
 view/purchase_reporting_main_time_series_list.xml          |    8 +
 view/purchase_reporting_product_list.xml                   |    9 +
 view/purchase_reporting_supplier_list.xml                  |    8 +
 19 files changed, 1168 insertions(+), 2 deletions(-)

diffs (1291 lines):

diff -r 3c8974df1eef -r 49a612454113 CHANGELOG
--- a/CHANGELOG Sat Oct 01 15:35:33 2022 +0200
+++ b/CHANGELOG Fri Oct 07 15:00:40 2022 +0200
@@ -1,3 +1,5 @@
+* Add reporting
+
 Version 6.4.0 - 2022-05-02
 * Bug fixes (see mercurial logs for details)
 * Add supplier currency on party
diff -r 3c8974df1eef -r 49a612454113 __init__.py
--- a/__init__.py       Sat Oct 01 15:35:33 2022 +0200
+++ b/__init__.py       Fri Oct 07 15:00:40 2022 +0200
@@ -3,7 +3,9 @@
 
 from trytond.pool import Pool
 
-from . import configuration, invoice, party, product, purchase, stock
+from . import (
+    configuration, invoice, party, product, purchase, purchase_reporting,
+    stock)
 
 
 def register():
@@ -35,6 +37,13 @@
         party.SupplierLeadTime,
         party.PartySupplierCurrency,
         purchase.ReturnPurchaseStart,
+        purchase_reporting.Context,
+        purchase_reporting.Main,
+        purchase_reporting.MainTimeseries,
+        purchase_reporting.Supplier,
+        purchase_reporting.SupplierTimeseries,
+        purchase_reporting.Product,
+        purchase_reporting.ProductTimeseries,
         module='purchase', type_='model')
     Pool.register(
         purchase.PurchaseReport,
diff -r 3c8974df1eef -r 49a612454113 doc/design.rst
--- a/doc/design.rst    Sat Oct 01 15:35:33 2022 +0200
+++ b/doc/design.rst    Fri Oct 07 15:00:40 2022 +0200
@@ -143,6 +143,54 @@
       .. |Products --> Products --> Suppliers| replace:: 
:menuselection:`Products --> Products --> Suppliers`
       __ https://demo.tryton.org/model/purchase.product_supplier
 
+.. _concept-purchase.reporting:
+
+Purchase Reporting
+==================
+
+Each of the different *Purchase Reporting* concepts are based on either an
+``Abstract`` purchases report, or an ``AbstractTimeseries``.
+There is also a base ``Context`` that is inherited and used to specify things
+such as the date range that is used by the report.
+
+The ``Abstract`` provides the basic properties that makes up a purchases report
+including the number of purchases and expense.
+The ``AbstractTimeseries`` is used to extend this with a date.
+This is used in purchases reports that cover multiple periods of time.
+These are combined together with additional specific properties to create the
+different purchases reports.
+
+.. seealso::
+
+   Purchases reports can be accessed from the main menu item:
+
+      :menuselection:`Purchases --> Reporting --> Purchases`
+
+.. _model-purchase.reporting.supplier:
+.. _model-purchase.reporting.supplier.time_series:
+
+By Supplier
+-----------
+
+The purchases reporting that is done *By Supplier* splits the purchases up
+based on what each `Supplier <party:model-party.party>` bought.
+This is done in two separate parts.
+One that shows the purchases, in total, for the selected period from the
+``Context``.
+Another that breaks them down by date into smaller chunks.
+
+.. _model-purchase.reporting.product:
+.. _model-purchase.reporting.product.time_series:
+
+By Product
+----------
+
+Purchases reporting that is done *By Product* splits up the purchases based on
+the `Product <product:concept-product>` that was purchased.
+This is structured as two parts.
+One that shows the total purchases for the selected period of time, and another
+that shows how the purchases were distributed over time.
+
 .. _model-purchase.configuration:
 
 Configuration
diff -r 3c8974df1eef -r 49a612454113 message.xml
--- a/message.xml       Sat Oct 01 15:35:33 2022 +0200
+++ b/message.xml       Fri Oct 07 15:00:40 2022 +0200
@@ -39,5 +39,29 @@
         <record model="ir.message" id="msg_purchase_line_tax_unique">
             <field name="text">A tax can be added only once to a purchase 
line.</field>
         </record>
+        <record model="ir.message" id="msg_purchase_reporting_company">
+            <field name="text">Company</field>
+        </record>
+        <record model="ir.message" id="msg_purchase_reporting_number">
+            <field name="text">#</field>
+        </record>
+        <record model="ir.message" id="msg_purchase_reporting_number_help">
+            <field name="text">Number of purchases.</field>
+        </record>
+        <record model="ir.message" id="msg_purchase_reporting_expense">
+            <field name="text">Expense</field>
+        </record>
+        <record model="ir.message" id="msg_purchase_reporting_expense_trend">
+            <field name="text">Expense Trend</field>
+        </record>
+        <record model="ir.message" id="msg_purchase_reporting_currency">
+            <field name="text">Currency</field>
+        </record>
+        <record model="ir.message" id="msg_purchase_reporting_date">
+            <field name="text">Date</field>
+        </record>
+        <record model="ir.message" id="msg_purchase_reporting_time_series">
+            <field name="text">Time Series</field>
+        </record>
     </data>
 </tryton>
diff -r 3c8974df1eef -r 49a612454113 purchase.xml
--- a/purchase.xml      Sat Oct 01 15:35:33 2022 +0200
+++ b/purchase.xml      Fri Oct 07 15:00:40 2022 +0200
@@ -215,7 +215,7 @@
             parent="menu_purchase"
             sequence="100"
             id="menu_reporting"
-            active="0"/>
+            active="True"/>
 
         <record model="ir.model.access" id="access_purchase">
             <field name="model" search="[('model', '=', 
'purchase.purchase')]"/>
diff -r 3c8974df1eef -r 49a612454113 purchase_reporting.py
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/purchase_reporting.py     Fri Oct 07 15:00:40 2022 +0200
@@ -0,0 +1,377 @@
+# This file is part of Tryton.  The COPYRIGHT file at the top level of
+# this repository contains the full copyright notices and license terms.
+from itertools import tee, zip_longest
+
+try:
+    import pygal
+except ImportError:
+    pygal = None
+from dateutil.relativedelta import relativedelta
+from sql import Literal, Null
+from sql.aggregate import Count, Min, Sum
+from sql.conditionals import Coalesce
+from sql.functions import CurrentTimestamp, DateTrunc
+
+from trytond.i18n import lazy_gettext
+from trytond.model import ModelSQL, ModelView, fields
+from trytond.modules.currency.fields import Monetary
+from trytond.pool import Pool
+from trytond.pyson import Eval, If
+from trytond.transaction import Transaction
+
+
+def pairwise(iterable):
+    a, b = tee(iterable)
+    next(b)
+    return zip_longest(a, b)
+
+
+class Abstract(ModelSQL):
+
+    company = fields.Many2One(
+        'company.company',
+        lazy_gettext("purchase.msg_purchase_reporting_company"))
+    number = fields.Integer(
+        lazy_gettext("purchase.msg_purchase_reporting_number"),
+        help=lazy_gettext("purchase.msg_purchase_reporting_number_help"))
+    expense = Monetary(
+        lazy_gettext("purchase.msg_purchase_reporting_expense"),
+        digits='currency', currency='currency')
+    expense_trend = fields.Function(
+        fields.Char(
+            lazy_gettext("purchase.msg_purchase_reporting_expense_trend")),
+        'get_trend')
+    time_series = None
+    currency = fields.Many2One(
+        'currency.currency',
+        lazy_gettext("purchase.msg_purchase_reporting_currency"))
+
+    @classmethod
+    def table_query(cls):
+        from_item, tables = cls._joins()
+        return from_item.select(*cls._columns(tables),
+            where=cls._where(tables),
+            group_by=cls._group_by(tables))
+
+    @classmethod
+    def _joins(cls):
+        pool = Pool()
+        Line = pool.get('purchase.line')
+        Purchase = pool.get('purchase.purchase')
+
+        tables = {}
+        tables['line'] = line = Line.__table__()
+        tables['line.purchase'] = purchase = Purchase.__table__()
+
+        from_item = (line
+            .join(purchase, condition=line.purchase == purchase.id))
+        return from_item, tables
+
+    @classmethod
+    def _columns(cls, tables):
+        line = tables['line']
+        purchase = tables['line.purchase']
+
+        quantity = Coalesce(line.actual_quantity, line.quantity)
+        expense = cls.expense.sql_cast(
+            Sum(quantity * line.unit_price))
+        return [
+            cls._column_id(tables).as_('id'),
+            Literal(0).as_('create_uid'),
+            CurrentTimestamp().as_('create_date'),
+            cls.write_uid.sql_cast(Literal(Null)).as_('write_uid'),
+            cls.write_date.sql_cast(Literal(Null)).as_('write_date'),
+            purchase.company.as_('company'),
+            purchase.currency.as_('currency'),
+            expense.as_('expense'),
+            Count(purchase.id, distinct=True).as_('number'),
+            ]
+
+    @classmethod
+    def _column_id(cls, tables):
+        line = tables['line']
+        return Min(line.id)
+
+    @classmethod
+    def _group_by(cls, tables):
+        purchase = tables['line.purchase']
+        return [purchase.company, purchase.currency]
+
+    @classmethod
+    def _where(cls, tables):
+        context = Transaction().context
+        purchase = tables['line.purchase']
+
+        where = purchase.company == context.get('company')
+        where &= purchase.state.in_(cls._purchase_states())
+        from_date = context.get('from_date')
+        if from_date:
+            where &= purchase.purchase_date >= from_date
+        to_date = context.get('to_date')
+        if to_date:
+            where &= purchase.purchase_date <= to_date
+        warehouse = context.get('warehouse')
+        if warehouse:
+            where &= purchase.warehouse == warehouse
+        return where
+
+    @classmethod
+    def _purchase_states(cls):
+        return ['confirmed', 'processing', 'done']
+
+    @property
+    def time_series_all(self):
+        delta = self._period_delta()
+        for ts, next_ts in pairwise(self.time_series or []):
+            yield ts
+            if delta and next_ts:
+                date = ts.date + delta
+                while date < next_ts.date:
+                    yield None
+                    date += delta
+
+    @classmethod
+    def _period_delta(cls):
+        context = Transaction().context
+        return {
+            'year': relativedelta(years=1),
+            'month': relativedelta(months=1),
+            'day': relativedelta(days=1),
+            }.get(context.get('period'))
+
+    def get_trend(self, name):
+        name = name[:-len('_trend')]
+        if pygal:
+            chart = pygal.Line()
+            chart.add('', [getattr(ts, name) if ts else 0
+                    for ts in self.time_series_all])
+            return chart.render_sparktext()
+
+
+class AbstractTimeseries(Abstract):
+
+    date = fields.Date(lazy_gettext('purchase.msg_purchase_reporting_date'))
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order = [('date', 'ASC')]
+
+    @classmethod
+    def _columns(cls, tables):
+        return super()._columns(tables) + [
+            cls._column_date(tables).as_('date')]
+
+    @classmethod
+    def _column_date(cls, tables):
+        context = Transaction().context
+        purchase = tables['line.purchase']
+        date = DateTrunc(context.get('period'), purchase.purchase_date)
+        date = cls.date.sql_cast(date)
+        return date
+
+    @classmethod
+    def _group_by(cls, tables):
+        return super()._group_by(tables) + [cls._column_date(tables)]
+
+
+class Context(ModelView):
+    "Purchase Reporting Context"
+    __name__ = 'purchase.reporting.context'
+
+    company = fields.Many2One('company.company', "Company", required=True)
+    from_date = fields.Date("From Date",
+        domain=[
+            If(Eval('to_date') & Eval('from_date'),
+                ('from_date', '<=', Eval('to_date')),
+                ()),
+            ],
+        depends=['to_date'])
+    to_date = fields.Date("To Date",
+        domain=[
+            If(Eval('from_date') & Eval('to_date'),
+                ('to_date', '>=', Eval('from_date')),
+                ()),
+            ],
+        depends=['from_date'])
+    period = fields.Selection([
+            ('year', "Year"),
+            ('month', "Month"),
+            ('day', "Day"),
+            ], "Period", required=True)
+    warehouse = fields.Many2One(
+        'stock.location', "Warehouse",
+        domain=[
+            ('type', '=', 'warehouse'),
+            ])
+
+    @classmethod
+    def default_company(cls):
+        return Transaction().context.get('company')
+
+    @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(years=1)
+
+    @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()
+
+    @classmethod
+    def default_period(cls):
+        return Transaction().context.get('period', 'month')
+
+    @classmethod
+    def default_warehouse(cls):
+        return Transaction().context.get('warehouse')
+
+
+class Main(Abstract, ModelView):
+    "Purchase Reporting"
+    __name__ = 'purchase.reporting.main'
+
+    time_series = fields.Function(fields.One2Many(
+            'purchase.reporting.main.time_series', None,
+            lazy_gettext('purchase.msg_purchase_reporting_time_series')),
+        'get_time_series')
+
+    def get_rec_name(self, name):
+        return ''
+
+    def get_time_series(self, name):
+        pool = Pool()
+        Timeseries = pool.get('purchase.reporting.main.time_series')
+        return [t.id for t in Timeseries.search([])]
+
+
+class MainTimeseries(AbstractTimeseries, ModelView):
+    "Purchase Reporting"
+    __name__ = 'purchase.reporting.main.time_series'
+
+
+class SupplierMixin(object):
+    __slots__ = ()
+    supplier = fields.Many2One(
+        'party.party', "Supplier",
+        context={
+            'company': Eval('company', -1),
+            },
+        depends=['company'])
+
+    @classmethod
+    def _columns(cls, tables):
+        purchase = tables['line.purchase']
+        return super()._columns(tables) + [purchase.party.as_('supplier')]
+
+    @classmethod
+    def _group_by(cls, tables):
+        purchase = tables['line.purchase']
+        return super()._group_by(tables) + [purchase.party]
+
+    def get_rec_name(self, name):
+        return self.supplier.rec_name
+
+
+class Supplier(SupplierMixin, Abstract, ModelView):
+    "Purchase Reporting per Supplier"
+    __name__ = 'purchase.reporting.supplier'
+
+    time_series = fields.One2Many(
+        'purchase.reporting.supplier.time_series', 'supplier',
+        lazy_gettext('purchase.msg_purchase_reporting_time_series')),
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order.insert(0, ('supplier', 'ASC'))
+
+    @classmethod
+    def _column_id(cls, tables):
+        purchase = tables['line.purchase']
+        return purchase.party
+
+
+class SupplierTimeseries(SupplierMixin, AbstractTimeseries, ModelView):
+    "Purchase Reporting per Supplier"
+    __name__ = 'purchase.reporting.supplier.time_series'
+
+
+class ProductMixin(object):
+    __slots__ = ()
+    product = fields.Many2One(
+        'product.product', "Product",
+        context={
+            'company': Eval('company', -1),
+            },
+        depends=['company'])
+    product_supplier = fields.Many2One(
+        'purchase.product_supplier', "Supplier's Product")
+
+    @classmethod
+    def _columns(cls, tables):
+        line = tables['line']
+        return super()._columns(tables) + [
+            line.product.as_('product'),
+            line.product_supplier.as_('product_supplier'),
+            ]
+
+    @classmethod
+    def _group_by(cls, tables):
+        line = tables['line']
+        return super()._group_by(tables) + [
+            line.product, line.product_supplier]
+
+    @classmethod
+    def _where(cls, tables):
+        context = Transaction().context
+        line = tables['line']
+        purchase = tables['line.purchase']
+        where = super()._where(tables)
+        where &= line.product != Null
+        where &= purchase.party == context.get('supplier')
+        return where
+
+    def get_rec_name(self, name):
+        pool = Pool()
+        Party = pool.get('party.party')
+        context = Transaction().context
+
+        name = self.product.rec_name if self.product else None
+        if context.get('supplier'):
+            supplier = Party(context['supplier'])
+            name += '@%s' % supplier.rec_name
+        return name
+
+
+class Product(ProductMixin, Abstract, ModelView):
+    "Purchase Reporting per Product"
+    __name__ = 'purchase.reporting.product'
+
+    time_series = fields.One2Many(
+        'purchase.reporting.product.time_series', 'product',
+        lazy_gettext('purchase.msg_purchase_reporting_time_series')),
+
+    @classmethod
+    def __setup__(cls):
+        super().__setup__()
+        cls._order.insert(0, ('product', 'ASC'))
+
+    @classmethod
+    def _column_id(cls, tables):
+        line = tables['line']
+        return line.product
+
+
+class ProductTimeseries(ProductMixin, AbstractTimeseries, ModelView):
+    "Purchase Reporting per Product"
+    __name__ = 'purchase.reporting.product.time_series'
diff -r 3c8974df1eef -r 49a612454113 purchase_reporting.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/purchase_reporting.xml    Fri Oct 07 15:00:40 2022 +0200
@@ -0,0 +1,456 @@
+<?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>
+
+        <menuitem parent="menu_reporting"
+            name="Purchases"
+            sequence="10"
+            id="menu_reporting_purchase"
+            icon="tryton-graph"/>
+
+        <record model="ir.ui.view" id="reporting_context_view_form">
+            <field name="model">purchase.reporting.context</field>
+            <field name="type">form</field>
+            <field name="name">purchase_reporting_context_form</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_main_view_list">
+            <field name="model">purchase.reporting.main</field>
+            <field name="type">tree</field>
+            <field name="name">purchase_reporting_main_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_main_view_graph_expense">
+            <field name="model">purchase.reporting.main</field>
+            <field name="type">graph</field>
+            <field name="name">purchase_reporting_main_graph_expense</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_main_view_graph_number">
+            <field name="model">purchase.reporting.main</field>
+            <field name="type">graph</field>
+            <field name="name">purchase_reporting_main_graph_number</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_main">
+            <field name="name">Purchases</field>
+            <field name="res_model">purchase.reporting.main</field>
+            <field name="context_model">purchase.reporting.context</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_main_view_list"/>
+            <field name="act_window" ref="act_reporting_main"/>
+        </record>
+        <record model="ir.action.keyword" id="act_reporting_main_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model" ref="menu_reporting_purchase"/>
+            <field name="action" ref="act_reporting_main"/>
+        </record>
+
+        <record model="ir.rule.group" id="rule_group_reporting_main_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'purchase.reporting.main')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_main_companies">
+            <field name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_main_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_main">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.main')]"/>
+            <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_main_purchase">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.main')]"/>
+            <field name="group" ref="group_purchase"/>
+            <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_main_time_series_view_list">
+            <field name="model">purchase.reporting.main.time_series</field>
+            <field name="type">tree</field>
+            <field name="name">purchase_reporting_main_time_series_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_main_time_series_view_graph_expense">
+            <field name="model">purchase.reporting.main.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">purchase_reporting_main_time_series_graph_expense</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_main_time_series_view_graph_number">
+            <field name="model">purchase.reporting.main.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">purchase_reporting_main_time_series_graph_number</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_main_time_series">
+            <field name="name">Purchases</field>
+            <field name="res_model">purchase.reporting.main.time_series</field>
+            <field name="context_model">purchase.reporting.context</field>
+            <field name="order" eval="[('date', 'DESC')]" pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_time_series_list_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_main_time_series_view_list"/>
+            <field name="act_window" ref="act_reporting_main_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_time_series_list_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" 
ref="reporting_main_time_series_view_graph_expense"/>
+            <field name="act_window" ref="act_reporting_main_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_main_time_series_list_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" 
ref="reporting_main_time_series_view_graph_number"/>
+            <field name="act_window" ref="act_reporting_main_time_series"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_main_time_series_list_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model">purchase.reporting.main,-1</field>
+            <field name="action" ref="act_reporting_main_time_series"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_main_time_series_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'purchase.reporting.main.time_series')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_main_time_series_companies">
+            <field name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_main_time_series_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_main_time_series">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.main.time_series')]"/>
+            <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_main_time_series_purchase">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.main.time_series')]"/>
+            <field name="group" ref="group_purchase"/>
+            <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>
+
+        <!-- Supplier -->
+
+        <record model="ir.ui.view" id="reporting_supplier_view_list">
+            <field name="model">purchase.reporting.supplier</field>
+            <field name="inherit" ref="reporting_main_view_list"/>
+            <field name="name">purchase_reporting_supplier_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_supplier_view_graph_expense">
+            <field name="model">purchase.reporting.supplier</field>
+            <field name="inherit" ref="reporting_main_view_graph_expense"/>
+            <field 
name="name">purchase_reporting_supplier_graph_expense</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_supplier_view_graph_number">
+            <field name="model">purchase.reporting.supplier</field>
+            <field name="inherit" ref="reporting_main_view_graph_number"/>
+            <field name="name">purchase_reporting_supplier_graph_number</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_supplier">
+            <field name="name">Purchases per Supplier</field>
+            <field name="res_model">purchase.reporting.supplier</field>
+            <field name="context_model">purchase.reporting.context</field>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_supplier_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_supplier_view_list"/>
+            <field name="act_window" ref="act_reporting_supplier"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_supplier_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="reporting_supplier_view_graph_expense"/>
+            <field name="act_window" ref="act_reporting_supplier"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_supplier_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" ref="reporting_supplier_view_graph_number"/>
+            <field name="act_window" ref="act_reporting_supplier"/>
+        </record>
+        <record model="ir.action.keyword" id="act_reporting_supplier_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model" ref="menu_reporting_purchase"/>
+            <field name="action" ref="act_reporting_supplier"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_supplier_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'purchase.reporting.supplier')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_supplier_companies">
+            <field name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_supplier_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_supplier">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.supplier')]"/>
+            <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_supplier_purchase">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.supplier')]"/>
+            <field name="group" ref="group_purchase"/>
+            <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_supplier_time_series_view_list">
+            <field name="model">purchase.reporting.supplier.time_series</field>
+            <field name="type">tree</field>
+            <field name="name">purchase_reporting_main_time_series_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_supplier_time_series_view_graph_expense">
+            <field name="model">purchase.reporting.supplier.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">purchase_reporting_main_time_series_graph_expense</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_supplier_time_series_view_graph_number">
+            <field name="model">purchase.reporting.supplier.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">purchase_reporting_main_time_series_graph_number</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_supplier_time_series">
+            <field name="name">Purchases per Supplier</field>
+            <field 
name="res_model">purchase.reporting.supplier.time_series</field>
+            <field name="context_model">purchase.reporting.context</field>
+            <field
+                name="domain"
+                eval="[('supplier', '=', Eval('active_id', -1))]"
+                pyson="1"/>
+            <field name="order" eval="[('date', 'DESC')]" pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_supplier_time_series_list_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_supplier_time_series_view_list"/>
+            <field name="act_window" ref="act_reporting_supplier_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_supplier_time_series_list_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" 
ref="reporting_supplier_time_series_view_graph_expense"/>
+            <field name="act_window" ref="act_reporting_supplier_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_supplier_time_series_list_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" 
ref="reporting_supplier_time_series_view_graph_number"/>
+            <field name="act_window" ref="act_reporting_supplier_time_series"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_supplier_time_series_list_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model">purchase.reporting.supplier,-1</field>
+            <field name="action" ref="act_reporting_supplier_time_series"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_supplier_time_series_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'purchase.reporting.supplier.time_series')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" 
id="rule_reporting_supplier_time_series_companies">
+            <field name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_supplier_time_series_companies"/>
+        </record>
+
+        <record model="ir.model.access" 
id="access_reporting_supplier_time_series">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.supplier.time_series')]"/>
+            <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_supplier_time_series_purchase">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.supplier.time_series')]"/>
+            <field name="group" ref="group_purchase"/>
+            <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>
+
+        <!-- Product -->
+
+        <record model="ir.ui.view" id="reporting_product_view_list">
+            <field name="model">purchase.reporting.product</field>
+            <field name="inherit" ref="reporting_main_view_list"/>
+            <field name="name">purchase_reporting_product_list</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_product_view_graph_expense">
+            <field name="model">purchase.reporting.product</field>
+            <field name="inherit" ref="reporting_main_view_graph_expense"/>
+            <field name="name">purchase_reporting_product_graph_expense</field>
+        </record>
+
+        <record model="ir.ui.view" id="reporting_product_view_graph_number">
+            <field name="model">purchase.reporting.product</field>
+            <field name="inherit" ref="reporting_main_view_graph_number"/>
+            <field name="name">purchase_reporting_product_graph_number</field>
+        </record>
+
+        <record model="ir.action.act_window" id="act_reporting_product">
+            <field name="name">Purchases per Product</field>
+            <field name="res_model">purchase.reporting.product</field>
+            <field name="context_model">purchase.reporting.context</field>
+            <field name="context" eval="{'supplier': Eval('active_id')}" 
pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_product_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_product_view_list"/>
+            <field name="act_window" ref="act_reporting_product"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_product_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" ref="reporting_product_view_graph_expense"/>
+            <field name="act_window" ref="act_reporting_product"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_product_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" ref="reporting_product_view_graph_number"/>
+            <field name="act_window" ref="act_reporting_product"/>
+        </record>
+        <record model="ir.action.keyword" id="act_reporting_product_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model">purchase.reporting.supplier,-1</field>
+            <field name="action" ref="act_reporting_product"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_product_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'purchase.reporting.product')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" id="rule_reporting_product_companies">
+            <field name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_product_companies"/>
+        </record>
+
+        <record model="ir.model.access" id="access_reporting_product">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.product')]"/>
+            <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_product_purchase">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.product')]"/>
+            <field name="group" ref="group_purchase"/>
+            <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_product_time_series_view_list">
+            <field name="model">purchase.reporting.product.time_series</field>
+            <field name="type">tree</field>
+            <field name="name">purchase_reporting_main_time_series_list</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_product_time_series_view_graph_expense">
+            <field name="model">purchase.reporting.product.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">purchase_reporting_main_time_series_graph_expense</field>
+        </record>
+
+        <record model="ir.ui.view" 
id="reporting_product_time_series_view_graph_number">
+            <field name="model">purchase.reporting.product.time_series</field>
+            <field name="type">graph</field>
+            <field 
name="name">purchase_reporting_main_time_series_graph_number</field>
+        </record>
+
+        <record model="ir.action.act_window" 
id="act_reporting_product_time_series">
+            <field name="name">Purchases per Product</field>
+            <field 
name="res_model">purchase.reporting.product.time_series</field>
+            <field name="context_model">purchase.reporting.context</field>
+            <field name="domain"
+                eval="[('product', '=', Eval('active_id', -1))]"
+                pyson="1"/>
+            <field name="order" eval="[('date', 'DESC')]" pyson="1"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_product_time_series_list_view1">
+            <field name="sequence" eval="10"/>
+            <field name="view" ref="reporting_product_time_series_view_list"/>
+            <field name="act_window" ref="act_reporting_product_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_product_time_series_list_view2">
+            <field name="sequence" eval="20"/>
+            <field name="view" 
ref="reporting_product_time_series_view_graph_expense"/>
+            <field name="act_window" ref="act_reporting_product_time_series"/>
+        </record>
+        <record model="ir.action.act_window.view" 
id="act_reporting_product_time_series_list_view3">
+            <field name="sequence" eval="30"/>
+            <field name="view" 
ref="reporting_product_time_series_view_graph_number"/>
+            <field name="act_window" ref="act_reporting_product_time_series"/>
+        </record>
+        <record model="ir.action.keyword" 
id="act_reporting_product_time_series_list_keyword1">
+            <field name="keyword">tree_open</field>
+            <field name="model">purchase.reporting.product,-1</field>
+            <field name="action" ref="act_reporting_product_time_series"/>
+        </record>
+
+        <record model="ir.rule.group" 
id="rule_group_reporting_product_time_series_companies">
+            <field name="name">User in companies</field>
+            <field name="model" search="[('model', '=', 
'purchase.reporting.product.time_series')]"/>
+            <field name="global_p" eval="True"/>
+        </record>
+        <record model="ir.rule" 
id="rule_reporting_product_time_series_companies">
+            <field name="domain"
+                eval="[('company', 'in', Eval('companies', []))]"
+                pyson="1"/>
+            <field name="rule_group" 
ref="rule_group_reporting_product_time_series_companies"/>
+        </record>
+
+        <record model="ir.model.access" 
id="access_reporting_product_time_series">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.product.time_series')]"/>
+            <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_product_time_series_purchase">
+            <field name="model" search="[('model', '=', 
'purchase.reporting.product.time_series')]"/>
+            <field name="group" ref="group_purchase"/>
+            <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 3c8974df1eef -r 49a612454113 setup.py
--- a/setup.py  Sat Oct 01 15:35:33 2022 +0200
+++ b/setup.py  Fri Oct 07 15:00:40 2022 +0200
@@ -142,6 +142,7 @@
     python_requires='>=3.7',
     install_requires=requires,
     extras_require={
+        'sparklines': ['pygal'],
         'test': tests_require,
         },
     dependency_links=dependency_links,
diff -r 3c8974df1eef -r 49a612454113 tests/scenario_purchase_reporting.rst
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/tests/scenario_purchase_reporting.rst     Fri Oct 07 15:00:40 2022 +0200
@@ -0,0 +1,154 @@
+===========================
+Purchase Reporting Scenario
+===========================
+
+Imports::
+
+    >>> from dateutil.relativedelta import relativedelta
+    >>> from decimal import Decimal
+    >>> from proteus import Model
+    >>> from trytond.tests.tools import activate_modules
+    >>> from trytond.modules.company.tests.tools import (
+    ...     create_company, get_company)
+    >>> from trytond.modules.account.tests.tools import (
+    ...     create_fiscalyear, create_chart, get_accounts)
+    >>> from trytond.modules.account_invoice.tests.tools import (
+    ...     set_fiscalyear_invoice_sequences)
+
+Activate modules::
+
+    >>> config = activate_modules('purchase')
+    >>> Party = Model.get('party.party')
+    >>> ProductUom = Model.get('product.uom')
+    >>> ProductTemplate = Model.get('product.template')
+    >>> Purchase = Model.get('purchase.purchase')
+    >>> Supplier = Model.get('purchase.reporting.supplier')
+    >>> SupplierTimeseries = Model.get(
+    ...     'purchase.reporting.supplier.time_series')
+    >>> Product = Model.get('purchase.reporting.product')
+    >>> ProductTimeseries = Model.get('purchase.reporting.product.time_series')
+
+Create company::
+
+    >>> _ = create_company()
+    >>> company = get_company()
+
+Create fiscal year::
+
+    >>> fiscalyear = set_fiscalyear_invoice_sequences(
+    ...     create_fiscalyear(company))
+    >>> fiscalyear.click('create_period')
+
+Create chart of accounts::
+
+    >>> _ = create_chart(company)
+    >>> accounts = get_accounts(company)
+    >>> expense = accounts['expense']
+
+Create parties::
+
+    >>> supplier1 = Party(name='Supplier1')
+    >>> supplier1.save()
+    >>> supplier2 = Party(name='Supplier2')
+    >>> supplier2.save()
+
+Create products::
+
+    >>> unit, = ProductUom.find([('name', '=', 'Unit')])
+
+    >>> template1 = ProductTemplate()
+    >>> template1.name = "Product1"
+    >>> template1.default_uom = unit
+    >>> template1.type = 'service'
+    >>> template1.purchasable = True
+    >>> template1.list_price = Decimal('20')
+    >>> template1.save()
+    >>> product1, = template1.products
+    >>> product1.cost_price = Decimal('10')
+    >>> product1.save()
+
+    >>> template2, = template1.duplicate(default={'name': "Product2"})
+    >>> product2, = template2.products
+    >>> product2.cost_price = Decimal('10')
+    >>> product2.save()
+
+Create purchases::
+
+    >>> purchase1 = Purchase()
+    >>> purchase1.party = supplier1
+    >>> purchase1.purchase_date = fiscalyear.start_date
+    >>> line = purchase1.lines.new()
+    >>> line.product = product1
+    >>> line.quantity = 2
+    >>> line = purchase1.lines.new()
+    >>> line.product = product2
+    >>> line.quantity = 1
+    >>> purchase1.click('quote')
+    >>> purchase1.click('confirm')
+
+    >>> purchase2 = Purchase()
+    >>> purchase2.party = supplier2
+    >>> purchase2.purchase_date = (
+    ...     fiscalyear.start_date + relativedelta(months=1))
+    >>> line = purchase2.lines.new()
+    >>> line.product = product1
+    >>> line.quantity = 1
+    >>> purchase2.click('quote')
+    >>> purchase2.click('confirm')
+
+Check purchase reporting per supplier::
+
+    >>> context = dict(
+    ...         from_date=fiscalyear.start_date,
+    ...         to_date=fiscalyear.end_date,
+    ...         period='month')
+    >>> with config.set_context(context=context):
+    ...     reports = Supplier.find([])
+    ...     time_series = SupplierTimeseries.find([])
+    >>> len(reports)
+    2
+    >>> with config.set_context(context=context):
+    ...     sorted((r.supplier.id, r.number, r.expense) for r in reports) == \
+    ...     sorted([(supplier1.id, 1, Decimal('30')),
+    ...             (supplier2.id, 1, Decimal('10'))])
+    True
+    >>> len(time_series)
+    2
+    >>> purchase1_ts_date = purchase1.purchase_date.replace(day=1)
+    >>> purchase2_ts_date = purchase2.purchase_date.replace(day=1)
+    >>> with config.set_context(context=context):
+    ...     sorted((r.supplier.id, r.date, r.number, r.expense)
+    ...         for r in time_series) == sorted(
+    ...     [(supplier1.id, purchase1_ts_date, 1, Decimal('30')),
+    ...     (supplier2.id, purchase2_ts_date, 1, Decimal('10'))])
+    True
+
+Check purchase reporting per product without supplier::
+
+    >>> with config.set_context(context=context):
+    ...     reports = Product.find([])
+    ...     time_series = ProductTimeseries.find([])
+    >>> len(reports)
+    0
+
+Check purchase reporting per product with supplier::
+
+    >>> context['supplier'] = supplier1.id
+    >>> with config.set_context(context=context):
+    ...     reports = Product.find([])
+    ...     time_series = ProductTimeseries.find([])
+    >>> len(reports)
+    2
+    >>> with config.set_context(context=context):
+    ...     sorted((r.product.id, r.number, r.expense) for r in reports) == \
+    ...     sorted([(product1.id, 1, Decimal('20')),
+    ...         (product2.id, 1, Decimal('10'))])
+    True
+    >>> len(time_series)
+    2
+    >>> with config.set_context(context=context):
+    ...     sorted((r.product.id, r.date, r.number, r.expense)
+    ...         for r in time_series) == sorted(
+    ...     [(product1.id, purchase1_ts_date, 1, Decimal('20')),
+    ...     (product2.id, purchase1_ts_date, 1, Decimal('10'))])
+    True
diff -r 3c8974df1eef -r 49a612454113 tryton.cfg
--- a/tryton.cfg        Sat Oct 01 15:35:33 2022 +0200
+++ b/tryton.cfg        Fri Oct 07 15:00:40 2022 +0200
@@ -14,6 +14,7 @@
     stock
 xml:
     purchase.xml
+    purchase_reporting.xml
     configuration.xml
     party.xml
     stock.xml
diff -r 3c8974df1eef -r 49a612454113 view/purchase_reporting_context_form.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_context_form.xml  Fri Oct 07 15:00:40 2022 +0200
@@ -0,0 +1,17 @@
+<?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>
+    <group id="dates" colspan="2" col="4">
+        <label name="from_date"/>
+        <field name="from_date"/>
+        <label name="to_date"/>
+        <field name="to_date"/>
+    </group>
+    <label name="period"/>
+    <field name="period"/>
+    <label name="company"/>
+    <field name="company"/>
+    <label name="warehouse"/>
+    <field name="warehouse"/>
+</form>
diff -r 3c8974df1eef -r 49a612454113 
view/purchase_reporting_main_graph_expense.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_main_graph_expense.xml    Fri Oct 07 15:00:40 
2022 +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>
+    <x>
+        <field name="id"/>
+    </x>
+    <y>
+        <field name="expense"/>
+    </y>
+</graph>
diff -r 3c8974df1eef -r 49a612454113 
view/purchase_reporting_main_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_main_graph_number.xml     Fri Oct 07 15:00:40 
2022 +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>
+    <x>
+        <field name="id"/>
+    </x>
+    <y>
+        <field name="number"/>
+    </y>
+</graph>
diff -r 3c8974df1eef -r 49a612454113 view/purchase_reporting_main_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_main_list.xml     Fri Oct 07 15:00:40 2022 +0200
@@ -0,0 +1,8 @@
+<?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 keyword_open="1">
+    <field name="number" sum="Number"/>
+    <field name="expense" sum="Expense"/>
+    <field name="expense_trend" expand="1"/>
+</tree>
diff -r 3c8974df1eef -r 49a612454113 
view/purchase_reporting_main_time_series_graph_expense.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_main_time_series_graph_expense.xml        Fri Oct 
07 15:00:40 2022 +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>
+    <x>
+        <field name="date"/>
+    </x>
+    <y>
+        <field name="expense"/>
+    </y>
+</graph>
diff -r 3c8974df1eef -r 49a612454113 
view/purchase_reporting_main_time_series_graph_number.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_main_time_series_graph_number.xml Fri Oct 07 
15:00:40 2022 +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>
+    <x>
+        <field name="date"/>
+    </x>
+    <y>
+        <field name="number"/>
+    </y>
+</graph>
diff -r 3c8974df1eef -r 49a612454113 
view/purchase_reporting_main_time_series_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_main_time_series_list.xml Fri Oct 07 15:00:40 
2022 +0200
@@ -0,0 +1,8 @@
+<?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="date"/>
+    <field name="number" sum="Number"/>
+    <field name="expense" sum="Expense"/>
+</tree>
diff -r 3c8974df1eef -r 49a612454113 view/purchase_reporting_product_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_product_list.xml  Fri Oct 07 15:00:40 2022 +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. -->
+<data>
+    <xpath expr="//field[@name='number']" position="before">
+        <field name="product" expand="1"/>
+        <field name="product_supplier" expand="1"/>
+    </xpath>
+</data>
diff -r 3c8974df1eef -r 49a612454113 view/purchase_reporting_supplier_list.xml
--- /dev/null   Thu Jan 01 00:00:00 1970 +0000
+++ b/view/purchase_reporting_supplier_list.xml Fri Oct 07 15:00:40 2022 +0200
@@ -0,0 +1,8 @@
+<?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. -->
+<data>
+    <xpath expr="//field[@name='number']" position="before">
+        <field name="supplier" expand="1"/>
+    </xpath>
+</data>

Reply via email to