Removed prices app

This commit is contained in:
Marc 2014-09-08 14:23:06 +00:00
parent fc44c8bfc0
commit 157fd54ce5
17 changed files with 337 additions and 137 deletions

View File

@ -78,9 +78,6 @@ at + clock time, midnight, noon- At 3:30 p.m., At 4:01, At noon
* make account_link to autoreplace account on change view.
* LAST version of this shit http://wkhtmltopdf.org/downloads.html
* Rename pack to plan ? one can have multiple plans?
* transaction.process FK?
* translations
from django.utils import translation

View File

@ -50,10 +50,9 @@ def get_account_items():
if isinstalled('orchestra.apps.users'):
url = reverse('admin:users_user_changelist')
childrens.append(items.MenuItem(_("Users"), url))
if isinstalled('orchestra.apps.prices'):
url = reverse('admin:prices_pack_changelist')
childrens.append(items.MenuItem(_("Packs"), url))
if isinstalled('orchestra.apps.orders'):
url = reverse('admin:orders_plan_changelist')
childrens.append(items.MenuItem(_("Plans"), url))
url = reverse('admin:orders_order_changelist')
childrens.append(items.MenuItem(_("Orders"), url))
if isinstalled('orchestra.apps.bills'):

View File

@ -15,7 +15,17 @@ from orchestra.utils.humanize import naturaldate
from .actions import BillSelectedOrders
from .filters import ActiveOrderListFilter, BilledOrderListFilter
from .models import Service, Order, MetricStorage
from .models import Plan, Rate, Service, Order, MetricStorage
class PlanAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('name', 'account_link')
list_filter = ('name',)
class RateInline(admin.TabularInline):
model = Rate
ordering = ('plan', 'quantity')
class ServiceAdmin(admin.ModelAdmin):
@ -38,9 +48,10 @@ class ServiceAdmin(admin.ModelAdmin):
'classes': ('wide',),
'fields': ('metric', 'pricing_period', 'rate_algorithm',
'orders_effect', 'on_cancel', 'payment_style',
'trial_period', 'refound_period', 'tax')
'trial_period', 'refound_period', 'tax', 'nominal_price')
}),
)
inlines = [RateInline]
def formfield_for_dbfield(self, db_field, **kwargs):
""" Improve performance of account field and filter by account """
@ -117,6 +128,7 @@ class MetricStorageAdmin(admin.ModelAdmin):
list_filter = ('order__service',)
admin.site.register(Plan, PlanAdmin)
admin.site.register(Service, ServiceAdmin)
admin.site.register(Order, OrderAdmin)
admin.site.register(MetricStorage, MetricStorageAdmin)

View File

@ -134,21 +134,21 @@ class ServiceHandler(plugins.Plugin):
def get_price_with_orders(self, order, size, ini, end):
porders = self.orders.filter(account=order.account).filter(
Q(cancelled_on__isnull=True) | Q(cancelled_on__gt=ini)
).filter(registered_on__lt=end)
).filter(registered_on__lt=end).order_by('registered_on')
price = 0
if self.orders_effect == self.REGISTER_OR_RENEW:
events = get_register_or_renew_events(porders, ini, end)
events = get_register_or_renew_events(porders, order, ini, end)
elif self.orders_effect == self.CONCURRENT:
events = get_register_or_cancel_events(porders, ini, end)
events = get_register_or_cancel_events(porders, order, ini, end)
else:
raise NotImplementedError
for metric, ratio in events:
price += self.get_rate(order, metric) * size * ratio
for metric, position, ratio in events:
price += self.get_price(order, metric, position=position) * size * ratio
return price
def get_price_with_metric(self, order, size, ini, end):
metric = order.get_metric(ini, end)
price = self.get_rate(order, metric) * size
price = self.get_price(order, metric) * size
return price
def create_line(self, order, price, size, ini, end):

View File

