Add margin reporting

issue9665
review292371002
This commit is contained in:
Cédric Krier
2021-01-17 00:57:09 +01:00
parent 582c2b57f0
commit acfcbff597
27 changed files with 1465 additions and 0 deletions
+1
View File
@@ -1,3 +1,4 @@
* Add margin reporting
* Add products_by_locations Model
Version 5.8.0 - 2020-11-02
+7
View File
@@ -12,6 +12,7 @@ from . import configuration
from . import party
from . import ir
from . import res
from . import stock_reporting_margin
from .move import StockMixin
@@ -55,6 +56,12 @@ def register():
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,
+24
View File
@@ -92,5 +92,29 @@ because the type does not support moves and location has existing moves.</field>
<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>
+3
View File
@@ -137,6 +137,9 @@ setup(name=name,
license='GPL-3',
python_requires='>=3.6',
install_requires=requires,
extras_require={
'sparklines': ['pygal'],
},
dependency_links=dependency_links,
zip_safe=False,
entry_points="""
+629
View File
@@ -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', '')),
]
+360
View File
@@ -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>
+215
View File
@@ -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
+5
View File
@@ -1563,4 +1563,9 @@ def suite():
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
+1
View File
@@ -19,3 +19,4 @@ xml:
period.xml
message.xml
res.xml
stock_reporting_margin.xml
@@ -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>
@@ -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>
@@ -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>
+13
View File
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>
+12
View File
@@ -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>
+20
View File
@@ -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>
@@ -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>
@@ -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>
@@ -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>
+14
View File
@@ -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>
@@ -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>
@@ -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>
@@ -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>
@@ -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>