changeset fcb961429f35 in modules/stock:default details: https://hg.tryton.org/modules/stock?cmd=changeset;node=fcb961429f35 description: Add margin reporting
issue9665 review292371002 diffstat: CHANGELOG | 1 + __init__.py | 7 + message.xml | 24 + setup.py | 3 + stock_reporting_margin.py | 629 ++++++++++++ stock_reporting_margin.xml | 360 ++++++ tests/scenario_stock_reporting_margin.rst | 215 ++++ tests/test_stock.py | 5 + tryton.cfg | 1 + view/reporting_margin_category_graph_amount.xml | 12 + view/reporting_margin_category_graph_margin.xml | 11 + view/reporting_margin_category_graph_profit.xml | 11 + view/reporting_margin_category_list.xml | 13 + view/reporting_margin_category_time_series_graph_amount.xml | 12 + view/reporting_margin_category_time_series_graph_margin.xml | 11 + view/reporting_margin_category_time_series_graph_profit.xml | 11 + view/reporting_margin_category_time_series_list.xml | 12 + view/reporting_margin_category_tree.xml | 12 + view/reporting_margin_context_form.xml | 20 + view/reporting_margin_product_graph_amount.xml | 12 + view/reporting_margin_product_graph_margin.xml | 11 + view/reporting_margin_product_graph_profit.xml | 11 + view/reporting_margin_product_list.xml | 14 + view/reporting_margin_product_time_series_graph_amount.xml | 12 + view/reporting_margin_product_time_series_graph_margin.xml | 11 + view/reporting_margin_product_time_series_graph_profit.xml | 11 + view/reporting_margin_product_time_series_list.xml | 13 + 27 files changed, 1465 insertions(+), 0 deletions(-) diffs (1607 lines): diff -r c9582871ec46 -r fcb961429f35 CHANGELOG --- a/CHANGELOG Sun Jan 17 00:43:31 2021 +0100 +++ b/CHANGELOG Sun Jan 17 00:57:09 2021 +0100 @@ -1,3 +1,4 @@ +* Add margin reporting * Add products_by_locations Model Version 5.8.0 - 2020-11-02 diff -r c9582871ec46 -r fcb961429f35 __init__.py --- a/__init__.py Sun Jan 17 00:43:31 2021 +0100 +++ b/__init__.py Sun Jan 17 00:57:09 2021 +0100 @@ -12,6 +12,7 @@ from . import party from . import ir from . import res +from . import stock_reporting_margin from .move import StockMixin @@ -55,6 +56,12 @@ configuration.ConfigurationLocation, ir.Cron, res.User, + stock_reporting_margin.Context, + stock_reporting_margin.Product, + stock_reporting_margin.ProductTimeseries, + stock_reporting_margin.Category, + stock_reporting_margin.CategoryTimeseries, + stock_reporting_margin.CategoryTree, module='stock', type_='model') Pool.register( shipment.Assign, diff -r c9582871ec46 -r fcb961429f35 message.xml --- a/message.xml Sun Jan 17 00:43:31 2021 +0100 +++ b/message.xml Sun Jan 17 00:57:09 2021 +0100 @@ -92,5 +92,29 @@ <record model="ir.message" id="msg_inventory_date_in_the_future"> <field name="text">The inventories "%(inventories)s" have dates in the future.</field> </record> + <record model="ir.message" id="msg_stock_reporting_company"> + <field name="text">Company</field> + </record> + <record model="ir.message" id="msg_stock_reporting_number"> + <field name="text">Number</field> + </record> + <record model="ir.message" id="msg_stock_reporting_cost"> + <field name="text">Cost</field> + </record> + <record model="ir.message" id="msg_stock_reporting_revenue"> + <field name="text">Revenue</field> + </record> + <record model="ir.message" id="msg_stock_reporting_profit"> + <field name="text">Profit</field> + </record> + <record model="ir.message" id="msg_stock_reporting_margin"> + <field name="text">Margin</field> + </record> + <record model="ir.message" id="msg_stock_reporting_margin_trend"> + <field name="text">Margin Trend</field> + </record> + <record model="ir.message" id="msg_stock_reporting_currency_digits"> + <field name="text">Currency Digits</field> + </record> </data> </tryton> diff -r c9582871ec46 -r fcb961429f35 setup.py --- a/setup.py Sun Jan 17 00:43:31 2021 +0100 +++ b/setup.py Sun Jan 17 00:57:09 2021 +0100 @@ -137,6 +137,9 @@ license='GPL-3', python_requires='>=3.6', install_requires=requires, + extras_require={ + 'sparklines': ['pygal'], + }, dependency_links=dependency_links, zip_safe=False, entry_points=""" diff -r c9582871ec46 -r fcb961429f35 stock_reporting_margin.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/stock_reporting_margin.py Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,629 @@ +# 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 decimal import Decimal +from itertools import tee, zip_longest + +from sql import Null, Literal, With +from sql.aggregate import Sum, Min, Max +from sql.conditionals import Case +from sql.functions import CurrentTimestamp, DateTrunc, Power, Ceil, Log, Round + +try: + import pygal +except ImportError: + pygal = None +from dateutil.relativedelta import relativedelta + +from trytond.i18n import lazy_gettext +from trytond.model import ModelSQL, ModelView, fields +from trytond.pool import Pool +from trytond.pyson import Eval, If +from trytond.tools import grouped_slice, reduce_ids +from trytond.transaction import Transaction + + +def pairwise(iterable): + a, b = tee(iterable) + next(b) + return zip_longest(a, b) + + +class Abstract(ModelSQL, ModelView): + + company = fields.Many2One( + 'company.company', lazy_gettext('stock.msg_stock_reporting_company')) + cost = fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_cost'), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + revenue = fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_revenue'), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + profit = fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_profit'), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + margin = fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_margin'), + digits=(14, 4), + states={ + 'invisible': ~Eval('margin'), + }, + depends=['currency_digits']) + margin_trend = fields.Function(fields.Char( + lazy_gettext('stock.msg_stock_reporting_margin_trend')), + 'get_trend') + time_series = None + + currency = fields.Many2One('currency.currency', "Currency") + currency_digits = fields.Integer( + lazy_gettext('stock.msg_stock_reporting_currency_digits')) + + @classmethod + def table_query(cls): + from_item, tables, withs = cls._joins() + return from_item.select(*cls._columns(tables, withs), + where=cls._where(tables, withs), + group_by=cls._group_by(tables, withs), + with_=withs.values()) + + @classmethod + def _joins(cls): + pool = Pool() + Company = pool.get('company.company') + Currency = pool.get('currency.currency') + Move = pool.get('stock.move') + Location = pool.get('stock.location') + + tables = {} + tables['move'] = move = Move.__table__() + tables['move.company'] = company = Company.__table__() + tables['move.company.currency'] = currency = Currency.__table__() + tables['move.from_location'] = from_location = Location.__table__() + tables['move.to_location'] = to_location = Location.__table__() + withs = {} + withs['currency_rate'] = currency_rate = With( + query=Currency.currency_rate_sql()) + withs['currency_rate_company'] = currency_rate_company = With( + query=Currency.currency_rate_sql()) + + from_item = (move + .join(currency_rate, + condition=(move.currency == currency_rate.currency) + & (currency_rate.start_date <= move.effective_date) + & ((currency_rate.end_date == Null) + | (currency_rate.end_date >= move.effective_date)) + ) + .join(company, + condition=move.company == company.id) + .join(currency, + condition=company.currency == currency.id) + .join(currency_rate_company, + condition=(company.currency == currency_rate_company.currency) + & (currency_rate_company.start_date <= move.effective_date) + & ((currency_rate_company.end_date == Null) + | (currency_rate_company.end_date >= move.effective_date)) + ) + .join(from_location, + condition=(move.from_location == from_location.id)) + .join(to_location, + condition=(move.to_location == to_location.id))) + return from_item, tables, withs + + @classmethod + def _columns(cls, tables, withs): + move = tables['move'] + from_location = tables['move.from_location'] + to_location = tables['move.to_location'] + currency = tables['move.company.currency'] + + sign = Case( + (from_location.type.in_(cls._to_location_types()) + & to_location.type.in_(cls._from_location_types()), + -1), + else_=1) + cost = cls._column_cost(tables, withs, sign) + revenue = cls._column_revenue(tables, withs, sign) + profit = revenue - cost + margin = Case( + (revenue != 0, profit / revenue), + else_=Null) + return [ + cls._column_id(tables, withs).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'), + move.company.as_('company'), + cls.cost.sql_cast( + Round(cost, currency.digits)).as_('cost'), + cls.revenue.sql_cast( + Round(revenue, currency.digits)).as_('revenue'), + cls.profit.sql_cast( + Round(profit, currency.digits)).as_('profit'), + cls.margin.sql_cast( + Round(margin, cls.margin.digits[1])).as_('margin'), + currency.id.as_('currency'), + currency.digits.as_('currency_digits'), + ] + + @classmethod + def _column_id(cls, tables, withs): + move = tables['move'] + return Min(move.id) + + @classmethod + def _column_cost(cls, tables, withs, sign): + move = tables['move'] + return Sum( + sign * cls.cost.sql_cast(move.internal_quantity) * move.cost_price) + + @classmethod + def _column_revenue(cls, tables, withs, sign): + move = tables['move'] + currency = withs['currency_rate'] + currency_company = withs['currency_rate_company'] + return Sum( + sign * cls.revenue.sql_cast(move.quantity) * move.unit_price + * currency_company.rate / currency.rate) + + @classmethod + def _group_by(cls, tables, withs): + move = tables['move'] + currency = tables['move.company.currency'] + return [move.company, currency.id, currency.digits] + + @classmethod + def _where(cls, tables, withs): + context = Transaction().context + move = tables['move'] + from_location = tables['move.from_location'] + to_location = tables['move.to_location'] + + where = move.company == context.get('company') + where &= (( + from_location.type.in_(cls._from_location_types()) + & to_location.type.in_(cls._to_location_types())) + | ( + from_location.type.in_(cls._to_location_types()) + & to_location.type.in_(cls._from_location_types()))) + where &= move.state == 'done' + from_date = context.get('from_date') + if from_date: + where &= move.effective_date >= from_date + to_date = context.get('to_date') + if to_date: + where &= move.effective_date <= to_date + return where + + @classmethod + def _from_location_types(cls): + return ['storage', 'drop'] + + @classmethod + def _to_location_types(cls): + types = ['customer'] + if Transaction().context.get('include_lost'): + types += ['lost_found'] + return types + + @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) or 0 if ts else 0 + for ts in self.time_series_all]) + return chart.render_sparktext() + + @classmethod + def view_attributes(cls): + return super().view_attributes() + [ + ('/tree/field[@name="profit"]', 'visual', + If(Eval('profit', 0) < 0, 'danger', '')), + ('/tree/field[@name="margin"]', 'visual', + If(Eval('margin', 0) < 0, 'danger', '')), + ] + + +class AbstractTimeseries(Abstract): + + date = fields.Date("Date") + + @classmethod + def __setup__(cls): + super().__setup__() + cls._order = [('date', 'ASC')] + + @classmethod + def _columns(cls, tables, withs): + return super()._columns(tables, withs) + [ + cls._column_date(tables, withs).as_('date')] + + @classmethod + def _column_date(cls, tables, withs): + context = Transaction().context + move = tables['move'] + date = DateTrunc(context.get('period'), move.effective_date) + date = cls.date.sql_cast(date) + return date + + @classmethod + def _group_by(cls, tables, withs): + return super()._group_by(tables, withs) + [ + cls._column_date(tables, withs)] + + +class Context(ModelView): + "Stock Reporting Margin Context" + __name__ = 'stock.reporting.margin.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) + include_lost = fields.Boolean( + "Include Lost", + help="If checked, the cost of product moved " + "to a lost and found location is included.") + + @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_include_lost(cls): + return Transaction().context.get('include_lost', False) + + +class ProductMixin: + __slots__ = () + + product = fields.Many2One('product.product', "Product") + internal_quantity = fields.Float("Internal Quantity") + quantity = fields.Function(fields.Float( + "Quantity", digits=(16, Eval('unit_digits', 2)), + depends=['unit_digits']), 'get_quantity') + unit = fields.Function(fields.Many2One('product.uom', "Unit"), 'get_unit') + unit_digits = fields.Function( + fields.Integer("Unit Digits"), 'get_unit_digits') + + @classmethod + def _columns(cls, tables, withs): + move = tables['move'] + from_location = tables['move.from_location'] + to_location = tables['move.to_location'] + sign = Case( + (from_location.type.in_(cls._to_location_types()) + & to_location.type.in_(cls._from_location_types()), + -1), + else_=1) + return super()._columns(tables, withs) + [ + move.product.as_('product'), + Sum(sign * move.internal_quantity).as_('internal_quantity'), + ] + + @classmethod + def _group_by(cls, tables, withs): + move = tables['move'] + return super()._group_by(tables, withs) + [move.product] + + def get_rec_name(self, name): + return self.product.rec_name + + def get_quantity(self, name): + return self.unit.round(self.internal_quantity) + + def get_unit(self, name): + return self.product.default_uom.id + + def get_unit_digits(self, name): + return self.product.default_uom.digits + + +class Product(ProductMixin, Abstract, ModelView): + "Stock Reporting Margin per Product" + __name__ = 'stock.reporting.margin.product' + + time_series = fields.One2Many( + 'stock.reporting.margin.product.time_series', 'product', "Time Series") + + @classmethod + def __setup__(cls): + super().__setup__() + cls._order.insert(0, ('product', 'ASC')) + + @classmethod + def _column_id(cls, tables, withs): + move = tables['move'] + return move.product + + +class ProductTimeseries(ProductMixin, AbstractTimeseries, ModelView): + "Stock Reporting Margin per Product" + __name__ = 'stock.reporting.margin.product.time_series' + + +class CategoryMixin: + __slots__ = () + + category = fields.Many2One('product.category', "Category") + + @classmethod + def _joins(cls): + pool = Pool() + Product = pool.get('product.product') + TemplateCategory = pool.get('product.template-product.category.all') + from_item, tables, withs = super()._joins() + if 'move.product' not in tables: + product = Product.__table__() + tables['move.product'] = product + move = tables['move'] + from_item = (from_item + .join(product, condition=move.product == product.id)) + if 'move.product.template_category' not in tables: + template_category = TemplateCategory.__table__() + tables['move.product.template_category'] = template_category + product = tables['move.product'] + from_item = (from_item + .join(template_category, + condition=product.template == template_category.template)) + return from_item, tables, withs + + @classmethod + def _columns(cls, tables, withs): + template_category = tables['move.product.template_category'] + return super()._columns(tables, withs) + [ + template_category.category.as_('category')] + + @classmethod + def _column_id(cls, tables, withs): + pool = Pool() + Category = pool.get('product.category') + category = Category.__table__() + move = tables['move'] + template_category = tables['move.product.template_category'] + # Get a stable number of category over time + # by using number one order bigger. + nb_category = category.select( + Power(10, (Ceil(Log(Max(category.id))) + Literal(1)))) + return Min(move.id * nb_category + template_category.id) + + @classmethod + def _group_by(cls, tables, withs): + template_category = tables['move.product.template_category'] + return super()._group_by(tables, withs) + [template_category.category] + + @classmethod + def _where(cls, tables, withs): + template_category = tables['move.product.template_category'] + where = super()._where(tables, withs) + where &= template_category.category != Null + return where + + def get_rec_name(self, name): + return self.category.rec_name if self.category else None + + +class Category(CategoryMixin, Abstract, ModelView): + "Stock Reporting Margin per Category" + __name__ = 'stock.reporting.margin.category' + + time_series = fields.One2Many( + 'stock.reporting.margin.category.time_series', 'category', + "Time Series") + + @classmethod + def __setup__(cls): + super().__setup__() + cls._order.insert(0, ('category', 'ASC')) + + @classmethod + def _column_id(cls, tables, withs): + template_category = tables['move.product.template_category'] + return template_category.category + + +class CategoryTimeseries(CategoryMixin, AbstractTimeseries, ModelView): + "Stock Reporting Margin per Category" + __name__ = 'stock.reporting.margin.category.time_series' + + +class CategoryTree(ModelSQL, ModelView): + "Stock Reporting Margin per Category" + __name__ = 'stock.reporting.margin.category.tree' + + name = fields.Function(fields.Char("Name"), 'get_name') + parent = fields.Many2One('stock.reporting.margin.category.tree', "Parent") + children = fields.One2Many( + 'stock.reporting.margin.category.tree', 'parent', "Children") + cost = fields.Function(fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_cost'), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_total') + revenue = fields.Function(fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_revenue'), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_total') + profit = fields.Function(fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_profit'), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']), 'get_total') + margin = fields.Function(fields.Numeric( + lazy_gettext('stock.msg_stock_reporting_margin'), + digits=(14, 4), + depends=['currency_digits']), + 'get_margin') + + currency_digits = fields.Function(fields.Integer( + lazy_gettext('stock.msg_stock_reporting_currency_digits')), + 'get_currency_digits') + + @classmethod + def __setup__(cls): + super().__setup__() + cls._order.insert(0, ('name', 'ASC')) + + @classmethod + def table_query(cls): + pool = Pool() + Category = pool.get('product.category') + return Category.__table__() + + @classmethod + def get_name(cls, categories, name): + pool = Pool() + Category = pool.get('product.category') + categories = Category.browse(categories) + return {c.id: c.name for c in categories} + + @classmethod + def order_name(cls, tables): + pool = Pool() + Category = pool.get('product.category') + table, _ = tables[None] + if 'category' not in tables: + category = Category.__table__() + tables['category'] = { + None: (category, table.id == category.id), + } + return Category.name.convert_order( + 'name', tables['category'], Category) + + def time_series_all(self): + return [] + + @classmethod + def get_total(cls, categories, names): + pool = Pool() + ReportingCategory = pool.get('stock.reporting.margin.category') + table = cls.__table__() + reporting_category = ReportingCategory.__table__() + cursor = Transaction().connection.cursor() + + categories = cls.search([ + ('parent', 'child_of', [c.id for c in categories]), + ]) + ids = [c.id for c in categories] + parents = {} + reporting_categories = [] + for sub_ids in grouped_slice(ids): + sub_ids = list(sub_ids) + where = reduce_ids(table.id, sub_ids) + cursor.execute(*table.select(table.id, table.parent, where=where)) + parents.update(cursor.fetchall()) + + where = reduce_ids(reporting_category.id, sub_ids) + cursor.execute( + *reporting_category.select(reporting_category.id, where=where)) + reporting_categories.extend(r for r, in cursor.fetchall()) + + result = {} + reporting_categories = ReportingCategory.browse(reporting_categories) + for name in names: + values = dict.fromkeys(ids, 0) + values.update( + (c.id, getattr(c, name)) for c in reporting_categories) + result[name] = cls._sum_tree(categories, values, parents) + return result + + @classmethod + def _sum_tree(cls, categories, values, parents): + result = values.copy() + categories = set((c.id for c in categories)) + leafs = categories - set(parents.values()) + while leafs: + for category in leafs: + categories.remove(category) + parent = parents.get(category) + if parent in result: + result[parent] += result[category] + next_leafs = set(categories) + for category in categories: + parent = parents.get(category) + if not parent: + continue + if parent in next_leafs and parent in categories: + next_leafs.remove(parent) + leafs = next_leafs + return result + + def get_margin(self, name): + digits = self.__class__.margin.digits + if self.profit is not None and self.revenue: + return (self.profit / self.revenue).quantize( + Decimal(1) / 10 ** digits[1]) + + def get_currency_digits(self, name): + pool = Pool() + Company = pool.get('company.company') + company = Transaction().context.get('company') + if company: + return Company(company).currency.digits + + @classmethod + def view_attributes(cls): + return super().view_attributes() + [ + ('/tree/field[@name="profit"]', 'visual', + If(Eval('profit', 0) < 0, 'danger', '')), + ('/tree/field[@name="margin"]', 'visual', + If(Eval('margin', 0) < 0, 'danger', '')), + ] diff -r c9582871ec46 -r fcb961429f35 stock_reporting_margin.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/stock_reporting_margin.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,360 @@ +<?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="product.menu_reporting" name="Margins" id="menu_reporting_margin" icon="tryton-graph"/> + <record model="ir.ui.menu-res.group" id="menu_reporting_margin_group_product_admin"> + <field name="menu" ref="menu_reporting_margin"/> + <field name="group" ref="product.group_product_admin"/> + </record> + + <record model="ir.ui.view" id="reporting_margin_context_view_form"> + <field name="model">stock.reporting.margin.context</field> + <field name="type">form</field> + <field name="name">reporting_margin_context_form</field> + </record> + + <!-- Product --> + + <record model="ir.ui.view" id="reporting_margin_product_view_list"> + <field name="model">stock.reporting.margin.product</field> + <field name="type">tree</field> + <field name="name">reporting_margin_product_list</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_product_view_graph_margin"> + <field name="model">stock.reporting.margin.product</field> + <field name="type">graph</field> + <field name="name">reporting_margin_product_graph_margin</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_product_view_graph_profit"> + <field name="model">stock.reporting.margin.product</field> + <field name="type">graph</field> + <field name="name">reporting_margin_product_graph_profit</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_product_view_graph_amount"> + <field name="model">stock.reporting.margin.product</field> + <field name="type">graph</field> + <field name="name">reporting_margin_product_graph_amount</field> + </record> + + <record model="ir.action.act_window" id="act_reporting_margin_product"> + <field name="name">Margins per Product</field> + <field name="res_model">stock.reporting.margin.product</field> + <field name="context_model">stock.reporting.margin.context</field> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_product_view1"> + <field name="sequence" eval="10"/> + <field name="view" ref="reporting_margin_product_view_list"/> + <field name="act_window" ref="act_reporting_margin_product"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_product_view2"> + <field name="sequence" eval="20"/> + <field name="view" ref="reporting_margin_product_view_graph_margin"/> + <field name="act_window" ref="act_reporting_margin_product"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_product_view3"> + <field name="sequence" eval="30"/> + <field name="view" ref="reporting_margin_product_view_graph_profit"/> + <field name="act_window" ref="act_reporting_margin_product"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_product_view4"> + <field name="sequence" eval="40"/> + <field name="view" ref="reporting_margin_product_view_graph_amount"/> + <field name="act_window" ref="act_reporting_margin_product"/> + </record> + <record model="ir.action.keyword" id="act_reporting_margin_product_keyword1"> + <field name="keyword">tree_open</field> + <field name="model" eval="'ir.ui.menu,%s' % ref('menu_reporting_margin')"/> + <field name="action" ref="act_reporting_margin_product"/> + </record> + + <record model="ir.model.access" id="access_reporting_margin_product"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.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_margin_product_product_admin"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.product')]"/> + <field name="group" ref="product.group_product_admin"/> + <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_margin_product_time_series_view_list"> + <field name="model">stock.reporting.margin.product.time_series</field> + <field name="type">tree</field> + <field name="name">reporting_margin_product_time_series_list</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_product_time_series_view_graph_margin"> + <field name="model">stock.reporting.margin.product.time_series</field> + <field name="type">graph</field> + <field name="name">reporting_margin_product_time_series_graph_margin</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_product_time_series_view_graph_profit"> + <field name="model">stock.reporting.margin.product.time_series</field> + <field name="type">graph</field> + <field name="name">reporting_margin_product_time_series_graph_profit</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_product_time_series_view_graph_amount"> + <field name="model">stock.reporting.margin.product.time_series</field> + <field name="type">graph</field> + <field name="name">reporting_margin_product_time_series_graph_amount</field> + </record> + + <record model="ir.action.act_window" id="act_reporting_margin_product_time_series"> + <field name="name">Margins per Product</field> + <field name="res_model">stock.reporting.margin.product.time_series</field> + <field name="context_model">stock.reporting.margin.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_margin_product_time_series_view1"> + <field name="sequence" eval="10"/> + <field name="view" ref="reporting_margin_product_time_series_view_list"/> + <field name="act_window" ref="act_reporting_margin_product_time_series"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_product_time_series_view2"> + <field name="sequence" eval="20"/> + <field name="view" ref="reporting_margin_product_time_series_view_graph_margin"/> + <field name="act_window" ref="act_reporting_margin_product_time_series"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_product_time_series_view3"> + <field name="sequence" eval="30"/> + <field name="view" ref="reporting_margin_product_time_series_view_graph_profit"/> + <field name="act_window" ref="act_reporting_margin_product_time_series"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_product_time_series_view4"> + <field name="sequence" eval="40"/> + <field name="view" ref="reporting_margin_product_time_series_view_graph_amount"/> + <field name="act_window" ref="act_reporting_margin_product_time_series"/> + </record> + <record model="ir.action.keyword" id="act_reporting_margin_product_time_series_keyword1"> + <field name="keyword">tree_open</field> + <field name="model">stock.reporting.margin.product,-1</field> + <field name="action" ref="act_reporting_margin_product_time_series"/> + </record> + + <record model="ir.model.access" id="access_reporting_margin_product_time_series"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.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_margin_product_time_series_product_admin"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.product.time_series')]"/> + <field name="group" ref="product.group_product_admin"/> + <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> + + <!-- Category --> + + <record model="ir.ui.view" id="reporting_margin_category_tree_view_tree"> + <field name="model">stock.reporting.margin.category.tree</field> + <field name="type">tree</field> + <field name="field_childs">children</field> + <field name="name">reporting_margin_category_tree</field> + </record> + + <record model="ir.action.act_window" id="act_reporting_margin_category_tree"> + <field name="name">Margins per Category</field> + <field name="res_model">stock.reporting.margin.category.tree</field> + <field name="context_model">stock.reporting.margin.context</field> + <field name="domain" eval="[('parent', '=', None)]" pyson="1"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_tree_view1"> + <field name="sequence" eval="10"/> + <field name="view" ref="reporting_margin_category_tree_view_tree"/> + <field name="act_window" ref="act_reporting_margin_category_tree"/> + </record> + <record model="ir.action.keyword" id="act_reporting_margin_category_tree_keyword1"> + <field name="keyword">tree_open</field> + <field name="model" eval="'ir.ui.menu,%s' % ref('menu_reporting_margin')"/> + <field name="action" ref="act_reporting_margin_category_tree"/> + </record> + + <record model="ir.model.access" id="access_reporting_margin_category_tree"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.category.tree')]"/> + <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_margin_category_tree_product_admin"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.category.tree')]"/> + <field name="group" ref="product.group_product_admin"/> + <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_margin_category_view_list"> + <field name="model">stock.reporting.margin.category</field> + <field name="type">tree</field> + <field name="name">reporting_margin_category_list</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_category_view_graph_margin"> + <field name="model">stock.reporting.margin.category</field> + <field name="type">graph</field> + <field name="name">reporting_margin_category_graph_margin</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_category_view_graph_profit"> + <field name="model">stock.reporting.margin.category</field> + <field name="type">graph</field> + <field name="name">reporting_margin_category_graph_profit</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_category_view_graph_amount"> + <field name="model">stock.reporting.margin.category</field> + <field name="type">graph</field> + <field name="name">reporting_margin_category_graph_amount</field> + </record> + + <record model="ir.action.act_window" id="act_reporting_margin_category"> + <field name="name">Margins per Category</field> + <field name="res_model">stock.reporting.margin.category</field> + <field name="context_model">stock.reporting.margin.context</field> + <field + name="domain" + eval="[('category', 'child_of', Eval('active_ids', []), 'parent')]" + pyson="1"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_view1"> + <field name="sequence" eval="10"/> + <field name="view" ref="reporting_margin_category_view_list"/> + <field name="act_window" ref="act_reporting_margin_category"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_view2"> + <field name="sequence" eval="20"/> + <field name="view" ref="reporting_margin_category_view_graph_margin"/> + <field name="act_window" ref="act_reporting_margin_category"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_view3"> + <field name="sequence" eval="30"/> + <field name="view" ref="reporting_margin_category_view_graph_profit"/> + <field name="act_window" ref="act_reporting_margin_category"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_view4"> + <field name="sequence" eval="40"/> + <field name="view" ref="reporting_margin_category_view_graph_amount"/> + <field name="act_window" ref="act_reporting_margin_category"/> + </record> + <record model="ir.action.keyword" id="act_reporting_margin_category_keyword1"> + <field name="keyword">tree_open</field> + <field name="model">stock.reporting.margin.category.tree,-1</field> + <field name="action" ref="act_reporting_margin_category"/> + </record> + + <record model="ir.model.access" id="access_reporting_margin_category"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.category')]"/> + <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_margin_category_product_admin"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.category')]"/> + <field name="group" ref="product.group_product_admin"/> + <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_margin_category_time_series_view_list"> + <field name="model">stock.reporting.margin.category.time_series</field> + <field name="type">tree</field> + <field name="name">reporting_margin_category_time_series_list</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_category_time_series_view_graph_margin"> + <field name="model">stock.reporting.margin.category.time_series</field> + <field name="type">graph</field> + <field name="name">reporting_margin_category_time_series_graph_margin</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_category_time_series_view_graph_profit"> + <field name="model">stock.reporting.margin.category.time_series</field> + <field name="type">graph</field> + <field name="name">reporting_margin_category_time_series_graph_profit</field> + </record> + + <record model="ir.ui.view" id="reporting_margin_category_time_series_view_graph_amount"> + <field name="model">stock.reporting.margin.category.time_series</field> + <field name="type">graph</field> + <field name="name">reporting_margin_category_time_series_graph_amount</field> + </record> + + <record model="ir.action.act_window" id="act_reporting_margin_category_time_series"> + <field name="name">Margins per Category</field> + <field name="res_model">stock.reporting.margin.category.time_series</field> + <field name="context_model">stock.reporting.margin.context</field> + <field + name="domain" + eval="[('category', '=', 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_margin_category_time_series_view1"> + <field name="sequence" eval="10"/> + <field name="view" ref="reporting_margin_category_time_series_view_list"/> + <field name="act_window" ref="act_reporting_margin_category_time_series"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_time_series_view2"> + <field name="sequence" eval="20"/> + <field name="view" ref="reporting_margin_category_time_series_view_graph_margin"/> + <field name="act_window" ref="act_reporting_margin_category_time_series"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_time_series_view3"> + <field name="sequence" eval="30"/> + <field name="view" ref="reporting_margin_category_time_series_view_graph_profit"/> + <field name="act_window" ref="act_reporting_margin_category_time_series"/> + </record> + <record model="ir.action.act_window.view" id="act_reporting_margin_category_time_series_view4"> + <field name="sequence" eval="40"/> + <field name="view" ref="reporting_margin_category_time_series_view_graph_amount"/> + <field name="act_window" ref="act_reporting_margin_category_time_series"/> + </record> + <record model="ir.action.keyword" id="act_reporting_margin_category_time_series_keyword1"> + <field name="keyword">tree_open</field> + <field name="model">stock.reporting.margin.category,-1</field> + <field name="action" ref="act_reporting_margin_category_time_series"/> + </record> + + <record model="ir.model.access" id="access_reporting_margin_category_time_series"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.category.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_margin_category_time_series_product_admin"> + <field name="model" search="[('model', '=', 'stock.reporting.margin.category.time_series')]"/> + <field name="group" ref="product.group_product_admin"/> + <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 c9582871ec46 -r fcb961429f35 tests/scenario_stock_reporting_margin.rst --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/tests/scenario_stock_reporting_margin.rst Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,215 @@ +=============================== +Stock Reporting Margin Scenario +=============================== + +Imports:: + + >>> import datetime as dt + >>> 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) + >>> today = dt.date.today() + >>> yesterday = today - dt.timedelta(days=1) + +Activate modules:: + + >>> config = activate_modules('stock') + +Create company:: + + >>> _ = create_company() + >>> company = get_company() + +Create product:: + + >>> ProductUom = Model.get('product.uom') + >>> ProductTemplate = Model.get('product.template') + >>> unit, = ProductUom.find([('name', '=', 'Unit')]) + >>> template = ProductTemplate() + >>> template.name = 'Product' + >>> template.default_uom = unit + >>> template.type = 'goods' + >>> template.list_price = Decimal('40') + >>> template.save() + >>> product, = template.products + >>> product.cost_price = Decimal('20') + >>> product.save() + >>> template2, = template.duplicate() + >>> product2, = template2.products + + >>> Category = Model.get('product.category') + >>> category_root = Category(name="Root") + >>> category_root.save() + >>> category1 = Category(name="Child1", parent=category_root) + >>> category1.save() + >>> category2 = Category(name="Child2", parent=category_root) + >>> category2.save() + + >>> template.categories.append(Category(category1.id)) + >>> template.save() + >>> template2.categories.append(Category(category2.id)) + >>> template2.save() + + +Get stock locations:: + + >>> Location = Model.get('stock.location') + >>> supplier_loc, = Location.find([('code', '=', 'SUP')]) + >>> customer_loc, = Location.find([('code', '=', 'CUS')]) + >>> storage_loc, = Location.find([('code', '=', 'STO')]) + >>> lost_loc, = Location.find([('type', '=', 'lost_found')]) + +Create some moves:: + + >>> Move = Model.get('stock.move') + >>> move = Move() + >>> move.product = product + >>> move.from_location = supplier_loc + >>> move.to_location = storage_loc + >>> move.quantity = 8 + >>> move.unit_price = Decimal('20') + >>> move.effective_date = yesterday + >>> move.click('do') + + >>> move = Move() + >>> move.product = product + >>> move.from_location = storage_loc + >>> move.to_location = customer_loc + >>> move.quantity = 2 + >>> move.unit_price = Decimal('40') + >>> move.effective_date = yesterday + >>> move.click('do') + + >>> move = Move() + >>> move.product = product + >>> move.from_location = storage_loc + >>> move.to_location = customer_loc + >>> move.quantity = 4 + >>> move.unit_price = Decimal('30') + >>> move.effective_date = today + >>> move.click('do') + + >>> move = Move() + >>> move.product = product + >>> move.from_location = customer_loc + >>> move.to_location = storage_loc + >>> move.quantity = 1 + >>> move.unit_price = Decimal('30') + >>> move.effective_date = today + >>> move.click('do') + + >>> move = Move() + >>> move.product = product2 + >>> move.from_location = storage_loc + >>> move.to_location = customer_loc + >>> move.quantity = 2 + >>> move.unit_price = Decimal('50') + >>> move.effective_date = today + >>> move.click('do') + + >>> move = Move() + >>> move.product = product + >>> move.from_location = storage_loc + >>> move.to_location = lost_loc + >>> move.quantity = 1 + >>> move.effective_date = today + >>> move.click('do') + +Check reporting margin per product:: + + >>> MarginProduct = Model.get('stock.reporting.margin.product') + >>> MarginProductTimeseries = Model.get( + ... 'stock.reporting.margin.product.time_series') + >>> context = { + ... 'from_date': yesterday, + ... 'to_date': today, + ... 'period': 'day', + ... } + >>> with config.set_context(context=context): + ... reports = MarginProduct.find([]) + ... time_series = MarginProductTimeseries.find([]) + >>> len(reports) + 2 + >>> report, = [r for r in reports if r.product == product] + >>> (report.quantity, report.cost, report.revenue, + ... report.profit, report.margin) == ( + ... 5, Decimal('100.00'), Decimal('170.00'), + ... Decimal('70.00'), Decimal('0.4118')) + True + >>> len(time_series) + 3 + >>> with config.set_context(context=context): + ... sorted(( + ... r.product.id, str(r.date), r.quantity, r.cost, r.revenue, + ... r.profit, r.margin) + ... for r in time_series) == sorted([ + ... (product.id, str(yesterday), 2, Decimal('40.00'), Decimal('80.00'), + ... Decimal('40.00'), Decimal('0.5000')), + ... (product.id, str(today), 3, Decimal('60.00'), Decimal('90.00'), + ... Decimal('30.00'), Decimal('0.3333')), + ... (product2.id, str(today), 2, Decimal('40.00'), Decimal('100.00'), + ... Decimal('60.00'), Decimal('0.6000'))]) + True + +Check reporting margin per categories:: + + >>> MarginCategory = Model.get('stock.reporting.margin.category') + >>> MarginCategoryTimeseries = Model.get( + ... 'stock.reporting.margin.category.time_series') + >>> MarginCategoryTree = Model.get( + ... 'stock.reporting.margin.category.tree') + >>> with config.set_context(context=context): + ... reports = MarginCategory.find([]) + ... time_series = MarginCategoryTimeseries.find([]) + ... tree = MarginCategoryTree.find([]) + >>> len(reports) + 2 + >>> with config.set_context(context=context): + ... sorted((r.category.id, r.cost, r.revenue, r.profit, r.margin) + ... for r in reports) == sorted([ + ... (category1.id, Decimal('100.00'), Decimal('170.00'), + ... Decimal('70.00'), Decimal('0.4118')), + ... (category2.id, Decimal('40.00'), Decimal('100.00'), + ... Decimal('60.00'), Decimal('0.6000'))]) + True + >>> len(time_series) + 3 + >>> with config.set_context(context=context): + ... sorted((r.category.id, str(r.date), r.cost, r.revenue, r.profit, r.margin) + ... for r in time_series) == sorted([ + ... (category1.id, str(yesterday), Decimal('40.00'), Decimal('80.00'), + ... Decimal('40.00'), Decimal('0.5000')), + ... (category1.id, str(today), Decimal('60.00'), Decimal('90.00'), + ... Decimal('30.00'), Decimal('0.3333')), + ... (category2.id, str(today), Decimal('40.00'), Decimal('100.00'), + ... Decimal('60.00'), Decimal('0.6000'))]) + True + >>> len(tree) + 3 + >>> with config.set_context(context=context): + ... sorted((r.name, r.cost, r.revenue, r.profit, r.margin) + ... for r in tree) == sorted([ + ... ("Root", Decimal('140.00'), Decimal('270.00'), + ... Decimal('130.00'), Decimal('0.4815')), + ... ("Child1", Decimal('100.00'), Decimal('170.00'), + ... Decimal('70.00'), Decimal('0.4118')), + ... ('Child2', Decimal('40.00'), Decimal('100.00'), + ... Decimal('60.00'), Decimal('0.6000'))]) + True + +Check reporting margin including lost:: + + >>> context['include_lost'] = True + + >>> with config.set_context(context=context): + ... reports = MarginProduct.find([]) + >>> len(reports) + 2 + >>> report, = [r for r in reports if r.product == product] + >>> (report.quantity, report.cost, report.revenue, + ... report.profit, report.margin) == ( + ... 6, Decimal('120.00'), Decimal('170.00'), + ... Decimal('50.00'), Decimal('0.2941')) + True diff -r c9582871ec46 -r fcb961429f35 tests/test_stock.py --- a/tests/test_stock.py Sun Jan 17 00:43:31 2021 +0100 +++ b/tests/test_stock.py Sun Jan 17 00:57:09 2021 +0100 @@ -1563,4 +1563,9 @@ tearDown=doctest_teardown, encoding='utf-8', checker=doctest_checker, optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) + suite.addTests(doctest.DocFileSuite( + 'scenario_stock_reporting_margin.rst', + tearDown=doctest_teardown, encoding='utf-8', + checker=doctest_checker, + optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)) return suite diff -r c9582871ec46 -r fcb961429f35 tryton.cfg --- a/tryton.cfg Sun Jan 17 00:43:31 2021 +0100 +++ b/tryton.cfg Sun Jan 17 00:57:09 2021 +0100 @@ -19,3 +19,4 @@ period.xml message.xml res.xml + stock_reporting_margin.xml diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_graph_amount.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_graph_amount.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph> + <x> + <field name="category"/> + </x> + <y> + <field name="cost"/> + <field name="revenue"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_graph_margin.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_graph_margin.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph type="line"> + <x> + <field name="category"/> + </x> + <y> + <field name="margin" empty="0"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_graph_profit.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_graph_profit.xml Sun Jan 17 00:57:09 2021 +0100 @@ -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="category"/> + </x> + <y> + <field name="profit"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_list.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_list.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,13 @@ +<?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="category" expand="1"/> + <field name="cost" symbol="currency"/> + <field name="revenue" symbol="currency"/> + <field name="profit" symbol="currency"/> + <field name="margin" factor="100"> + <suffix string="%" name="margin"/> + </field> + <field name="margin_trend" expand="1"/> +</tree> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_time_series_graph_amount.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_time_series_graph_amount.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph> + <x> + <field name="date"/> + </x> + <y> + <field name="cost"/> + <field name="revenue"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_time_series_graph_margin.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_time_series_graph_margin.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph type="line"> + <x> + <field name="date"/> + </x> + <y> + <field name="margin" empty="0"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_time_series_graph_profit.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_time_series_graph_profit.xml Sun Jan 17 00:57:09 2021 +0100 @@ -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="profit"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_time_series_list.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_time_series_list.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<tree> + <field name="date"/> + <field name="cost" symbol="currency"/> + <field name="revenue" symbol="currency"/> + <field name="profit" symbol="currency"/> + <field name="margin" factor="100"> + <suffix string="%" name="margin"/> + </field> +</tree> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_category_tree.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_category_tree.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<tree keyword_open="1"> + <field name="name" expand="1"/> + <field name="cost"/> + <field name="revenue"/> + <field name="profit"/> + <field name="margin" factor="100"> + <suffix string="%" name="margin"/> + </field> +</tree> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_context_form.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_context_form.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,20 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<form> + <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"/> + + <group col="-1" colspan="2" id="checkboxes"> + <label name="include_lost"/> + <field name="include_lost" xexpand="0" width="25"/> + </group> + <label name="company"/> + <field name="company"/> +</form> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_graph_amount.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_graph_amount.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph> + <x> + <field name="product"/> + </x> + <y> + <field name="cost"/> + <field name="revenue"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_graph_margin.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_graph_margin.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph type="line"> + <x> + <field name="product"/> + </x> + <y> + <field name="margin" empty="0"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_graph_profit.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_graph_profit.xml Sun Jan 17 00:57:09 2021 +0100 @@ -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="product"/> + </x> + <y> + <field name="profit"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_list.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_list.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,14 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<tree keyword_open="1"> + <field name="product" expand="1"/> + <field name="quantity" symbol="unit"/> + <field name="cost" symbol="currency"/> + <field name="revenue" symbol="currency"/> + <field name="profit" symbol="currency"/> + <field name="margin" factor="100"> + <suffix string="%" name="margin"/> + </field> + <field name="margin_trend" expand="1"/> +</tree> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_time_series_graph_amount.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_time_series_graph_amount.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,12 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph> + <x> + <field name="date"/> + </x> + <y> + <field name="cost"/> + <field name="revenue"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_time_series_graph_margin.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_time_series_graph_margin.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,11 @@ +<?xml version="1.0"?> +<!-- This file is part of Tryton. The COPYRIGHT file at the top level of +this repository contains the full copyright notices and license terms. --> +<graph type="line"> + <x> + <field name="date"/> + </x> + <y> + <field name="margin" empty="0"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_time_series_graph_profit.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_time_series_graph_profit.xml Sun Jan 17 00:57:09 2021 +0100 @@ -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="profit"/> + </y> +</graph> diff -r c9582871ec46 -r fcb961429f35 view/reporting_margin_product_time_series_list.xml --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/view/reporting_margin_product_time_series_list.xml Sun Jan 17 00:57:09 2021 +0100 @@ -0,0 +1,13 @@ +<?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="quantity" symbol="unit"/> + <field name="cost" symbol="currency"/> + <field name="revenue" symbol="currency"/> + <field name="profit" symbol="currency"/> + <field name="margin" factor="100"> + <suffix string="%" name="margin"/> + </field> +</tree>