@ -36,45 +36,54 @@ def get_related_objects(origin, max_depth=2):
new_models.append(related)
queue.append(new_models)
def get_register_or_cancel_events(porders, ini, end):
def get_register_or_cancel_events(porders, order, ini, end):
assert ini <= end, "ini > end"
CANCEL = 'cancel'
REGISTER = 'register'
changes = {}
counter = 0
for order in porders:
if order.cancelled_on:
cancel = order.cancelled_on
if order.billed_until and order.cancelled_on < order.billed_until:
cancel = order.billed_until
for num, porder in enumerate(porders.order_by('registered_on')):
if porder == order:
position = num
if porder.cancelled_on:
cancel = porder.cancelled_on
if porder.billed_until and porder.cancelled_on < porder.billed_until:
cancel = porder.billed_until
if cancel > ini and cancel < end:
changes.setdefault(cancel, [])
changes[cancel].append(CANCEL)
if order.registered_on <= ini:
changes[cancel].append((CANCEL, num))
if porder.registered_on <= ini:
counter += 1
elif order.registered_on < end:
changes.setdefault(order.registered_on, [])
changes[order.registered_on].append(REGISTER)
elif porder.registered_on < end:
changes.setdefault(porder.registered_on, [])
changes[porder.registered_on].append((REGISTER, num))
pointer = ini
total = float((end-ini).days)
for date in sorted(changes.keys()):
yield counter, (date-pointer).days/total
for change in changes[date]:
yield counter, position, (date-pointer).days/total
for change, num in changes[date]:
if change is CANCEL:
counter -= 1
if num < position:
position -= 1
else:
counter += 1
pointer = date
yield counter, (end-pointer).days/total
yield counter, position, (end-pointer).days/total
def get_register_or_renew_events(handler, porders, ini, end):
def get_register_or_renew_events(handler, porders, order, ini, end):
total = float((end-ini).days)
for sini, send in handler.get_pricing_slots(ini, end):
counter = 0
for order in porders:
if order.registered_on >= sini and order.registered_on < send:
position = 0
for porder in porders.order_by('registered_on'):
if porder == order:
position = abs(position)
elif position < 0:
position -= 1
if porder.registered_on >= sini and porder.registered_on < send:
counter += 1
elif order.billed_until > send or order.cancelled_on > send:
elif porder.billed_until > send or porder.cancelled_on > send:
counter += 1
yield counter, (send-sini)/total
yield counter, position, (send-sini)/total

View File

