From a05df151f441cbbcd0471ddede50aff4c686249d Mon Sep 17 00:00:00 2001 From: wilson gomez Date: Tue, 18 Jan 2022 11:21:11 -0500 Subject: [PATCH] add goal reporting --- __init__.py | 6 +- goal.py | 391 +++++------------ goal.xml | 25 +- goal_reporting.py | 445 ++++++++++++++++++++ goal_reporting.xml | 101 +++++ message.xml | 35 ++ sale.py | 136 ------ sale.xml | 20 - sale_month_by_shop.ods | Bin 17525 -> 0 bytes tryton.cfg | 5 +- view/account_reporting_operation_center.xml | 10 + view/goal_annual_ranking_start_form.xml | 4 +- view/goal_line_form.xml | 24 +- view/goal_line_list.xml | 11 + view/goal_line_tree.xml | 10 +- view/goal_reporting_context_form.xml | 19 + view/goal_reporting_indicator_list.xml | 9 + view/goal_tree.xml | 2 +- view/sale_indicator_form.xml | 6 +- view/sale_indicator_tree.xml | 6 +- view/sale_month_shop_start_form.xml | 13 - 21 files changed, 786 insertions(+), 492 deletions(-) create mode 100644 goal_reporting.py create mode 100644 goal_reporting.xml create mode 100644 message.xml delete mode 100644 sale_month_by_shop.ods create mode 100644 view/account_reporting_operation_center.xml create mode 100644 view/goal_line_list.xml create mode 100644 view/goal_reporting_context_form.xml create mode 100644 view/goal_reporting_indicator_list.xml delete mode 100644 view/sale_month_shop_start_form.xml diff --git a/__init__.py b/__init__.py index 54dabb4..27224bb 100644 --- a/__init__.py +++ b/__init__.py @@ -3,6 +3,7 @@ # the full copyright notices and license terms. from trytond.pool import Pool from . import goal +from . import goal_reporting from . import sale @@ -15,14 +16,14 @@ def register(): goal.SaleGoalAnnualRankingStart, goal.SaleGoalJournalStart, sale.SaleDailyStart, - sale.SaleMonthByShopStart, + goal_reporting.Context, + goal_reporting.GoalIndicator, module='sale_goal', type_='model') Pool.register( goal.SaleGoalMonthRanking, goal.SaleGoalAnnualRanking, goal.SaleGoalJournal, sale.SaleDaily, - sale.SaleMonthByShop, module='sale_goal', type_='wizard') Pool.register( goal.SaleGoalReport, @@ -30,5 +31,4 @@ def register(): goal.SaleGoalMonthRankingReport, goal.SaleGoalAnnualRankingReport, sale.SaleDailyReport, - sale.SaleMonthByShopReport, module='sale_goal', type_='report') diff --git a/goal.py b/goal.py index 3ea05de..8ce4168 100644 --- a/goal.py +++ b/goal.py @@ -15,6 +15,19 @@ _STATES = { 'readonly': Eval('state') != 'draft', } +KIND = [ + ('salesman', 'Salesman'), + ('product', 'Product'), + ('category', 'Category'), + ] + +TYPE = [ + ('monthly', 'Monthly'), + ('bimonthly', 'Bimonthly'), + ('quarterly', 'Quarterly'), + ('annual', 'Annual'), +] + _ZERO = Decimal(0) @@ -109,206 +122,38 @@ class Goal(Workflow, ModelSQL, ModelView): periods_goal.append(p) return periods_goal - @classmethod - def _get_sales_by_category_month(cls, period, indicator_id, data): - #FIXME bug when any period is missing in fiscal year - pool = Pool() - InvoiceLine = pool.get('account.invoice.line') - if indicator_id == 0: - indicator_id = None - dom_ = [ - ('invoice.company', '=', data['company']), - ('invoice.invoice_date', '>=', period.start_date), - ('invoice.invoice_date', '<=', period.end_date), - ('invoice.type', '=', 'out'), - ('invoice.shop', '=', data['shop']), - ('invoice.state', 'in', ['posted', 'paid']), - ('product.template.account_category', '=', indicator_id), - ] - lines = InvoiceLine.search(dom_) - if data['report_kind'] == 'total_sale': - amount = sum([i.amount for i in lines]) - else: - amount = sum([i.amount - l.product.template.cost_price for l in lines]) - return amount - - @classmethod - def _get_sales_month(cls, period, indicator_id, data): - #FIXME bug when any period is missing in fiscal year - pool = Pool() - Invoice = pool.get('account.invoice') - if indicator_id == 0: - indicator_id = None - dom_ = [ - ('company', '=', data['company']), - ('invoice_date', '>=', period.start_date), - ('invoice_date', '<=', period.end_date), - ('type', '=', 'out'), - ('state', 'in', ['posted', 'paid', 'validated']), - ] - # if data.get('indicator'): - # indicator_field = data['indicator'] - # dom.append((indicator_field, '=', indicator_id)) - - invoices = Invoice.search(dom_) - - if data.get('report_kind') in [None, 'total_sale']: - amount = sum([i.total_amount for i in invoices]) - else: - amount = sum([i.untaxed_amount - cls.get_inv_cost(i) for i in invoices]) - return amount - - @classmethod - def sales_monthly(cls, data): - pool = Pool() - Fiscalyear = pool.get('account.fiscalyear') - Employee = pool.get('company.employee') - Company = pool.get('company.company') - Shop = pool.get('sale.shop') - fiscalyear_id = data.get('fiscalyear', None) - - if not fiscalyear_id: - dom = [ - ('start_date', '<=', date.today()), - ('end_date', '>=', date.today()), - ] - fiscalyears = Fiscalyear.search(dom) - if fiscalyears: - fiscalyear = fiscalyears[0] - else: - fiscalyear = Fiscalyear(fiscalyear_id) - - fiscalyear_id = fiscalyear.id - - report_context = {} - indicators = {} - shop_name = '' - periods_zeros = cls.get_periods_zeros(fiscalyear) - periods_goal = cls.get_periods_goal(fiscalyear) - sum_annual_goal = [] - # data.update(periods_zeros.copy()) - if not data.get('indicator'): - records = [Company(data['company'])] - elif data['indicator'] == 'shop': - records = Shop.search([]) - elif data['indicator'] == 'by_category': - shop = Shop(data['shop']) - records = shop.product_categories - shop_name = shop.name - else: - records = [] - cursor = Transaction().connection.cursor() - query = "SELECT salesman FROM account_invoice WHERE invoice_date>='%s' AND invoice_date<='%s' GROUP BY salesman" - cursor.execute(query % (fiscalyear.start_date, fiscalyear.end_date)) - values = [i[0] for i in cursor.fetchall()] - if None in values: - records.append(None) - values.remove(None) - records.extend(Employee.browse(values)) - - total_year = [] - for ind in records: - if ind: - if not data.get('indicator'): - # By company - ind_name = ind.party.name - elif data['indicator'] == 'shop': - ind_name = ind.name - elif data['indicator'] == 'by_category': - ind_name = ind.name - ind_id = ind.id - else: - ind_id = 0 - ind_name = 'N.D.' - - if ind_id not in indicators.keys(): - indicators[ind_id] = {} - indicators[ind_id].update(periods_zeros.copy()) - - dom_goal = [ - ('goal.fiscalyear', '=', fiscalyear_id) - ] - - if not data.get('indicator'): - attribute = None - elif data['indicator'] == 'shop': - attribute = 'indicator.shop' - elif data['indicator'] == 'salesman': - attribute = 'indicator.salesman' - - if attribute: - dom_goal.append( - (attribute, '=', ind_id), - ) - goal_lines = GoalLine.search(dom_goal) - annual_goal = sum([l.amount for l in goal_lines]) - sum_annual_goal.append(annual_goal) - - res_total = _ZERO - for p in periods_goal: - nperiod = p.name[-2:] - if data.get('indicator') in (None, 'salesman', 'shop'): - res = cls._get_sales_month( - p, - ind_id, - data, - ) - else: # by_category - res = cls._get_sales_by_category_month( - p, - ind_id, - data, - ) - indicators[ind_id][nperiod] = res - - res_total += res - # data[nperiod] += res - total_year.append(res) - - if annual_goal > 0: - achieve = (float(res_total) / float(annual_goal)) * 100 - else: - achieve = '---' - - labels = [] - values = [] - for ind in indicators.values(): - for k, v in ind.items(): - labels.append(calendar.month_abbr[int(k)]) - values.append(v) - - report_context['data'] = data - report_context['shop'] = shop_name - report_context['labels'] = labels - report_context['values'] = values - report_context['year'] = fiscalyear.name - report_context['total_year'] = sum(total_year) - report_context['sum_annual_goal'] = sum(sum_annual_goal) - if sum(sum_annual_goal) > 0: - res = (float(report_context['total_year']) / float(sum(sum_annual_goal))) * 100 - else: - res = '---' - report_context['avg_achieve'] = res - return report_context - - -class GoalLine(Workflow, ModelSQL, ModelView): +class GoalLine(ModelSQL, ModelView): "Goal Line" __name__ = 'sale.goal.line' goal = fields.Many2One('sale.goal', 'Goal', required=True) - indicator = fields.Many2One('sale.indicator', 'Indicator', - required=True) - period = fields.Many2One('account.period', 'Period', required=True, - domain=[ - ('fiscalyear', '=', Eval('_parent_goal', {}).get('fiscalyear')), - ]) - amount = fields.Numeric('Amount', digits=(16, 2), required=False) + kind = fields.Selection(KIND, 'kind', states={ + 'required': True + }) + type = fields.Selection(TYPE, 'Type', required=True) + + start_date = fields.Date('Start Date', required=True) + end_date = fields.Date('End Date', required=True) + lines = fields.One2Many('sale.indicator', 'goal_line', 'Goal Lines') + fixed_amount = fields.Numeric('Fixed Amount', digits=(16, 2)) + type_string = type.translated('type') @classmethod def __setup__(cls): super(GoalLine, cls).__setup__() + @classmethod + def __register__(cls, module_name): + super(GoalLine, cls).__register__(module_name) + sql_table = cls.__table_handler__(module_name) + + if sql_table.column_exist('indicator'): + sql_table.drop_column('indicator') + if sql_table.column_exist('period'): + sql_table.drop_column('period') + if sql_table.column_exist('amount'): + sql_table.column_rename('amount', 'fixed_amount') + def get_sales_period(self, period): Invoice = Pool().get('account.invoice') InvoiceLine = Pool().get('account.invoice.line') @@ -342,80 +187,42 @@ class GoalLine(Workflow, ModelSQL, ModelView): class SaleIndicator(ModelSQL, ModelView): "Sale Indicator" __name__ = 'sale.indicator' - _rec_name = 'name' - name = fields.Char('Name', states={ - 'required': True, - 'readonly': True, - }, depends=['salesman', 'shop']) - kind = fields.Selection([ - ('salesman', 'Salesman'), - ('shop', 'Shop'), - ('product', 'Product'), - ('category', 'Category'), - ], 'Kind', required=True) - salesman = fields.Many2One('company.employee', 'Salesman', - states={ - 'required': Eval('kind') == 'salesman', - 'invisible': Eval('kind') != 'salesman', - }, depends=['kind']) - shop = fields.Many2One('sale.shop', 'Shop', - states={ - 'required': Eval('kind') == 'shop', - 'invisible': Eval('kind') != 'shop', - }, depends=['kind']) + # _rec_name = 'name' + + name = fields.Function(fields.Char('Name'), 'get_rec_name') + goal_line = fields.Many2One('sale.goal.line', 'Goal Line', required=True) + salesman = fields.Many2One('company.employee', 'Salesman') product = fields.Many2One('product.product', 'Product', domain=[ - ('template.type', '=', 'goods'), + ('template.type', 'in', ['goods', 'services']), ('template.salable', '=', True), - ], - states={ - 'required': Eval('kind') == 'product', - 'invisible': Eval('kind') != 'product', - }, depends=['kind']) - category = fields.Many2One('product.category', 'Category', - states={ - 'required': Eval('kind') == 'category', - 'invisible': Eval('kind') != 'category', - }, depends=['kind']) + ]) + category = fields.Many2One('product.category', 'Category') - @fields.depends('kind', 'name', 'shop', 'salesman') - def on_change_kind(self): - self.shop = None - self.salesman = None - self.product = None - self.category = None + amount = fields.Numeric('Amount', digits=(16, 2), required=False) - @fields.depends('kind', 'name', 'salesman', 'shop', 'product', 'category') - def on_change_shop(self): - if self.kind == 'shop' and self.shop: - self.name = self.shop.name - self.salesman = None - self.product = None - self.category = None + @classmethod + def __register__(cls, module_name): + super(SaleIndicator, cls).__register__(module_name) + table = cls.__table_handler__(module_name) - @fields.depends('kind', 'name', 'salesman', 'shop', 'product', 'category') - def on_change_salesman(self): - if self.kind == 'salesman' and self.salesman: - self.name = self.salesman.party.name - self.shop = None - self.product = None - self.category = None + if table.column_exist('name'): + table.drop_column('name') - @fields.depends('kind', 'name', 'salesman', 'shop', 'product', 'category') - def on_change_product(self): - if self.kind == 'product' and self.product: - self.name = self.product.rec_name - self.salesman = None - self.shop = None - self.category = None + if table.column_exist('kind'): + table.drop_column('kind') - @fields.depends('kind', 'name', 'salesman', 'shop', 'product', 'category') - def on_change_category(self): - if self.kind == 'category' and self.category: - self.name = self.category.name - self.salesman = None - self.shop = None - self.product = None + def get_rec_name(self, name): + if self.goal_line: + type = '' + if self.goal_line.type: + type = self.goal_line.type + attribute = self.goal_line.kind + return getattr(self, attribute).name + '_' + type + + def get_period_name(self, name): + if self.goal_line and self.goal_line.type: + pass class SaleGoalReport(Report): @@ -458,9 +265,17 @@ class SaleGoalJournalStart(ModelView): fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscalyear', required=True) indicator = fields.Many2One('sale.indicator', 'Indicator', required=True, domain=[('kind', 'in', ('salesman', 'shop'))]) - period = fields.Many2One('account.period', 'Period', domain=[ - ('fiscalyear', '=', Eval('fiscalyear')), - ], depends=['fiscalyear'], required=True) + type = fields.Selection([ + ('monthly', 'Monthly'), + ('annual', 'Annual'), + ], 'Type', required=True) + period = fields.Many2One('account.period', 'Period', + states = { + 'required': Eval('type') == 'monthly', + }, + domain=[ + ('fiscalyear', '=', Eval('fiscalyear')), + ], depends=['fiscalyear'],) @staticmethod def default_company(): @@ -661,7 +476,6 @@ class SaleGoalAnnualRankingStart(ModelView): fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year', required=True) company = fields.Many2One('company.company', 'Company', required=True) indicator = fields.Selection([ - ('shop', 'Shop'), ('salesman', 'Salesman'), ('by_category', 'By Category'), ], 'Indicator', required=True) @@ -669,10 +483,6 @@ class SaleGoalAnnualRankingStart(ModelView): ('profit', 'Profit'), ('total_sale', 'Total Sale'), ], 'Report Kind', required=True) - shop = fields.Many2One('sale.shop', 'Shop', states={ - 'required': Eval('indicator') == 'by_category', - 'invisible': Eval('indicator') != 'by_category' - }) @staticmethod def default_report_class(): @@ -682,10 +492,6 @@ class SaleGoalAnnualRankingStart(ModelView): def default_company(): return Transaction().context.get('company') - @staticmethod - def default_indicator(): - return 'shop' - @staticmethod def default_fiscalyear(): FiscalYear = Pool().get('account.fiscalyear') @@ -704,16 +510,11 @@ class SaleGoalAnnualRanking(Wizard): print_ = StateReport('sale_goal.annual_ranking_report') def do_print_(self, action): - shop_id = None - if self.start.shop: - shop_id = self.start.shop.id - data = { 'report_kind': self.start.report_kind, 'company': self.start.company.id, 'fiscalyear': self.start.fiscalyear.id, 'indicator': self.start.indicator, - 'shop': shop_id } return action, data @@ -761,10 +562,11 @@ class SaleGoalAnnualRankingReport(Report): ('invoice.invoice_date', '>=', period.start_date), ('invoice.invoice_date', '<=', period.end_date), ('invoice.type', '=', 'out'), - ('invoice.shop', '=', data['shop']), ('invoice.state', 'in', ['posted', 'paid']), ('product.template.account_category', '=', indicator_id), ] + if data.get('shop'): + dom_.append([('invoice.shop', '=', data['shop']), ]) lines = InvoiceLine.search(dom_) if data['report_kind'] == 'total_sale': @@ -785,14 +587,32 @@ class SaleGoalAnnualRankingReport(Report): res.append((line.product.template.cost_price + expense) * Decimal(line.quantity)) return sum(res) + @classmethod + def get_records_to_eval(cls, fiscalyear, data): + pool = Pool() + Employee = pool.get('company.employee') + Category = pool.get('product.category') + records = [] + if data['indicator'] == 'by_category': + records = Category.search([('templates', '!=', None)]) + else: + cursor = Transaction().connection.cursor() + query = "SELECT salesman FROM account_invoice WHERE invoice_date>='%s' AND invoice_date<='%s' GROUP BY salesman" + cursor.execute(query % (fiscalyear.start_date, fiscalyear.end_date)) + values = [i[0] for i in cursor.fetchall()] + if None in values: + records.append(None) + values.remove(None) + records.extend(Employee.browse(values)) + return records + @classmethod def get_context(cls, records, header, data): report_context = super().get_context(records, header, data) pool = Pool() Fiscalyear = pool.get('account.fiscalyear') - Employee = pool.get('company.employee') + Shop = pool.get('sale.shop') - # Category = pool.get('product.category') Goal = pool.get('sale.goal') fiscalyear = Fiscalyear(data['fiscalyear']) @@ -805,22 +625,11 @@ class SaleGoalAnnualRankingReport(Report): data.update(periods_zeros.copy()) if data['indicator'] == 'shop': records = Shop.search([]) - elif data['indicator'] == 'by_category': - shop = Shop(data['shop']) - records = shop.product_categories - shop_name = shop.name - else: - records = [] - cursor = Transaction().connection.cursor() - query = "SELECT salesman FROM account_invoice WHERE invoice_date>='%s' AND invoice_date<='%s' GROUP BY salesman" - cursor.execute(query % (fiscalyear.start_date, fiscalyear.end_date)) - values = [i[0] for i in cursor.fetchall()] - if None in values: - records.append(None) - values.remove(None) - records.extend(Employee.browse(values)) + records = cls.get_records_to_eval(fiscalyear, data) total_year = [] + if len(records) < 1: + records = [None] for ind in records: if ind: if data['indicator'] == 'shop': diff --git a/goal.xml b/goal.xml index 0c64250..7f9d701 100644 --- a/goal.xml +++ b/goal.xml @@ -58,12 +58,29 @@ this repository contains the full copyright notices and license terms. --> goal_line_tree - + sale.goal.line tree - goal_line_tree2 + goal_line_list + + Sale Goal Line + sale.goal.line + + + + + + + + + + + + + sale.indicator form @@ -74,7 +91,7 @@ this repository contains the full copyright notices and license terms. --> tree sale_indicator_tree - + + sequence="8" id="menu_sale_indicator" action="act_sale_indicator_tree"/> --> diff --git a/goal_reporting.py b/goal_reporting.py new file mode 100644 index 0000000..2d9ca4c --- /dev/null +++ b/goal_reporting.py @@ -0,0 +1,445 @@ +# This file is part sale_shop module for Tryton. +# The COPYRIGHT file at the top level of this repository contains the full +# copyright notices and license terms. +import calendar +from decimal import Decimal +from trytond.pool import Pool +from trytond.model import Workflow, ModelView, ModelSQL, fields +from trytond.pyson import Eval, If +from trytond.wizard import Wizard, StateView, Button, StateAction, StateReport +from trytond.report import Report +from sql import Null, Literal, Column, With +from sql.aggregate import Sum, Min +from sql.conditionals import Case +from trytond.transaction import Transaction +from trytond.i18n import lazy_gettext +from itertools import tee, zip_longest +from dateutil.relativedelta import relativedelta +from sql.functions import CurrentTimestamp, DateTrunc, Extract, Trunc +from sql.operators import Concat +from .goal import KIND, TYPE + +type_dict = { + 'monthly': Extract('MONTH', ''), + 'bimonthly': Trunc((Extract('MONTH', '')+1)/2), + 'quarterly': Extract('QUARTER', ''), + 'annual': Extract('YEAR', ''), +} + +_ZERO = Decimal(0) + + +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_goal.msg_goal_reporting_company")) + quantity = fields.Integer(lazy_gettext("sale_goal.msg_goal_reporting_number"), + help=lazy_gettext("sale_goal.msg_goal_reporting_number_help")) + amount_target = fields.Numeric( + lazy_gettext("sale_goal.msg_goal_reporting_amount_target"), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + amount_achieved = fields.Numeric( + lazy_gettext("sale_goal.msg_goal_reporting_amount_achieved"), + digits=(16, Eval('currency_digits', 2)), + depends=['currency_digits']) + # percentage = fields.Numeric( + # lazy_gettext("sale_goal.msg_goal_reporting_percentage"), + # digits=(16, Eval('currency_digits', 2)), + # depends=['currency_digits']) + + time_series = None + + currency = fields.Function(fields.Many2One( + 'currency.currency', + lazy_gettext("sale.msg_sale_reporting_currency")), + 'get_currency') + currency_digits = fields.Function( + fields.Integer( + lazy_gettext("sale.msg_sale_reporting_currency_digits")), + 'get_currency_digits') + + @classmethod + def table_query(cls): + from_item, tables, withs = cls._joins() + print(from_item.select(*cls._columns(tables, withs), + where=cls._where(tables, withs), + group_by=cls._group_by(tables, withs), + with_=withs.values())) + 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('account.invoice.line') + Invoice = pool.get('account.invoice') + + tables = {} + tables['line'] = line = Line.__table__() + tables['line.invoice'] = invoice = Invoice.__table__() + tables['line.invoice.company'] = company = Company.__table__() + + withs = {} + currency_invoice = With(query=Currency.currency_rate_sql()) + withs['currency_invoice'] = currency_invoice + currency_company = With(query=Currency.currency_rate_sql()) + withs['currency_company'] = currency_company + + from_item = (line + .join(invoice, condition=line.invoice == invoice.id) + .join(currency_invoice, + condition=(invoice.currency == currency_invoice.currency) + & (currency_invoice.start_date <= invoice.invoice_date) + & ((currency_invoice.end_date == Null) + | (currency_invoice.end_date >= invoice.invoice_date)) + ) + .join(company, condition=invoice.company == company.id) + .join(currency_company, + condition=(company.currency == currency_company.currency) + & (currency_company.start_date <= invoice.invoice_date) + & ((currency_company.end_date == Null) + | (currency_company.end_date >= invoice.invoice_date)) + )) + return from_item, tables, withs + + @classmethod + def _columns(cls, tables, withs): + line = tables['line'] + invoice = tables['line.invoice'] + currency_company = withs['currency_company'] + currency_invoice = withs['currency_invoice'] + + quantity = line.quantity + amount_achieved = cls.amount_achieved.sql_cast( + Sum(quantity * line.unit_price + * currency_company.rate / currency_invoice.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'), + invoice.company.as_('company'), + amount_achieved.as_('amount_achieved'), + Sum(quantity).as_('quantity'), + ] + + @classmethod + def _column_id(cls, tables, withs): + line = tables['line'] + return Min(line.id) + + @classmethod + def _group_by(cls, tables, withs): + invoice = tables['line.invoice'] + return [invoice.company] + + @classmethod + def _having(cls, tables, withs): + having = (0 == 0) + return having + + @classmethod + def _where(cls, tables, withs): + context = Transaction().context + invoice = tables['line.invoice'] + + where = invoice.company == context.get('company') + where &= invoice.state.in_(cls._invoice_states()) + from_date = context.get('from_date') + if from_date: + where &= invoice.invoice_date >= from_date + to_date = context.get('to_date') + if to_date: + where &= invoice.invoice_date <= to_date + return where + + # @classmethod + # def _column_amount_achieved(cls, tables, withs, sign): + # move = tables['move'] + # currency = withs['currency_rate'] + # currency_company = withs['currency_rate_company'] + # return Case(Sum( + # sign * cls.revenue.sql_cast(move.quantity) * move.unit_price + # * currency_company.rate / currency.rate)) + + @classmethod + def _invoice_states(cls): + return ['posted', 'paid', 'validated'] + + @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 + + def get_currency_digits(self, name): + return self.company.currency.digits + + +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 + move = tables['line.move'] + date = DateTrunc(context.get('period'), move.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): + "Goal Reporting Context" + __name__ = 'goal.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) + type = fields.Selection(TYPE, 'Type', required=True) + + kind = fields.Selection(KIND, 'kind', states={ + 'required': True + }) + + @classmethod + def default_company(cls): + return Transaction().context.get('company') + + @classmethod + def default_type(cls): + return 'monthly' + + @classmethod + def default_kind(cls): + return 'salesman' + + @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') + + +class GoalIndicatorMixin(object): + __slots__ = () + indicator = fields.Many2One( + 'sale.indicator', "Indicator", + context={ + 'company': Eval('company', -1), + }, + depends=['company']) + year = fields.Numeric('Year') + period = fields.Numeric('Period') + year_period = fields.Char('Year Period') + + @classmethod + def _joins(cls): + from_item, tables, withs = super(GoalIndicatorMixin, cls)._joins() + pool = Pool() + Indicator = pool.get('sale.indicator') + GoalLine = pool.get('sale.goal.line') + Goal = pool.get('sale.goal') + Product = pool.get('product.product') + TemplateCategory = pool.get('product.template-product.category.all') + + tables['line.product'] = product = Product.__table__() + tables['line.product.template_category'] = template_category = TemplateCategory.__table__() + tables['sale.indicator'] = indicator = Indicator.__table__() + tables['sale.goal.line'] = goal_line = GoalLine.__table__() + tables['sale.goal'] = goal = Goal.__table__() + invoice = tables['line.invoice'] + line = tables['line'] + + context = Transaction().context + kind = context['kind'] + if kind == 'salesman': + from_item = (from_item + .join(indicator, condition=indicator.salesman == invoice.salesman)) + elif kind == 'category': + from_item = (from_item + .join(product, condition=line.product == product.id) + .join(template_category, + condition=product.template == template_category.template) + .join(indicator, + condition=indicator.category == template_category.id)) + elif kind == 'product': + from_item = (from_item + .join(indicator, condition=indicator.product == line.product)) + from_item = (from_item + .join(goal_line, condition=goal_line.id == indicator.goal_line)) + return from_item, tables, withs + + @classmethod + def _columns(cls, tables, withs): + indicator = tables['sale.indicator'] + invoice = tables['line.invoice'] + type_year_period = cls.year_period.sql_type().base + columns_ = [ + indicator.id.as_('indicator'), + Extract('YEAR', invoice.invoice_date).as_('year'), + cls._column_date(tables, withs).as_('period'), + indicator.amount.as_('amount_target'), + Concat(Extract('YEAR', invoice.invoice_date).cast(type_year_period), cls._column_date(tables, withs).cast(type_year_period)).as_('year_period') + ] + return super(GoalIndicatorMixin, cls)._columns(tables, withs) + columns_ + + @classmethod + def _column_date(cls, tables, withs): + context = Transaction().context + invoice = tables['line.invoice'] + type = context.get('type') + if type == 'monthly': + date = Extract('MONTH', invoice.invoice_date) + elif type == 'bimonthly': + date = Trunc((Extract('MONTH', invoice.date)+1)/2) + elif type == 'quarterly': + date = Extract('QUARTER', invoice.date), + elif type == 'annual': + date = Extract('YEAR', invoice.date), + + date = cls.period.sql_cast(date) + return date + + @classmethod + def _group_by(cls, tables, withs): + indicator = tables['sale.indicator'] + invoice = tables['line.invoice'] + type_year_period = cls.year_period.sql_type().base + group_by_ = [ + indicator.id, + Extract('YEAR', invoice.invoice_date), + cls._column_date(tables, withs), + indicator.amount.as_('amount_target'), + Concat(Extract('YEAR', invoice.invoice_date).cast(type_year_period), cls._column_date(tables, withs).cast(type_year_period)) + ] + return super(GoalIndicatorMixin, cls)._group_by(tables, withs) + group_by_ + + @classmethod + def _having(cls, tables, withs): + having = super(GoalIndicatorMixin, cls)._having(tables, withs) + + having &= (0 == 0) + return having + + # def get_rec_name(self, name): + # return self.indicator.rec_name + + @classmethod + def _where(cls, tables, withs): + where = super(GoalIndicatorMixin, cls)._where(tables, withs) + context = Transaction().context + invoice = tables['line.invoice'] + + where = invoice.company == context.get('company') + where &= invoice.state.in_(cls._invoice_states()) + from_date = context.get('from_date') + if from_date: + where &= invoice.invoice_date >= from_date + to_date = context.get('to_date') + if to_date: + where &= invoice.invoice_date <= to_date + return where + + +class GoalIndicator(GoalIndicatorMixin, Abstract, ModelView): + "Goal Reporting per Indicator" + __name__ = 'goal.reporting.indicator' + + # time_series = fields.One2Many( + # 'goal.reporting.indicator.time_series', 'Indicator', "Time Series") + + @classmethod + def __setup__(cls): + super().__setup__() + # cls._order.insert(0, ('indicator', 'ASC')) + + @classmethod + def _column_id(cls, tables, withs): + indicator = tables['sale.indicator'] + return indicator.id diff --git a/goal_reporting.xml b/goal_reporting.xml new file mode 100644 index 0000000..6ae5b02 --- /dev/null +++ b/goal_reporting.xml @@ -0,0 +1,101 @@ + + + + + + + + + + + + + + goal.reporting.context + form + goal_reporting_context_form + + + + + + goal.reporting.indicator + tree + goal_reporting_indicator_list + + + + goal.reporting.indicator + graph + goal_reporting_indicator_graph_amount_achieved + + + + goal.reporting.indicator + graph + goal_reporting_indicator_graph_percentage + + + + Goal Indicators + goal.reporting.indicator + goal.reporting.context + + + + + + + + + + + + + + + + + + tree_open + + + + + + User in companies + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/message.xml b/message.xml new file mode 100644 index 0000000..0f3faf6 --- /dev/null +++ b/message.xml @@ -0,0 +1,35 @@ + + + + + + Company + + + # + + + Number of products + + + Amount Target + + + Amount Achieved + + + Percentage + + + Salesman + + + Category + + + Product + + + + diff --git a/sale.py b/sale.py index 7e2c56a..e15bcdb 100644 --- a/sale.py +++ b/sale.py @@ -3,7 +3,6 @@ # copyright notices and license terms. from decimal import Decimal from datetime import date -import calendar from trytond.pool import Pool from trytond.model import ModelView, fields from trytond.pyson import Eval @@ -133,138 +132,3 @@ class SaleDailyReport(Report): report_context['journal'] = journal_name report_context['total_journal_paid'] = sum(total_journal_paid_) return report_context - - -class SaleMonthByShopStart(ModelView): - 'Sale Month By Shop Start' - __name__ = 'sale_goal.sale_month_shop.start' - company = fields.Many2One('company.company', 'Company', required=True) - shop = fields.Many2One('sale.shop', 'Shop', required=True, - depends=['company'], domain=[ - ('company', '=', Eval('company')) - ]) - fiscalyear = fields.Many2One('account.fiscalyear', 'Fiscal Year', - required=True) - period = fields.Many2One('account.period', 'Period', - depends=['fiscalyear'], required=True, domain=[ - ('fiscalyear', '=', Eval('fiscalyear')), - ]) - - @staticmethod - def default_company(): - return Transaction().context.get('company') - - @staticmethod - def default_fiscalyear(): - FiscalYear = Pool().get('account.fiscalyear') - return FiscalYear.find( - Transaction().context.get('company'), exception=False) - - @fields.depends('fiscalyear') - def on_change_fiscalyear(self): - self.period = None - - -class SaleMonthByShop(Wizard): - 'Sale MonthByShop' - __name__ = 'sale_goal.sale_month_shop' - start = StateView('sale_goal.sale_month_shop.start', - 'sale_goal.sale_month_shop_start_view_form', [ - Button('Cancel', 'end', 'tryton-cancel'), - Button('Print', 'print_', 'tryton-ok', default=True), - ]) - print_ = StateReport('sale_goal.sale_month_shop_report') - - def do_print_(self, action): - data = { - 'company': self.start.company.id, - 'shop': self.start.shop.id, - 'period': self.start.period.id, - 'fiscalyear': self.start.fiscalyear.id, - } - return action, data - - def transition_print_(self): - return 'end' - - -class SaleMonthByShopReport(Report): - 'Sale Month By Shop' - __name__ = 'sale_goal.sale_month_shop_report' - - @classmethod - def get_context(cls, records, header, data): - report_context = super().get_context(records, header, data) - pool = Pool() - Company = pool.get('company.company') - Invoice = pool.get('account.invoice') - Period = pool.get('account.period') - Shop = pool.get('sale.shop') - company = Company(data['company']) - period = Period(data['period']) - - invoices = Invoice.search([ - ('company', '=', data['company']), - ('shop', '=', data['shop']), - ('invoice_date', '>=', period.start_date), - ('invoice_date', '<=', period.end_date), - ('type', '=', 'out'), - ('state', 'in', ['posted', 'paid', 'validated']), - ], order=[('invoice_date', 'ASC')]) - year = period.start_date.year - month = period.start_date.month - _, last_day = calendar.monthrange(year, period.start_date.month) - - mdays = {(nday + 1): { - 'date': date(year, month, nday + 1), - 'num_invoices': [], - 'untaxed_amount': [], - 'tax_amount': [], - 'total_amount': [], - 'cash': [], - 'credit': [] - } for nday in range(last_day)} - - sum_untaxed_amount = [] - sum_total_cash = [] - sum_total_credit = [] - sum_tax_amount = [] - sum_total_amount = [] - - def _is_credit(payment_term): - res = [] - for line in payment_term.lines: - res.append(sum([d.months + d.weeks + d.days - for d in line.relativedeltas])) - if sum(res) > 0: - return True - return False - - for invoice in invoices: - if _is_credit(invoice.payment_term): - mdays[invoice.invoice_date.day]['credit'].append(invoice.total_amount) - sum_total_credit.append(invoice.total_amount) - else: - mdays[invoice.invoice_date.day]['cash'].append(invoice.total_amount) - sum_total_cash.append(invoice.total_amount) - - mdays[invoice.invoice_date.day]['num_invoices'].append(1) - mdays[invoice.invoice_date.day]['untaxed_amount'].append(invoice.untaxed_amount) - mdays[invoice.invoice_date.day]['tax_amount'].append(invoice.tax_amount) - mdays[invoice.invoice_date.day]['total_amount'].append(invoice.total_amount) - - sum_untaxed_amount.append(invoice.untaxed_amount) - sum_tax_amount.append(invoice.tax_amount) - sum_total_amount.append(invoice.total_amount) - - report_context['records'] = mdays.values() - report_context['total_credit'] = sum(sum_total_credit) - report_context['total_cash'] = sum(sum_total_cash) - report_context['untaxed_amount'] = sum(sum_untaxed_amount) - report_context['tax_amount'] = sum(sum_tax_amount) - report_context['total_amount'] = sum(sum_total_amount) - report_context['shop'] = Shop(data['shop']).name - report_context['period'] = period.name - report_context['company'] = company.party.name - - return report_context diff --git a/sale.xml b/sale.xml index 28fc7ae..764e693 100644 --- a/sale.xml +++ b/sale.xml @@ -24,25 +24,5 @@ The COPYRIGHT file at the top level of this repository contains the full copyrig - - Report Sale Month By Shop - - sale_goal.sale_month_shop_report - sale_goal/sale_month_by_shop.ods - ods - False - - - sale_goal.sale_month_shop.start - form - sale_month_shop_start_form - - - Sale Month By Shop - sale_goal.sale_month_shop - - - diff --git a/sale_month_by_shop.ods b/sale_month_by_shop.ods deleted file mode 100644 index 019bd794a89a5a328616a03438b9428010d89067..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17525 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 t + + + + + + + + diff --git a/view/goal_annual_ranking_start_form.xml b/view/goal_annual_ranking_start_form.xml index 343be6a..05aff10 100644 --- a/view/goal_annual_ranking_start_form.xml +++ b/view/goal_annual_ranking_start_form.xml @@ -1,7 +1,7 @@ -

+