Also inherit from the main view to reduce the number of XML files. And add sum attribute to reporting with unique grouped lines. issue10663 review350321002
974 lines
32 KiB
Python
974 lines
32 KiB
Python
# 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 collections import defaultdict
|
|
from itertools import tee, zip_longest
|
|
|
|
try:
|
|
import pygal
|
|
except ImportError:
|
|
pygal = None
|
|
from dateutil.relativedelta import relativedelta
|
|
from sql import Null, Literal, Column, With
|
|
from sql.aggregate import Sum, Max, Min, Count
|
|
from sql.conditionals import Coalesce
|
|
from sql.functions import CurrentTimestamp, DateTrunc, Power, Ceil, Log
|
|
|
|
from trytond.pool import Pool
|
|
from trytond.model import ModelSQL, ModelView, UnionMixin, fields
|
|
from trytond.tools import grouped_slice, reduce_ids
|
|
from trytond.transaction import Transaction
|
|
from trytond.pyson import Eval, If
|
|
from trytond.wizard import Wizard, StateTransition, StateAction
|
|
from trytond.i18n import lazy_gettext
|
|
|
|
from trytond.modules.currency.fields import Monetary
|
|
|
|
|
|
def pairwise(iterable):
|
|
a, b = tee(iterable)
|
|
next(b)
|
|
return zip_longest(a, b)
|
|
|
|
|
|
class Abstract(ModelSQL):
|
|
|
|
company = fields.Many2One(
|
|
'company.company', lazy_gettext("sale.msg_sale_reporting_company"))
|
|
number = fields.Integer(lazy_gettext("sale.msg_sale_reporting_number"),
|
|
help=lazy_gettext("sale.msg_sale_reporting_number_help"))
|
|
revenue = Monetary(
|
|
lazy_gettext("sale.msg_sale_reporting_revenue"),
|
|
digits='currency', currency='currency')
|
|
revenue_trend = fields.Function(
|
|
fields.Char(lazy_gettext("sale.msg_sale_reporting_revenue_trend")),
|
|
'get_trend')
|
|
time_series = None
|
|
|
|
currency = fields.Function(fields.Many2One(
|
|
'currency.currency',
|
|
lazy_gettext("sale.msg_sale_reporting_currency")),
|
|
'get_currency')
|
|
|
|
@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')
|
|
Line = pool.get('sale.line')
|
|
Sale = pool.get('sale.sale')
|
|
|
|
tables = {}
|
|
tables['line'] = line = Line.__table__()
|
|
tables['line.sale'] = sale = Sale.__table__()
|
|
tables['line.sale.company'] = company = Company.__table__()
|
|
withs = {}
|
|
currency_sale = With(query=Currency.currency_rate_sql())
|
|
withs['currency_sale'] = currency_sale
|
|
currency_company = With(query=Currency.currency_rate_sql())
|
|
withs['currency_company'] = currency_company
|
|
|
|
from_item = (line
|
|
.join(sale, condition=line.sale == sale.id)
|
|
.join(currency_sale,
|
|
condition=(sale.currency == currency_sale.currency)
|
|
& (currency_sale.start_date <= sale.sale_date)
|
|
& ((currency_sale.end_date == Null)
|
|
| (currency_sale.end_date >= sale.sale_date))
|
|
)
|
|
.join(company, condition=sale.company == company.id)
|
|
.join(currency_company,
|
|
condition=(company.currency == currency_company.currency)
|
|
& (currency_company.start_date <= sale.sale_date)
|
|
& ((currency_company.end_date == Null)
|
|
| (currency_company.end_date >= sale.sale_date))
|
|
))
|
|
return from_item, tables, withs
|
|
|
|
@classmethod
|
|
def _columns(cls, tables, withs):
|
|
line = tables['line']
|
|
sale = tables['line.sale']
|
|
currency_company = withs['currency_company']
|
|
currency_sale = withs['currency_sale']
|
|
|
|
quantity = Coalesce(line.actual_quantity, line.quantity)
|
|
revenue = cls.revenue.sql_cast(
|
|
Sum(quantity * line.unit_price
|
|
* currency_company.rate / currency_sale.rate))
|
|
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'),
|
|
sale.company.as_('company'),
|
|
revenue.as_('revenue'),
|
|
Count(sale.id, distinct=True).as_('number'),
|
|
]
|
|
|
|
@classmethod
|
|
def _column_id(cls, tables, withs):
|
|
line = tables['line']
|
|
return Min(line.id)
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
sale = tables['line.sale']
|
|
return [sale.company]
|
|
|
|
@classmethod
|
|
def _where(cls, tables, withs):
|
|
context = Transaction().context
|
|
sale = tables['line.sale']
|
|
|
|
where = sale.company == context.get('company')
|
|
where &= sale.state.in_(cls._sale_states())
|
|
from_date = context.get('from_date')
|
|
if from_date:
|
|
where &= sale.sale_date >= from_date
|
|
to_date = context.get('to_date')
|
|
if to_date:
|
|
where &= sale.sale_date <= to_date
|
|
warehouse = context.get('warehouse')
|
|
if warehouse:
|
|
where &= sale.warehouse == warehouse
|
|
return where
|
|
|
|
@classmethod
|
|
def _sale_states(cls):
|
|
return ['confirmed', 'processing', 'done']
|
|
|
|
@property
|
|
def time_series_all(self):
|
|
delta = self._period_delta()
|
|
for ts, next_ts in pairwise(self.time_series or []):
|
|
yield ts
|
|
if delta and next_ts:
|
|
date = ts.date + delta
|
|
while date < next_ts.date:
|
|
yield None
|
|
date += delta
|
|
|
|
@classmethod
|
|
def _period_delta(cls):
|
|
context = Transaction().context
|
|
return {
|
|
'year': relativedelta(years=1),
|
|
'month': relativedelta(months=1),
|
|
'day': relativedelta(days=1),
|
|
}.get(context.get('period'))
|
|
|
|
def get_trend(self, name):
|
|
name = name[:-len('_trend')]
|
|
if pygal:
|
|
chart = pygal.Line()
|
|
chart.add('', [getattr(ts, name) if ts else 0
|
|
for ts in self.time_series_all])
|
|
return chart.render_sparktext()
|
|
|
|
def get_currency(self, name):
|
|
return self.company.currency.id
|
|
|
|
|
|
class AbstractTimeseries(Abstract):
|
|
|
|
date = fields.Date("Date")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super(AbstractTimeseries, cls).__setup__()
|
|
cls._order = [('date', 'ASC')]
|
|
|
|
@classmethod
|
|
def _columns(cls, tables, withs):
|
|
return super(AbstractTimeseries, cls)._columns(tables, withs) + [
|
|
cls._column_date(tables, withs).as_('date')]
|
|
|
|
@classmethod
|
|
def _column_date(cls, tables, withs):
|
|
context = Transaction().context
|
|
sale = tables['line.sale']
|
|
date = DateTrunc(context.get('period'), sale.sale_date)
|
|
date = cls.date.sql_cast(date)
|
|
return date
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
return super(AbstractTimeseries, cls)._group_by(tables, withs) + [
|
|
cls._column_date(tables, withs)]
|
|
|
|
|
|
class Context(ModelView):
|
|
"Sale Reporting Context"
|
|
__name__ = 'sale.reporting.context'
|
|
|
|
company = fields.Many2One('company.company', "Company", required=True)
|
|
from_date = fields.Date("From Date",
|
|
domain=[
|
|
If(Eval('to_date') & Eval('from_date'),
|
|
('from_date', '<=', Eval('to_date')),
|
|
()),
|
|
],
|
|
depends=['to_date'])
|
|
to_date = fields.Date("To Date",
|
|
domain=[
|
|
If(Eval('from_date') & Eval('to_date'),
|
|
('to_date', '>=', Eval('from_date')),
|
|
()),
|
|
],
|
|
depends=['from_date'])
|
|
period = fields.Selection([
|
|
('year', "Year"),
|
|
('month', "Month"),
|
|
('day', "Day"),
|
|
], "Period", required=True)
|
|
warehouse = fields.Many2One(
|
|
'stock.location', "Warehouse",
|
|
domain=[
|
|
('type', '=', 'warehouse'),
|
|
])
|
|
|
|
@classmethod
|
|
def default_company(cls):
|
|
return Transaction().context.get('company')
|
|
|
|
@classmethod
|
|
def default_from_date(cls):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
context = Transaction().context
|
|
if 'from_date' in context:
|
|
return context['from_date']
|
|
return Date.today() - relativedelta(years=1)
|
|
|
|
@classmethod
|
|
def default_to_date(cls):
|
|
pool = Pool()
|
|
Date = pool.get('ir.date')
|
|
context = Transaction().context
|
|
if 'to_date' in context:
|
|
return context['to_date']
|
|
return Date.today()
|
|
|
|
@classmethod
|
|
def default_period(cls):
|
|
return Transaction().context.get('period', 'month')
|
|
|
|
@classmethod
|
|
def default_warehouse(cls):
|
|
return Transaction().context.get('warehouse')
|
|
|
|
|
|
class Main(Abstract, ModelView):
|
|
"Sale Reporting"
|
|
__name__ = 'sale.reporting.main'
|
|
|
|
time_series = fields.Function(fields.One2Many(
|
|
'sale.reporting.main.time_series', None, "Time Series"),
|
|
'get_time_series')
|
|
|
|
def get_rec_name(self, name):
|
|
return ''
|
|
|
|
def get_time_series(self, name):
|
|
pool = Pool()
|
|
Timeseries = pool.get('sale.reporting.main.time_series')
|
|
return [t.id for t in Timeseries.search([])]
|
|
|
|
|
|
class MainTimeseries(AbstractTimeseries, ModelView):
|
|
"Sale Reporting"
|
|
__name__ = 'sale.reporting.main.time_series'
|
|
|
|
|
|
class CustomerMixin(object):
|
|
__slots__ = ()
|
|
customer = fields.Many2One(
|
|
'party.party', "Customer",
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
|
|
@classmethod
|
|
def _columns(cls, tables, withs):
|
|
sale = tables['line.sale']
|
|
return super(CustomerMixin, cls)._columns(tables, withs) + [
|
|
sale.party.as_('customer')]
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
sale = tables['line.sale']
|
|
return super(CustomerMixin, cls)._group_by(tables, withs) + [
|
|
sale.party]
|
|
|
|
def get_rec_name(self, name):
|
|
return self.customer.rec_name
|
|
|
|
|
|
class Customer(CustomerMixin, Abstract, ModelView):
|
|
"Sale Reporting per Customer"
|
|
__name__ = 'sale.reporting.customer'
|
|
|
|
time_series = fields.One2Many(
|
|
'sale.reporting.customer.time_series', 'customer', "Time Series")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('customer', 'ASC'))
|
|
|
|
@classmethod
|
|
def _column_id(cls, tables, withs):
|
|
sale = tables['line.sale']
|
|
return sale.party
|
|
|
|
|
|
class CustomerTimeseries(CustomerMixin, AbstractTimeseries, ModelView):
|
|
"Sale Reporting per Customer"
|
|
__name__ = 'sale.reporting.customer.time_series'
|
|
|
|
|
|
class CustomerCategoryMixin:
|
|
__slots__ = ()
|
|
category = fields.Many2One('party.category', "Category")
|
|
|
|
@classmethod
|
|
def _joins(cls):
|
|
pool = Pool()
|
|
PartyCategory = pool.get('party.party-party.category')
|
|
from_item, tables, withs = super()._joins()
|
|
if 'line.sale.party.party_category' not in tables:
|
|
party_category = PartyCategory.__table__()
|
|
tables['line.sale.party.party_category'] = party_category
|
|
sale = tables['line.sale']
|
|
from_item = (from_item
|
|
.join(party_category,
|
|
condition=sale.party == party_category.party))
|
|
return from_item, tables, withs
|
|
|
|
@classmethod
|
|
def _columns(cls, tables, withs):
|
|
party_category = tables['line.sale.party.party_category']
|
|
return super()._columns(tables, withs) + [
|
|
party_category.category.as_('category')]
|
|
|
|
@classmethod
|
|
def _column_id(cls, tables, withs):
|
|
pool = Pool()
|
|
Category = pool.get('party.category')
|
|
category = Category.__table__()
|
|
line = tables['line']
|
|
party_category = tables['line.sale.party.party_category']
|
|
# Get a stable number of categories over time
|
|
# by using a number one order bigger.
|
|
nb_category = category.select(
|
|
Power(10, (Ceil(Log(Max(category.id))) + Literal(1))))
|
|
return Min(line.id * nb_category + party_category.id)
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
party_category = tables['line.sale.party.party_category']
|
|
return super()._group_by(tables, withs) + [party_category.category]
|
|
|
|
@classmethod
|
|
def _where(cls, tables, withs):
|
|
party_category = tables['line.sale.party.party_category']
|
|
where = super()._where(tables, withs)
|
|
where &= party_category.category != Null
|
|
return where
|
|
|
|
def get_rec_name(self, name):
|
|
return self.category.rec_name if self.category else None
|
|
|
|
|
|
class CustomerCategory(CustomerCategoryMixin, Abstract, ModelView):
|
|
"Sale Reporting per Customer Category"
|
|
__name__ = 'sale.reporting.customer.category'
|
|
|
|
time_series = fields.One2Many(
|
|
'sale.reporting.customer.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):
|
|
party_category = tables['line.sale.party.party_category']
|
|
return party_category.category
|
|
|
|
|
|
class CustomerCategoryTimeseries(
|
|
CustomerCategoryMixin, AbstractTimeseries, ModelView):
|
|
"Sale Reporting per Customer Category"
|
|
__name__ = 'sale.reporting.customer.category.time_series'
|
|
|
|
|
|
class CustomerCategoryTree(ModelSQL, ModelView):
|
|
"Sale Reporting per Customer Category"
|
|
__name__ = 'sale.reporting.customer.category.tree'
|
|
|
|
name = fields.Function(fields.Char("Name"), 'get_name')
|
|
parent = fields.Many2One('sale.reporting.customer.category.tree', "Parent")
|
|
children = fields.One2Many(
|
|
'sale.reporting.customer.category.tree', 'parent', "Children")
|
|
revenue = fields.Function(Monetary(
|
|
"Revenue", digits='currency', currency='currency'), 'get_total')
|
|
|
|
currency = fields.Function(fields.Many2One(
|
|
'currency.currency', "Currency"), 'get_currency')
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('name', 'ASC'))
|
|
|
|
@classmethod
|
|
def table_query(cls):
|
|
pool = Pool()
|
|
Category = pool.get('party.category')
|
|
return Category.__table__()
|
|
|
|
@classmethod
|
|
def get_name(cls, categories, name):
|
|
pool = Pool()
|
|
Category = pool.get('party.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('party.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()
|
|
ReportingCustomerCategory = pool.get(
|
|
'sale.reporting.customer.category')
|
|
table = cls.__table__()
|
|
reporting_product_category = ReportingCustomerCategory.__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_customer_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)
|
|
|
|
where = reduce_ids(reporting_product_category.id, sub_ids)
|
|
cursor.execute(
|
|
*reporting_product_category.select(
|
|
reporting_product_category.id, where=where))
|
|
reporting_customer_categories.extend(r for r, in cursor)
|
|
|
|
result = {}
|
|
reporting_product_categories = ReportingCustomerCategory.browse(
|
|
reporting_customer_categories)
|
|
for name in names:
|
|
values = dict.fromkeys(ids, 0)
|
|
values.update(
|
|
(c.id, getattr(c, name)) for c in reporting_product_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_currency(self, name):
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
company = Transaction().context.get('company')
|
|
if company:
|
|
return Company(company).currency.id
|
|
|
|
|
|
class ProductMixin(object):
|
|
__slots__ = ()
|
|
product = fields.Many2One(
|
|
'product.product', "Product",
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
|
|
@classmethod
|
|
def _columns(cls, tables, withs):
|
|
line = tables['line']
|
|
return super(ProductMixin, cls)._columns(tables, withs) + [
|
|
line.product.as_('product')]
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
line = tables['line']
|
|
return super(ProductMixin, cls)._group_by(tables, withs) + [
|
|
line.product]
|
|
|
|
@classmethod
|
|
def _where(cls, tables, withs):
|
|
line = tables['line']
|
|
where = super(ProductMixin, cls)._where(tables, withs)
|
|
where &= line.product != Null
|
|
return where
|
|
|
|
def get_rec_name(self, name):
|
|
return self.product.rec_name if self.product else None
|
|
|
|
|
|
class Product(ProductMixin, Abstract, ModelView):
|
|
"Sale Reporting per Product"
|
|
__name__ = 'sale.reporting.product'
|
|
|
|
time_series = fields.One2Many(
|
|
'sale.reporting.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):
|
|
line = tables['line']
|
|
return line.product
|
|
|
|
|
|
class ProductTimeseries(ProductMixin, AbstractTimeseries, ModelView):
|
|
"Sale Reporting per Product"
|
|
__name__ = 'sale.reporting.product.time_series'
|
|
|
|
|
|
class ProductCategoryMixin(object):
|
|
__slots__ = ()
|
|
category = fields.Many2One(
|
|
'product.category', "Category",
|
|
context={
|
|
'company': Eval('company', -1),
|
|
},
|
|
depends=['company'])
|
|
|
|
@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 'line.product' not in tables:
|
|
product = Product.__table__()
|
|
tables['line.product'] = product
|
|
line = tables['line']
|
|
from_item = (from_item
|
|
.join(product, condition=line.product == product.id))
|
|
if 'line.product.template_category' not in tables:
|
|
template_category = TemplateCategory.__table__()
|
|
tables['line.product.template_category'] = template_category
|
|
product = tables['line.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['line.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__()
|
|
line = tables['line']
|
|
template_category = tables['line.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(line.id * nb_category + template_category.id)
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
template_category = tables['line.product.template_category']
|
|
return super()._group_by(tables, withs) + [template_category.category]
|
|
|
|
@classmethod
|
|
def _where(cls, tables, withs):
|
|
template_category = tables['line.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 ProductCategory(ProductCategoryMixin, Abstract, ModelView):
|
|
"Sale Reporting per Product Category"
|
|
__name__ = 'sale.reporting.product.category'
|
|
|
|
time_series = fields.One2Many(
|
|
'sale.reporting.product.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['line.product.template_category']
|
|
return template_category.category
|
|
|
|
|
|
class ProductCategoryTimeseries(
|
|
ProductCategoryMixin, AbstractTimeseries, ModelView):
|
|
"Sale Reporting per Product Category"
|
|
__name__ = 'sale.reporting.product.category.time_series'
|
|
|
|
|
|
class ProductCategoryTree(ModelSQL, ModelView):
|
|
"Sale Reporting per Product Category"
|
|
__name__ = 'sale.reporting.product.category.tree'
|
|
|
|
name = fields.Function(fields.Char("Name"), 'get_name')
|
|
parent = fields.Many2One('sale.reporting.product.category.tree', "Parent")
|
|
children = fields.One2Many(
|
|
'sale.reporting.product.category.tree', 'parent', "Children")
|
|
revenue = fields.Function(Monetary(
|
|
"Revenue", digits='currency'), 'get_total')
|
|
|
|
currency = fields.Function(fields.Many2One(
|
|
'currency.currency', "Currency"), 'get_currency')
|
|
|
|
@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()
|
|
ReportingProductCategory = pool.get('sale.reporting.product.category')
|
|
table = cls.__table__()
|
|
reporting_product_category = ReportingProductCategory.__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_product_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)
|
|
|
|
where = reduce_ids(reporting_product_category.id, sub_ids)
|
|
cursor.execute(
|
|
*reporting_product_category.select(
|
|
reporting_product_category.id, where=where))
|
|
reporting_product_categories.extend(r for r, in cursor)
|
|
|
|
result = {}
|
|
reporting_product_categories = ReportingProductCategory.browse(
|
|
reporting_product_categories)
|
|
for name in names:
|
|
values = dict.fromkeys(ids, 0)
|
|
values.update(
|
|
(c.id, getattr(c, name)) for c in reporting_product_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_currency(self, name):
|
|
pool = Pool()
|
|
Company = pool.get('company.company')
|
|
company = Transaction().context.get('company')
|
|
if company:
|
|
return Company(company).currency.id
|
|
|
|
|
|
class CountryMixin(object):
|
|
__slots__ = ()
|
|
country = fields.Many2One('country.country', "Country")
|
|
|
|
@classmethod
|
|
def _joins(cls):
|
|
pool = Pool()
|
|
Address = pool.get('party.address')
|
|
from_item, tables, withs = super(CountryMixin, cls)._joins()
|
|
if 'line.sale.shipment_address' not in tables:
|
|
address = Address.__table__()
|
|
tables['line.sale.shipment_address'] = address
|
|
sale = tables['line.sale']
|
|
from_item = (from_item
|
|
.join(address, condition=sale.shipment_address == address.id))
|
|
return from_item, tables, withs
|
|
|
|
@classmethod
|
|
def _columns(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
return super(CountryMixin, cls)._columns(tables, withs) + [
|
|
address.country.as_('country')]
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
return super(CountryMixin, cls)._group_by(tables, withs) + [
|
|
address.country]
|
|
|
|
@classmethod
|
|
def _where(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
where = super(CountryMixin, cls)._where(tables, withs)
|
|
where &= address.country != Null
|
|
return where
|
|
|
|
|
|
class Country(CountryMixin, Abstract):
|
|
"Sale Reporting per Country"
|
|
__name__ = 'sale.reporting.country'
|
|
|
|
time_series = fields.One2Many(
|
|
'sale.reporting.country.time_series', 'country', "Time Series")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('country', 'ASC'))
|
|
|
|
@classmethod
|
|
def _column_id(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
return address.country
|
|
|
|
def get_rec_name(self, name):
|
|
return self.country.rec_name
|
|
|
|
|
|
class CountryTimeseries(CountryMixin, AbstractTimeseries, ModelView):
|
|
"Sale Reporting per Country"
|
|
__name__ = 'sale.reporting.country.time_series'
|
|
|
|
|
|
class SubdivisionMixin(CountryMixin):
|
|
__slots__ = ()
|
|
subdivision = fields.Many2One('country.subdivision', "Subdivision")
|
|
|
|
@classmethod
|
|
def _columns(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
return super(SubdivisionMixin, cls)._columns(tables, withs) + [
|
|
address.subdivision.as_('subdivision')]
|
|
|
|
@classmethod
|
|
def _group_by(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
return super(SubdivisionMixin, cls)._group_by(tables, withs) + [
|
|
address.subdivision]
|
|
|
|
@classmethod
|
|
def _where(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
where = super(SubdivisionMixin, cls)._where(tables, withs)
|
|
where &= address.subdivision != Null
|
|
return where
|
|
|
|
|
|
class Subdivision(SubdivisionMixin, Abstract):
|
|
"Sale Reporting per Subdivision"
|
|
__name__ = 'sale.reporting.country.subdivision'
|
|
|
|
time_series = fields.One2Many(
|
|
'sale.reporting.country.subdivision.time_series', 'subdivision',
|
|
"Time Series")
|
|
|
|
@classmethod
|
|
def __setup__(cls):
|
|
super().__setup__()
|
|
cls._order.insert(0, ('subdivision', 'ASC'))
|
|
|
|
@classmethod
|
|
def _column_id(cls, tables, withs):
|
|
address = tables['line.sale.shipment_address']
|
|
return address.subdivision
|
|
|
|
def get_rec_name(self, name):
|
|
return self.subdivision.rec_name
|
|
|
|
|
|
class SubdivisionTimeseries(SubdivisionMixin, AbstractTimeseries, ModelView):
|
|
"Sale Reporting per Subdivision"
|
|
__name__ = 'sale.reporting.country.subdivision.time_series'
|
|
|
|
|
|
class Region(UnionMixin, Abstract, ModelView):
|
|
"Sale Reporting per Region"
|
|
__name__ = 'sale.reporting.region'
|
|
|
|
region = fields.Function(fields.Char("Region"), 'get_rec_name')
|
|
parent = fields.Many2One('sale.reporting.region', "Parent")
|
|
children = fields.One2Many('sale.reporting.region', 'parent', "Children")
|
|
|
|
@classmethod
|
|
def union_models(cls):
|
|
return ['sale.reporting.country', 'sale.reporting.country.subdivision']
|
|
|
|
@classmethod
|
|
def union_column(cls, name, field, table, Model):
|
|
column = super(Region, cls).union_column(
|
|
name, field, table, Model)
|
|
if (name == 'parent'
|
|
and Model.__name__ == 'sale.reporting.country.subdivision'):
|
|
column = cls.union_shard(
|
|
Column(table, 'country'), 'sale.reporting.country')
|
|
return column
|
|
|
|
@classmethod
|
|
def get_rec_name(cls, records, name):
|
|
names = {}
|
|
classes = defaultdict(list)
|
|
for record in records:
|
|
record = cls.union_unshard(record.id)
|
|
classes[record.__class__].append(record)
|
|
for klass, records in classes.items():
|
|
for record in klass.browse(records):
|
|
names[cls.union_shard(record.id, klass.__name__)] = (
|
|
record.rec_name)
|
|
return names
|
|
|
|
@property
|
|
def time_series(self):
|
|
record = self.union_unshard(self.id)
|
|
return record.time_series
|
|
|
|
|
|
class OpenRegion(Wizard):
|
|
"Open Region"
|
|
__name__ = 'sale.reporting.region.open'
|
|
|
|
start = StateTransition()
|
|
country = StateAction('sale.act_reporting_country_time_series')
|
|
subdivision = StateAction(
|
|
'sale.act_reporting_country_subdivision_time_series')
|
|
|
|
def transition_start(self):
|
|
pool = Pool()
|
|
Country = pool.get('sale.reporting.country')
|
|
Subdivision = pool.get('sale.reporting.country.subdivision')
|
|
record = self.model.union_unshard(self.record.id)
|
|
if isinstance(record, Country):
|
|
return 'country'
|
|
elif isinstance(record, Subdivision):
|
|
return 'subdivision'
|
|
|
|
def _do_action(self, action):
|
|
record = self.model.union_unshard(self.record.id)
|
|
data = {
|
|
'id': record.id,
|
|
'ids': [record.id],
|
|
}
|
|
action['name'] += ' (%s)' % record.rec_name
|
|
return action, data
|
|
do_country = _do_action
|
|
do_subdivision = _do_action
|