@ -0,0 +1,85 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '__first__'),
('contenttypes', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='MetricStorage',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('value', models.BigIntegerField(verbose_name='value')),
('created_on', models.DateField(auto_now_add=True, verbose_name='created on')),
('updated_on', models.DateField(auto_now=True, verbose_name='updated on')),
],
options={
'get_latest_by': 'created_on',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('object_id', models.PositiveIntegerField(null=True)),
('registered_on', models.DateField(auto_now_add=True, verbose_name='registered on')),
('cancelled_on', models.DateField(null=True, verbose_name='cancelled on', blank=True)),
('billed_on', models.DateField(null=True, verbose_name='billed on', blank=True)),
('billed_until', models.DateField(null=True, verbose_name='billed until', blank=True)),
('ignore', models.BooleanField(default=False, verbose_name='ignore')),
('description', models.TextField(verbose_name='description', blank=True)),
('account', models.ForeignKey(related_name=b'orders', verbose_name='account', to='accounts.Account')),
('content_type', models.ForeignKey(to='contenttypes.ContentType')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Service',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('description', models.CharField(unique=True, max_length=256, verbose_name='description')),
('match', models.CharField(max_length=256, verbose_name='match', blank=True)),
('handler_type', models.CharField(blank=True, help_text='Handler used for processing this Service. A handler enables customized behaviour far beyond what options here allow to.', max_length=256, verbose_name='handler', choices=[(b'', 'Default')])),
('is_active', models.BooleanField(default=True, verbose_name='is active')),
('billing_period', models.CharField(default=b'ANUAL', choices=[(b'', 'One time service'), (b'MONTHLY', 'Monthly billing'), (b'ANUAL', 'Anual billing')], max_length=16, blank=True, help_text='Renewal period for recurring invoicing', verbose_name='billing period')),
('billing_point', models.CharField(default=b'ON_FIXED_DATE', help_text='Reference point for calculating the renewal date on recurring invoices', max_length=16, verbose_name='billing point', choices=[(b'ON_REGISTER', 'Registration date'), (b'ON_FIXED_DATE', 'Fixed billing date')])),
('delayed_billing', models.CharField(default=b'ONE_MONTH', choices=[(b'', 'No delay (inmediate billing)'), (b'TEN_DAYS', 'Ten days'), (b'ONE_MONTH', 'One month')], max_length=16, blank=True, help_text='Period in which this service will be ignored for billing', verbose_name='delayed billing')),
('is_fee', models.BooleanField(default=False, help_text='Designates whether this service should be billed as membership fee or not', verbose_name='is fee')),
('metric', models.CharField(help_text='Metric used to compute the pricing rate. Number of orders is used when left blank.', max_length=256, verbose_name='metric', blank=True)),
('tax', models.PositiveIntegerField(default=0, verbose_name='tax', choices=[(0, 'Duty free'), (7, '7%'), (21, '21%')])),
('pricing_period', models.CharField(default=b'BILLING_PERIOD', help_text='Period used for calculating the metric used on the pricing rate', max_length=16, verbose_name='pricing period', choices=[(b'BILLING_PERIOD', 'Same as billing period'), (b'MONTHLY', 'Monthly data'), (b'ANUAL', 'Anual data')])),
('rate_algorithm', models.CharField(default=b'BEST_PRICE', help_text='Algorithm used to interprete the rating table', max_length=16, verbose_name='rate algorithm', choices=[(b'BEST_PRICE', 'Best progressive price'), (b'PROGRESSIVE_PRICE', 'Conservative progressive price'), (b'MATCH_PRICE', 'Match price')])),
('orders_effect', models.CharField(default=b'CONCURRENT', help_text='Defines the lookup behaviour when using orders for the pricing rate computation of this service.', max_length=16, verbose_name='orders effect', choices=[(b'REGISTER_OR_RENEW', 'Register or renew events'), (b'CONCURRENT', 'Active at every given time')])),
('on_cancel', models.CharField(default=b'DISCOUNT', help_text='Defines the cancellation behaviour of this service', max_length=16, verbose_name='on cancel', choices=[(b'NOTHING', 'Nothing'), (b'DISCOUNT', 'Discount'), (b'COMPENSATE', 'Discount and compensate'), (b'REFOUND', 'Discount, compensate and refound')])),
('payment_style', models.CharField(default=b'PREPAY', help_text='Designates whether this service should be paid after consumtion (postpay/on demand) or prepaid', max_length=16, verbose_name='payment style', choices=[(b'PREPAY', 'Prepay'), (b'POSTPAY', 'Postpay (on demand)')])),
('trial_period', models.CharField(default=b'', choices=[(b'', 'No trial'), (b'TEN_DAYS', 'Ten days'), (b'ONE_MONTH', 'One month')], max_length=16, blank=True, help_text='Period in which no charge will be issued', verbose_name='trial period')),
('refound_period', models.CharField(default=b'', choices=[(b'', 'Never refound'), (b'TEN_DAYS', 'Ten days'), (b'ONE_MONTH', 'One month'), (b'ALWAYS', 'Always refound')], max_length=16, blank=True, help_text='Period in which automatic refound will be performed on service cancellation', verbose_name='refound period')),
('content_type', models.ForeignKey(verbose_name='content type', to='contenttypes.ContentType')),
],
options={
},
bases=(models.Model,),
),
migrations.AddField(
model_name='order',
name='service',
field=models.ForeignKey(related_name=b'orders', verbose_name='service', to='orders.Service'),
preserve_default=True,
),
migrations.AddField(
model_name='metricstorage',
name='order',
field=models.ForeignKey(verbose_name='order', to='orders.Order'),
preserve_default=True,
),
]

