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>