From 25883d7a7c032f601f0c243664010db0e2eafacc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Krier?= Date: Fri, 11 Jun 2021 00:17:19 +0200 Subject: [PATCH] Add reporting issue10397 review342321002 --- CHANGELOG | 2 + __init__.py | 4 + commission.py | 11 + commission_reporting.py | 249 ++++++++++++++++++ commission_reporting.xml | 176 +++++++++++++ invoice.py | 5 +- setup.py | 5 +- tests/scenario_commission.rst | 27 ++ tryton.cfg | 1 + view/commission_form.xml | 8 +- view/commission_list.xml | 4 +- view/reporting_agent_graph_amount.xml | 12 + view/reporting_agent_graph_number.xml | 11 + view/reporting_agent_list.xml | 10 + ...porting_agent_time_series_graph_amount.xml | 12 + ...porting_agent_time_series_graph_number.xml | 11 + view/reporting_agent_time_series_list.xml | 9 + view/reporting_context_form.xml | 18 ++ 18 files changed, 567 insertions(+), 8 deletions(-) create mode 100644 commission_reporting.py create mode 100644 commission_reporting.xml create mode 100644 view/reporting_agent_graph_amount.xml create mode 100644 view/reporting_agent_graph_number.xml create mode 100644 view/reporting_agent_list.xml create mode 100644 view/reporting_agent_time_series_graph_amount.xml create mode 100644 view/reporting_agent_time_series_graph_number.xml create mode 100644 view/reporting_agent_time_series_list.xml create mode 100644 view/reporting_context_form.xml diff --git a/CHANGELOG b/CHANGELOG index fc3a3bd517..2ac8d57c3f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,5 @@ +* Add reporting + Version 6.0.0 - 2021-05-03 * Bug fixes (see mercurial logs for details) * Use the invoice date or payment date for commission date diff --git a/__init__.py b/__init__.py index 023995301a..b344598794 100644 --- a/__init__.py +++ b/__init__.py @@ -4,6 +4,7 @@ from trytond.pool import Pool from . import account from . import commission +from . import commission_reporting from . import invoice from . import sale from . import product @@ -19,6 +20,9 @@ def register(): commission.AgentSelection, commission.Commission, commission.CreateInvoiceAsk, + commission_reporting.Agent, + commission_reporting.AgentTimeseries, + commission_reporting.Context, invoice.Invoice, invoice.InvoiceLine, invoice.CreditInvoiceStart, diff --git a/commission.py b/commission.py index 8e3643dfc4..aa72b73482 100644 --- a/commission.py +++ b/commission.py @@ -344,11 +344,17 @@ class Commission(ModelSQL, ModelView): product = fields.Many2One('product.product', 'Product', required=True, states=_readonly_states, depends=_readonly_depends, help="The product that is used on the invoice line.") + base_amount = fields.Numeric( + "Base Amount", digits=(16, Eval('currency_digits', 2)), + states=_readonly_states, + depends=_readonly_depends + ['currency_digits']) amount = fields.Numeric('Amount', required=True, digits=price_digits, domain=[('amount', '!=', 0)], states=_readonly_states, depends=_readonly_depends) currency = fields.Function(fields.Many2One('currency.currency', 'Currency'), 'on_change_with_currency') + currency_digits = fields.Function(fields.Integer( + "Currency Digits"), 'on_change_with_currency_digits') type_ = fields.Function(fields.Selection([ ('in', 'Incoming'), ('out', 'Outgoing'), @@ -394,6 +400,11 @@ class Commission(ModelSQL, ModelView): if self.agent: return self.agent.currency.id + @fields.depends('agent') + def on_change_with_currency_digits(self, name=None): + if self.agent: + return self.agent.currency.digits + @fields.depends('agent') def on_change_with_type_(self, name=None): if self.agent: diff --git a/commission_reporting.py b/commission_reporting.py new file mode 100644 index 0000000000..7c1750f437 --- /dev/null +++ b/commission_reporting.py @@ -0,0 +1,249 @@ +# This file is part of Tryton. The COPYRIGHT file at the top level of +# this repository contains the full copyright notices and license terms. +from itertools import tee, zip_longest + +from dateutil.relativedelta import relativedelta +try: + import pygal +except ImportError: + pygal = None +from sql import Literal, Null +from sql.aggregate import Min, Sum, Count +from sql.functions import CurrentTimestamp, DateTrunc + +from trytond.model import ModelSQL, ModelView, fields +from trytond.pool import Pool +from trytond.pyson import Eval, If +from trytond.transaction import Transaction + +from trytond.modules.product import price_digits + + +def pairwise(iterable): + a, b = tee(iterable) + next(b) + return zip_longest(a, b) + + +class Agent(ModelView, ModelSQL): + "Commission Reporting per Agent" + __name__ = 'commission.reporting.agent' + + agent = fields.Many2One('commission.agent', 'Agent') + number = fields.Integer("Number") + + base_amount = fields.Numeric( + "Base Amount", digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + base_amount_trend = fields.Function( + fields.Char("Base Amount Trend"), 'get_trend') + amount = fields.Numeric("Amount", digits=price_digits) + + time_series = fields.One2Many( + 'commission.reporting.agent.time_series', 'agent', "Time Series") + + currency = fields.Function(fields.Many2One( + 'currency.currency', "Currency"), 'get_currency') + currency_digits = fields.Function(fields.Integer( + "Currency Digits"), 'get_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() + Commission = pool.get('commission') + Agent = pool.get('commission.agent') + + tables = {} + tables['commission'] = commission = Commission.__table__() + tables['commission.agent'] = agent = Agent.__table__() + withs = {} + + from_item = (commission + .join(agent, condition=commission.agent == agent.id)) + return from_item, tables, withs + + @classmethod + def _columns(cls, tables, withs): + commission = tables['commission'] + 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'), + commission.agent.as_('agent'), + Count(commission.id, distinct=True).as_('number'), + Sum(commission.base_amount).as_('base_amount'), + Sum(commission.amount).as_('amount'), + ] + + @classmethod + def _column_id(cls, tables, withs): + commission = tables['commission'] + return commission.agent + + @classmethod + def _group_by(cls, tables, withs): + commission = tables['commission'] + return [commission.agent] + + @classmethod + def _where(cls, tables, withs): + context = Transaction().context + commission = tables['commission'] + agent = tables['commission.agent'] + where = agent.type_ == cls._get_agent_type(context.get('type', 'out')) + from_date = context.get('from_date') + if from_date: + where &= commission.date >= from_date + to_date = context.get('to_date') + if to_date: + where &= commission.date <= to_date + if context.get('invoiced'): + where &= commission.invoice_line != Null + return where + + @classmethod + def _get_agent_type(cls, type): + return { + 'out': 'agent', + 'in': 'principal', + }.get(type) + + @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, 0) or 0 + for ts in self.time_series_all]) + return chart.render_sparktext() + + def get_currency(self, name): + return self.agent.currency.id + + def get_currency_digits(self, name): + return self.agent.currency.digits + + +class AgentTimeseries(Agent): + "Commission Reporting per Agent" + __name__ = 'commission.reporting.agent.time_series' + + date = fields.Date("Date") + time_series = None + + @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_id(cls, tables, withs): + commission = tables['commission'] + return Min(commission.id) + + @classmethod + def _column_date(cls, tables, withs): + context = Transaction().context + commission = tables['commission'] + date = DateTrunc(context.get('period'), commission.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): + "Commission Reporting Context" + __name__ = 'commission.reporting.context' + + type = fields.Selection([ + ('in', "Incoming"), + ('out', "Outgoing"), + ], "Type", 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) + invoiced = fields.Boolean( + "Invoiced", + help="Only included invoiced commissions.") + + @classmethod + def default_type(cls): + return Transaction().context.get('type', 'out') + + @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') diff --git a/commission_reporting.xml b/commission_reporting.xml new file mode 100644 index 0000000000..894a76d551 --- /dev/null +++ b/commission_reporting.xml @@ -0,0 +1,176 @@ + + + + + + + + + + commission.reporting.context + form + reporting_context_form + + + + commission.reporting.agent + tree + reporting_agent_list + + + + commission.reporting.agent + graph + reporting_agent_graph_amount + + + + commission.reporting.agent + graph + reporting_agent_graph_number + + + + Commissions per Agent + commission.reporting.agent + commission.reporting.context + + + + + + + + + + + + + + + + + + tree_open + + + + + + User in companies + + + + + + + + + + + + + + + + + + + + + + + + + + commission.reporting.agent.time_series + tree + reporting_agent_time_series_list + + + + commission.reporting.agent.time_series + graph + reporting_agent_time_series_graph_amount + + + + commission.reporting.agent.time_series + graph + reporting_agent_time_series_graph_number + + + + Commissions per Agent + commission.reporting.agent.time_series + commission.reporting.context + + + + + + + + + + + + + + + + + + + + tree_open + commission.reporting.agent,-1 + + + + + User in companies + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/invoice.py b/invoice.py index 53dcbe3bc8..a9be38d74b 100644 --- a/invoice.py +++ b/invoice.py @@ -162,9 +162,9 @@ class InvoiceLine(metaclass=PoolMeta): if not plan: continue with Transaction().set_context(date=self.invoice.currency_date): - amount = Currency.compute(self.invoice.currency, + base_amount = Currency.compute(self.invoice.currency, self.amount, agent.currency, round=False) - amount = self._get_commission_amount(amount, plan) + amount = self._get_commission_amount(base_amount, plan) if amount: amount = round_price(amount) if not amount: @@ -179,6 +179,7 @@ class InvoiceLine(metaclass=PoolMeta): commission.date = self.invoice.reconciled or today commission.agent = agent commission.product = plan.commission_product + commission.base_amount = base_amount commission.amount = amount commissions.append(commission) return commissions diff --git a/setup.py b/setup.py index c9f146fdad..4176e74211 100755 --- a/setup.py +++ b/setup.py @@ -57,7 +57,7 @@ else: if local_version: version += '+' + '.'.join(local_version) -requires = ['simpleeval', 'python-sql'] +requires = ['simpleeval', 'python-sql', 'python-dateutil'] for dep in info.get('depends', []): if not re.match(r'(ir|res)(\W|$)', dep): requires.append(get_require_version('trytond_%s' % dep)) @@ -139,6 +139,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=""" diff --git a/tests/scenario_commission.rst b/tests/scenario_commission.rst index 03b3f98a16..320a84ddcc 100644 --- a/tests/scenario_commission.rst +++ b/tests/scenario_commission.rst @@ -22,6 +22,9 @@ Activate modules:: >>> config = activate_modules('commission') + >>> ReportingAgent = Model.get('commission.reporting.agent') + >>> ReportingAgentTimeseries = Model.get('commission.reporting.agent.time_series') + Create company:: >>> _ = create_company() @@ -151,6 +154,8 @@ Post invoice:: >>> line, = invoice.lines >>> len(line.commissions) 2 + >>> [c.base_amount for c in line.commissions] + [Decimal('100.00'), Decimal('100.00')] >>> [c.amount for c in line.commissions] [Decimal('10.0000'), Decimal('10.0000')] >>> [c.invoice_state for c in line.commissions] @@ -222,3 +227,25 @@ Credit invoice:: >>> credit_note, = credit.actions[0] >>> credit_note.agent == agent True + +Check commission reporting per agent:: + + >>> with config.set_context(type='out'): + ... reporting_agent, = ReportingAgent.find([]) + ... reporting_agent_timeseries, = ReportingAgentTimeseries.find([]) + + >>> reporting_agent.base_amount == Decimal('100.00') + True + >>> reporting_agent.amount == Decimal('10.0000') + True + >>> reporting_agent.number + 1 + + >>> reporting_agent_timeseries.date == tomorrow + True + >>> reporting_agent_timeseries.base_amount == Decimal('100.00') + True + >>> reporting_agent_timeseries.amount == Decimal('10.00') + True + >>> reporting_agent_timeseries.number + 1 diff --git a/tryton.cfg b/tryton.cfg index aa56f53311..349ade0748 100644 --- a/tryton.cfg +++ b/tryton.cfg @@ -12,6 +12,7 @@ extras_depend: sale xml: commission.xml + commission_reporting.xml invoice.xml sale.xml account.xml diff --git a/view/commission_form.xml b/view/commission_form.xml index 8bb90bcd7c..a2969f7d9d 100644 --- a/view/commission_form.xml +++ b/view/commission_form.xml @@ -10,10 +10,12 @@ this repository contains the full copyright notices and license terms. -->