View File

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('orders', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='service',
name='nominal_price',
field=models.DecimalField(default=0.0, verbose_name='nominal price', max_digits=12, decimal_places=2),
preserve_default=False,
),
]

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('accounts', '__first__'),
('orders', '0002_service_nominal_price'),
]
operations = [
migrations.CreateModel(
name='Plan',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(default=b'basic', max_length=128, verbose_name='plan', choices=[(b'basic', 'Basic'), (b'advanced', 'Advanced')])),
('account', models.ForeignKey(related_name=b'plans', verbose_name='account', to='accounts.Account')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Rate',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('plan', models.CharField(blank=True, max_length=128, verbose_name='plan', choices=[(b'', 'default'), (b'basic', 'Basic'), (b'advanced', 'Advanced')])),
('quantity', models.PositiveIntegerField(null=True, verbose_name='quantity', blank=True)),
('value', models.DecimalField(verbose_name='value', max_digits=12, decimal_places=2)),
('service', models.ForeignKey(related_name=b'rates', verbose_name='service', to='orders.Service')),
],
options={
},
bases=(models.Model,),
),
migrations.AlterUniqueTogether(
name='rate',
unique_together=set([('service', 'plan', 'quantity')]),
),
]

View File

@ -15,10 +15,49 @@ from orchestra.models import queryset
from orchestra.utils.apps import autodiscover
from orchestra.utils.python import import_class
from . import settings, helpers
from . import helpers, settings, pricing
from .handlers import ServiceHandler
class Plan(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='plans')
name = models.CharField(_("plan"), max_length=128,
choices=settings.ORDERS_PLANS,
default=settings.ORDERS_DEFAULT_PLAN)
def __unicode__(self):
return self.name
class RateQuerySet(models.QuerySet):
group_by = queryset.group_by
def by_account(self, account):
# Default allways selected
qset = Q(plan__isnull=True)
for plan in account.plans.all():
qset |= Q(plan=plan)
return self.filter(qset)
class Rate(models.Model):
service = models.ForeignKey('orders.Service', verbose_name=_("service"),
related_name='rates')
plan = models.CharField(_("plan"), max_length=128, blank=True,
choices=(('', _("Default")),) + settings.ORDERS_PLANS)
quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True)
value = models.DecimalField(_("value"), max_digits=12, decimal_places=2)
objects = RateQuerySet.as_manager()
class Meta:
unique_together = ('service', 'plan', 'quantity')
def __unicode__(self):
return "{}-{}".format(str(self.value), self.quantity)
autodiscover('handlers')
@ -43,6 +82,10 @@ class Service(models.Model):
BEST_PRICE = 'BEST_PRICE'
PROGRESSIVE_PRICE = 'PROGRESSIVE_PRICE'
MATCH_PRICE = 'MATCH_PRICE'
PRICING_METHODS = {
BEST_PRICE: pricing.best_price,
MATCH_PRICE: pricing.match_price,
}
description = models.CharField(_("description"), max_length=256, unique=True)
content_type = models.ForeignKey(ContentType, verbose_name=_("content type"))
@ -85,6 +128,8 @@ class Service(models.Model):
metric = models.CharField(_("metric"), max_length=256, blank=True,
help_text=_("Metric used to compute the pricing rate. "
"Number of orders is used when left blank."))
nominal_price = models.DecimalField(_("nominal price"), max_digits=12,
decimal_places=2)
tax = models.PositiveIntegerField(_("tax"), choices=settings.ORDERS_SERVICE_TAXES,
default=settings.ORDERS_SERVICE_DEFAUL_TAX)
pricing_period = models.CharField(_("pricing period"), max_length=16,
@ -99,8 +144,8 @@ class Service(models.Model):
rate_algorithm = models.CharField(_("rate algorithm"), max_length=16,
help_text=_("Algorithm used to interprete the rating table"),
choices=(
(BEST_PRICE, _("Best price")),
(PROGRESSIVE_PRICE, _("Progressive price")),
(BEST_PRICE, _("Best progressive price")),
(PROGRESSIVE_PRICE, _("Conservative progressive price")),
(MATCH_PRICE, _("Match price")),
),
default=BEST_PRICE)
@ -121,22 +166,6 @@ class Service(models.Model):
(REFOUND, _("Discount, compensate and refound")),
),
default=DISCOUNT)
# TODO remove, orders are not disabled (they are cancelled user.is_active)
# on_disable = models.CharField(_("on disable"), max_length=16,
# help_text=_("Defines the behaviour of this service when disabled"),
# choices=(
# (NOTHING, _("Nothing")),
# (DISCOUNT, _("Discount")),
# (REFOUND, _("Refound")),
# ),
# default=DISCOUNT)
# on_register = models.CharField(_("on register"), max_length=16,
# help_text=_("Defines the behaviour of this service on registration"),
# choices=(
# (NOTHING, _("Nothing")),
# (DISCOUNT, _("Discount (fixed BP)")),
# ),
# default=DISCOUNT)
payment_style = models.CharField(_("payment style"), max_length=16,
help_text=_("Designates whether this service should be paid after "
"consumtion (postpay/on demand) or prepaid"),
@ -164,11 +193,6 @@ class Service(models.Model):
),
default=NEVER, blank=True)
@property
def nominal_price(self):
# FIXME delete and make it a model field
return 10
def __unicode__(self):
return self.description
@ -226,9 +250,36 @@ class Service(models.Model):
return self.billing_period
return self.pricing_period
def get_rate(self, order, metric):
# TODO implement
return 12
def get_price(self, order, metric, position=None):
"""
if position is provided an specific price for that position is returned,
accumulated price is returned otherwise
"""
rates = self.rates.by_account(order.account)
if not rates:
return self.nominal_price
rates = self.rate_method(rates, metric)
counter = 0
if position is None:
ant_counter = 0
accumulated = 0
for rate in self.get_rates(order.account, metric):
counter += rate['number']
if counter >= metric:
counter = metric
accumulated += (counter - ant_counter) * rate['price']
return accumulated
ant_counter = counter
accumulated += rate['price'] * rate['number']
else:
for rate in self.get_rates(order.account, metric):
counter += rate['number']
if counter >= position:
return rate['price']
@property
def rate_method(self, *args, **kwargs):
return self.RATE_METHODS[self.rate_algorithm]
class OrderQuerySet(models.QuerySet):
@ -323,8 +374,7 @@ class Order(models.Model):
self.save()
def get_metric(self, ini, end):
# TODO implement
return 10
return MetricStorage.get(self, ini, end)
class MetricStorage(models.Model):
@ -353,8 +403,11 @@ class MetricStorage(models.Model):
@classmethod
def get(cls, order, ini, end):
# TODO
pass
try:
return cls.objects.filter(order=order, updated_on__lt=end,
updated_on__gte=ini).latest('updated_on').value
except cls.DoesNotExist:
return 0
@receiver(pre_delete, dispatch_uid="orders.cancel_orders")
@ -379,3 +432,5 @@ def update_orders(sender, **kwargs):
accounts.register(Order)
accounts.register(Plan)
services.register(Plan, menu=False)

View File

@ -0,0 +1,42 @@
import sys
def best_price(rates, metric):
rates = rates.order_by('metric').order_by('plan')
ix = 0
steps = []
num = rates.count()
while ix < num:
if ix+1 == num or rates[ix].plan != rates[ix+1].plan:
number = metric
else:
number = rates[ix+1].metric - rates[ix].metric
steps.append({
'number': sys.maxint,
'price': rates[ix].price
})
ix += 1
steps.sort(key=lambda s: s['price'])
acumulated = 0
for step in steps:
previous = acumulated
acumulated += step['number']
if acumulated >= metric:
step['number'] = metric - previous
yield step
raise StopIteration
yield step
def match_price(rates, metric):
minimal = None
for plan, rates in rates.order_by('-metric').group_by('plan'):
if minimal is None:
minimal = rates[0].price
else:
minimal = min(minimal, rates[0].price)
return [{
'number': sys.maxint,
'price': minimal
}]

View File

@ -16,3 +16,11 @@ ORDERS_SERVICE_ANUAL_BILLING_MONTH = getattr(settings, 'ORDERS_SERVICE_ANUAL_BIL
ORDERS_BILLING_BACKEND = getattr(settings, 'ORDERS_BILLING_BACKEND',
'orchestra.apps.orders.billing.BillsBackend')
ORDERS_PLANS = getattr(settings, 'ORDERS_PLANS', (
('basic', _("Basic")),
('advanced', _("Advanced")),
))
ORDERS_DEFAULT_PLAN = getattr(settings, 'ORDERS_DEFAULT_PLAN', 'basic')

View File

@ -1,23 +0,0 @@
from django.contrib import admin
from orchestra.admin.utils import insertattr
from orchestra.apps.accounts.admin import AccountAdminMixin
from orchestra.apps.orders.models import Service
from .models import Pack, Rate
class PackAdmin(AccountAdminMixin, admin.ModelAdmin):
list_display = ('name', 'account_link')
list_filter = ('name',)
admin.site.register(Pack, PackAdmin)
class RateInline(admin.TabularInline):
model = Rate
ordering = ('pack', 'quantity')
insertattr(Service, 'inlines', RateInline)

View File

@ -1,36 +0,0 @@
from django.db import models
from django.contrib.contenttypes.models import ContentType
from django.utils.translation import ugettext_lazy as _
from orchestra.core import accounts, services
from . import settings
class Pack(models.Model):
account = models.ForeignKey('accounts.Account', verbose_name=_("account"),
related_name='packs')
name = models.CharField(_("pack"), max_length=128,
choices=settings.PRICES_PACKS,
default=settings.PRICES_DEFAULT_PACK)
def __unicode__(self):
return self.name
class Rate(models.Model):
service = models.ForeignKey('orders.Service', verbose_name=_("service"))
pack = models.CharField(_("pack"), max_length=128, blank=True,
choices=(('', _("default")),) + settings.PRICES_PACKS)
quantity = models.PositiveIntegerField(_("quantity"), null=True, blank=True)
value = models.DecimalField(_("value"), max_digits=12, decimal_places=2)
class Meta:
unique_together = ('service', 'pack', 'quantity')
def __unicode__(self):
return "{}-{}".format(str(self.value), self.quantity)
accounts.register(Pack)
services.register(Pack, menu=False)

View File

@ -1,10 +0,0 @@
from django.conf import settings
from django.utils.translation import ugettext_lazy as _
PRICES_PACKS = getattr(settings, 'PRICES_PACKS', (
('basic', _("Basic")),
('advanced', _("Advanced")),
))
PRICES_DEFAULT_PACK = getattr(settings, 'PRICES_DEFAULT_PACK', 'basic')

View File

@ -139,7 +139,7 @@ function install_requirements () {
django-extensions==1.1.1 \
django-transaction-signals==1.0.0 \
django-celery==3.1.10 \
celery==3.1.7 \
celery==3.1.13 \
kombu==3.0.8 \
Markdown==2.4 \
django-debug-toolbar==1.2.1 \

View File

@ -79,7 +79,6 @@ INSTALLED_APPS = (
'orchestra.apps.databases',
'orchestra.apps.vps',
'orchestra.apps.issues',
'orchestra.apps.prices',
'orchestra.apps.orders',
'orchestra.apps.miscellaneous',
'orchestra.apps.bills',
@ -145,7 +144,7 @@ FLUENT_DASHBOARD_APP_GROUPS = (
'orchestra.apps.contacts.models.Contact',
'orchestra.apps.users.models.User',
'orchestra.apps.orders.models.Order',
'orchestra.apps.prices.models.Pack',
'orchestra.apps.orders.models.Pack',
'orchestra.apps.bills.models.Bill',
# 'orchestra.apps.payments.models.PaymentSource',
'orchestra.apps.payments.models.Transaction',
@ -187,7 +186,7 @@ FLUENT_DASHBOARD_APP_ICONS = {
'contacts/contact': 'contact_book.png',
'orders/order': 'basket.png',
'orders/service': 'price.png',
'prices/pack': 'Pack.png',
'orders/plan': 'Pack.png',
'bills/bill': 'invoice.png',
'payments/paymentsource': 'card_in_use.png',
'payments/transaction': 'transaction.png',