From a7684829121de0eccbc70fb659b21cd8f84de58c Mon Sep 17 00:00:00 2001 From: bashmak Date: Tue, 19 Jun 2018 19:29:01 +0300 Subject: [PATCH] little bit refactoring --- abonapp/models.py | 2 +- abonapp/templates/abonapp/peoples.html | 2 + abonapp/views.py | 17 ++---- agent/commands/dhcp.py | 2 +- devapp/base_intr.py | 29 +++++---- devapp/dev_types.py | 81 ++++++++------------------ devapp/models.py | 4 +- devapp/views.py | 2 +- djing/lib/__init__.py | 18 ++---- djing/lib/decorators.py | 10 ---- docs/dev.md | 19 ++---- ip_pool/templates/ip_pool/net_add.html | 2 + mapapp/urls.py | 4 +- mapapp/views.py | 34 +++++------ tariff_app/base_intr.py | 20 +++++-- tariff_app/custom_tariffs.py | 30 +++------- 16 files changed, 111 insertions(+), 165 deletions(-) diff --git a/abonapp/models.py b/abonapp/models.py index 1862b24..5a19756 100644 --- a/abonapp/models.py +++ b/abonapp/models.py @@ -162,7 +162,7 @@ class Abon(BaseAccount): Return icon list of set flags from self.markers :return: ['m-icon-donkey', 'm-icon-tv', ...] """ - return ["m-%s" % name for name, state in self.markers if state] + return tuple("m-%s" % name for name, state in self.markers if state) def is_markers_empty(self): return int(self.markers) == 0 diff --git a/abonapp/templates/abonapp/peoples.html b/abonapp/templates/abonapp/peoples.html index 1e923a7..354305f 100644 --- a/abonapp/templates/abonapp/peoples.html +++ b/abonapp/templates/abonapp/peoples.html @@ -10,6 +10,8 @@ {% endblock %} +{% block page-header %}{{ group.title }}{% endblock %} + {% block main %}
diff --git a/abonapp/views.py b/abonapp/views.py index 51a5b16..b4a9bf9 100644 --- a/abonapp/views.py +++ b/abonapp/views.py @@ -30,16 +30,11 @@ from guardian.shortcuts import get_objects_for_user, assign_perm from guardian.decorators import permission_required_or_403 as permission_required from djing import ping from djing import lib -from djing.global_base_views import OrderingMixin, BaseListWithFiltering, SecureApiView - - -class BaseAbonListView(OrderingMixin, BaseListWithFiltering): - paginate_by = getattr(settings, 'PAGINATION_ITEMS_PER_PAGE', 10) - http_method_names = ('get',) +from djing.global_base_views import BaseOrderedFilteringList, SecureApiView @method_decorator((login_required, lib.decorators.only_admins), name='dispatch') -class PeoplesListView(BaseAbonListView): +class PeoplesListView(BaseOrderedFilteringList): context_object_name = 'peoples' template_name = 'abonapp/peoples.html' @@ -84,7 +79,7 @@ class PeoplesListView(BaseAbonListView): @method_decorator((login_required, lib.decorators.only_admins), name='dispatch') -class GroupListView(BaseAbonListView): +class GroupListView(BaseOrderedFilteringList): context_object_name = 'groups' template_name = 'abonapp/group_list.html' queryset = Group.objects.annotate(usercount=Count('abon')) @@ -212,7 +207,7 @@ def abonamount(request, gid, uname): @method_decorator((login_required, lib.decorators.only_admins), name='dispatch') @method_decorator(permission_required('group_app.can_view_group', (Group, 'pk', 'gid')), name='dispatch') -class DebtsListView(BaseAbonListView): +class DebtsListView(BaseOrderedFilteringList): context_object_name = 'invoices' template_name = 'abonapp/invoiceForPayment.html' @@ -230,7 +225,7 @@ class DebtsListView(BaseAbonListView): @method_decorator((login_required, lib.decorators.only_admins), name='dispatch') @method_decorator(permission_required('group_app.can_view_group', (Group, 'pk', 'gid')), name='dispatch') -class PayHistoryListView(BaseAbonListView): +class PayHistoryListView(BaseOrderedFilteringList): context_object_name = 'pay_history' template_name = 'abonapp/payHistory.html' @@ -756,7 +751,7 @@ def abon_ping(request): @method_decorator((login_required, lib.decorators.only_admins,), name='dispatch') -class DialsListView(BaseAbonListView): +class DialsListView(BaseOrderedFilteringList): context_object_name = 'logs' template_name = 'abonapp/dial_log.html' diff --git a/agent/commands/dhcp.py b/agent/commands/dhcp.py index 92051a8..92fc7b5 100644 --- a/agent/commands/dhcp.py +++ b/agent/commands/dhcp.py @@ -9,7 +9,7 @@ def dhcp_commit(client_ip: str, client_mac: str, switch_mac: str, switch_port: i dev = Device.objects.get(mac_addr=switch_mac) mngr_class = dev.get_manager_klass() - if mngr_class.is_use_device_port(): + if mngr_class.get_is_use_device_port(): abon = Abon.objects.get(dev_port__device=dev, dev_port__num=switch_port, device=dev) diff --git a/devapp/base_intr.py b/devapp/base_intr.py index 3b63998..0345527 100644 --- a/devapp/base_intr.py +++ b/devapp/base_intr.py @@ -5,15 +5,13 @@ from easysnmp import Session from django.utils.translation import gettext -from djing.lib.decorators import abstract_static_method - ListOrError = Union[ Iterable, Union[Exception, Iterable] ] -class DeviceImplementationError(Exception): +class DeviceImplementationError(NotImplementedError): pass @@ -25,10 +23,15 @@ class DevBase(object, metaclass=ABCMeta): def __init__(self, dev_instance=None): self.db_instance = dev_instance - @abstract_static_method - def description() -> AnyStr: + @property + @abstractmethod + def description(self) -> AnyStr: pass + @classmethod + def get_description(cls): + return cls.description + @abstractmethod def reboot(self): pass @@ -49,23 +52,29 @@ class DevBase(object, metaclass=ABCMeta): def get_template_name(self) -> AnyStr: """Return path to html template for device""" + @property @abstractmethod def has_attachable_to_subscriber(self) -> bool: """Can connect device to subscriber""" - @abstract_static_method - def is_use_device_port() -> bool: + @property + @abstractmethod + def is_use_device_port(self) -> bool: """True if used device port while opt82 authorization""" - # fixme: only that is abstract static - @abstract_static_method - def validate_extra_snmp_info(v: str) -> None: + @classmethod + def get_is_use_device_port(cls) -> bool: + return cls.is_use_device_port + + @classmethod + def validate_extra_snmp_info(cls, v: str) -> None: """ Validate extra snmp field for each device. If validation failed then raise en exception from djing.lib.tln.ValidationError with description of error. :param v: String value for validate """ + raise NotImplementedError @abstractmethod def register_device(self, extra_data: Dict): diff --git a/devapp/dev_types.py b/devapp/dev_types.py index 7baf5e0..ee07a11 100644 --- a/devapp/dev_types.py +++ b/devapp/dev_types.py @@ -60,14 +60,14 @@ class DLinkPort(BasePort): class DLinkDevice(DevBase, SNMPBaseWorker): + has_attachable_to_subscriber = True + description = _('DLink switch') + is_use_device_port = True + def __init__(self, dev_instance): DevBase.__init__(self, dev_instance) SNMPBaseWorker.__init__(self, dev_instance.ip_address, dev_instance.man_passw, 2) - @staticmethod - def description(): - return _('DLink switch') - def reboot(self): return self.get_item('.1.3.6.1.4.1.2021.8.1.101.1') @@ -103,15 +103,8 @@ class DLinkDevice(DevBase, SNMPBaseWorker): def get_template_name(self): return 'ports.html' - def has_attachable_to_subscriber(self) -> bool: - return True - - @staticmethod - def is_use_device_port(): - return True - - @staticmethod - def validate_extra_snmp_info(v: str) -> None: + @classmethod + def validate_extra_snmp_info(cls, v: str) -> None: # Dlink has no require snmp info pass @@ -142,14 +135,14 @@ class ONUdev(BasePort): class OLTDevice(DevBase, SNMPBaseWorker): + has_attachable_to_subscriber = False + description = _('PON OLT') + is_use_device_port = False + def __init__(self, dev_instance): DevBase.__init__(self, dev_instance) SNMPBaseWorker.__init__(self, dev_instance.ip_address, dev_instance.man_passw, 2) - @staticmethod - def description(): - return gettext('PON OLT') - def reboot(self): pass @@ -187,15 +180,8 @@ class OLTDevice(DevBase, SNMPBaseWorker): def get_template_name(self): return 'olt.html' - def has_attachable_to_subscriber(self) -> bool: - return False - - @staticmethod - def is_use_device_port(): - return False - - @staticmethod - def validate_extra_snmp_info(v: str) -> None: + @classmethod + def validate_extra_snmp_info(cls, v: str) -> None: # Olt has no require snmp info pass @@ -208,6 +194,10 @@ class OLTDevice(DevBase, SNMPBaseWorker): class OnuDevice(DevBase, SNMPBaseWorker): + has_attachable_to_subscriber = True + description = _('PON ONU') + is_use_device_port = False + def __init__(self, dev_instance): DevBase.__init__(self, dev_instance) dev_ip_addr = None @@ -223,15 +213,11 @@ class OnuDevice(DevBase, SNMPBaseWorker): )) SNMPBaseWorker.__init__(self, dev_ip_addr, dev_instance.man_passw, 2) - @staticmethod - def description() -> AnyStr: - return gettext('PON ONU') - def reboot(self): pass def get_ports(self) -> ListOrError: - return [] + return () def get_device_name(self): pass @@ -242,13 +228,6 @@ class OnuDevice(DevBase, SNMPBaseWorker): def get_template_name(self): return "onu.html" - def has_attachable_to_subscriber(self) -> bool: - return True - - @staticmethod - def is_use_device_port(): - return False - def get_details(self): if self.db_instance is None: return @@ -274,7 +253,7 @@ class OnuDevice(DevBase, SNMPBaseWorker): return {'err': "%s: %s" % (_('ONU not connected'), e)} @staticmethod - def validate_extra_snmp_info(v: str) -> None: + def validate_extra_snmp_info(cls, v: str) -> None: # DBCOM Onu have en integer snmp port try: int(v) @@ -330,9 +309,8 @@ class EltexPort(BasePort): class EltexSwitch(DLinkDevice): - @staticmethod - def description(): - return _('Eltex switch') + description = _('Eltex switch') + is_use_device_port = False def get_ports(self) -> ListOrError: res = [] @@ -355,13 +333,6 @@ class EltexSwitch(DLinkDevice): tm = RuTimedelta(timedelta(seconds=uptimestamp / 100)) or RuTimedelta(timedelta()) return tm - def has_attachable_to_subscriber(self) -> bool: - return True - - @staticmethod - def is_use_device_port(): - return False - def monitoring_template(self, *args, **kwargs) -> Optional[str]: device = self.db_instance return plain_ip_device_mon_template(device) @@ -378,9 +349,7 @@ def conv_signal(lvl: int) -> float: class Olt_ZTE_C320(OLTDevice): - @staticmethod - def description(): - return gettext('OLT ZTE C320') + description = _('OLT ZTE C320') def get_fibers(self): fibers = ({ @@ -443,9 +412,7 @@ class Olt_ZTE_C320(OLTDevice): class ZteOnuDevice(OnuDevice): - @staticmethod - def description(): - return _('ZTE PON ONU') + description = _('ZTE PON ONU') def get_details(self) -> Optional[Dict]: if self.db_instance is None: @@ -472,8 +439,8 @@ class ZteOnuDevice(OnuDevice): def get_template_name(self): return 'onu_for_zte.html' - @staticmethod - def validate_extra_snmp_info(v: str) -> None: + @classmethod + def validate_extra_snmp_info(cls, v: str) -> None: # for example 268501760.5 try: fiber_num, onu_port = v.split('.') diff --git a/devapp/models.py b/devapp/models.py index 2b6575f..7933583 100644 --- a/devapp/models.py +++ b/devapp/models.py @@ -90,8 +90,8 @@ class Device(models.Model): # Can attach device to subscriber in subscriber page def has_attachable_to_subscriber(self) -> bool: - mngr = self.get_manager_object() - return mngr.has_attachable_to_subscriber() + mngr = self.get_manager_klass() + return mngr.has_attachable_to_subscriber def __str__(self): return "%s: (%s) %s %s" % (self.comment, self.get_devtype_display(), self.ip_address or '', self.mac_addr or '') diff --git a/devapp/views.py b/devapp/views.py index 73a5c87..f1adfec 100644 --- a/devapp/views.py +++ b/devapp/views.py @@ -650,7 +650,7 @@ class OnDeviceMonitoringEvent(global_base_views.SecureApiView): recipients = UserProfile.objects.get_profiles_by_group(device_down.group.pk) names = list() - for recipient in recipients: + for recipient in recipients.iterator(): send_notify( msg_text=gettext(notify_text) % { 'device_name': "%s(%s) %s" % ( diff --git a/djing/lib/__init__.py b/djing/lib/__init__.py index 0442c0a..199547e 100644 --- a/djing/lib/__init__.py +++ b/djing/lib/__init__.py @@ -38,24 +38,16 @@ def safe_int(i): # классы передавать для того чтоб по значению кода из базы понять какой класс нужно взять для нужной функциональности. # Например по коду в базе вам нужно определять как считать тариф абонента, что реализовано в возвращаемом классе. class MyChoicesAdapter(Iterator): - chs = tuple() - current_index = 0 - _max_index = 0 + _chs = None # На вход принимает кортеж кортежей, вложенный из 2х элементов: кода и класса, как: TARIFF_CHOICES def __init__(self, choices): - self._max_index = len(choices) - self.chs = choices + self._chs = iter(choices) def __next__(self): - if self.current_index >= self._max_index: - raise StopIteration - else: - e = self.chs - ci = self.current_index - res = e[ci][0], e[ci][1].description() - self.current_index += 1 - return res + obj = next(self._chs) + choice_code, choice_class = obj + return choice_code, choice_class.get_description() # Russian localized timedelta diff --git a/djing/lib/decorators.py b/djing/lib/decorators.py index 2c0d49d..2a69fed 100644 --- a/djing/lib/decorators.py +++ b/djing/lib/decorators.py @@ -55,13 +55,3 @@ def hash_auth_view(fn): else: return HttpResponseForbidden('Access Denied') return wrapped - - -class abstract_static_method(staticmethod): - __slots__ = () - - def __init__(self, func): - super(abstract_static_method, self).__init__(func) - func.__isabstractmethod__ = True - - __isabstractmethod__ = True diff --git a/docs/dev.md b/docs/dev.md index b9f728e..09ca0ec 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -47,10 +47,9 @@ class EltexPort(BasePort): Теперь реализация для свича: ```python class EltexSwitch(DLinkDevice): - - @staticmethod - def description(): - return _('Eltex switch') + has_attachable_to_subscriber = False + description = _('Eltex switch') + is_use_device_port = False def get_ports(self): #nams = self.get_list('.1.3.6.1.4.1.171.10.134.2.1.1.100.2.1.3') @@ -77,14 +76,8 @@ class EltexSwitch(DLinkDevice): tm = RuTimedelta(timedelta(seconds=uptimestamp/100)) or RuTimedelta(timedelta()) return tm - def has_attachable_to_subscriber(self) -> bool: - return False - - @staticmethod - def is_use_device_port(): - return False ``` -Метод **@description** Просто отображает человекопонятное название вашего устройства в биллинге. +Свойство **@description** Просто отображает человекопонятное название вашего устройства в биллинге. Заметьте что строка на английском и заключена в процедуру **_** (это ugettext_lazy, см. в импорте вверху файла), это локализация для текущего языка. Про локализацию можно почитать в соответствующем разделе [django translation](https://docs.djangoproject.com/en/1.11/topics/i18n/translation/). @@ -98,11 +91,11 @@ class EltexSwitch(DLinkDevice): Метод **@uptime**, понятно что возвращает, укажите нужный OID. Вернётся тип *RuTimedelta*, это переопределённый тип **timedelta**, я его реализовал для локализации временного промежутка на русский. -Метод **@has_attachable_to_subscriber** возвращает правду если это устройство можно привязать к абоненту. +Свойство **@has_attachable_to_subscriber** возвращает правду если это устройство можно привязать к абоненту. Например у Dlink стоит True потому что Dlink стоит во многих местах на доступе, и его порты принадлежат абонентам при авторизации. -Статический метод **@is_use_device_port** используется в DHCP чтоб понять что мы используем для привязки к абоненту всё устройство или +Свойство **@is_use_device_port** используется в DHCP чтоб понять что мы используем для привязки к абоненту всё устройство или только порт устройства. Например, если у устройства только 1 порт абонента (PON ONU), и мы привязываем этого абонента ко всему устройству а не к порту, то нужно вернуть False, На обычных свичах где мы авторизуем абонента на порту возвращаем True. diff --git a/ip_pool/templates/ip_pool/net_add.html b/ip_pool/templates/ip_pool/net_add.html index 3dbb6cd..6fb8450 100644 --- a/ip_pool/templates/ip_pool/net_add.html +++ b/ip_pool/templates/ip_pool/net_add.html @@ -11,6 +11,8 @@ {% endblock %} +{% block page-header %}{% trans 'Add new subnet' %}{% endblock %} + {% block main %}
{% csrf_token %}
diff --git a/mapapp/urls.py b/mapapp/urls.py index def6b23..e3673e3 100644 --- a/mapapp/urls.py +++ b/mapapp/urls.py @@ -7,8 +7,8 @@ app_name = 'mapapp' urlpatterns = [ url(r'^$', views.home, name='home'), url(r'^options$', views.OptionsListView.as_view(), name='options'), - url(r'^options/add$', views.dot, name='add_dot'), - url(r'^options/(?P\d+)/edit$', views.dot, name='edit_dot'), + url(r'^options/add$', views.dot_edit, name='add_dot'), + url(r'^options/(?P\d+)/edit$', views.dot_edit, name='edit_dot'), url(r'^options/(?P\d+)/remove$', views.remove, name='remove_dot'), url(r'^options/(?P\d+)/add_dev$', views.add_dev, name='add_dev'), url(r'^preload_devices$', views.preload_devices, name='preload_devices'), diff --git a/mapapp/views.py b/mapapp/views.py index 1b61c28..3989678 100644 --- a/mapapp/views.py +++ b/mapapp/views.py @@ -31,8 +31,8 @@ def home(request): dots = Dot.objects.all() groups = Group.objects.all() return render(request, 'maps/ya_index.html', { - 'dots': dots, - 'groups': groups + 'dots': dots.iterator(), + 'groups': groups.iterator() }) @@ -49,7 +49,7 @@ class OptionsListView(BaseListView): @login_required -def dot(request, did=0): +def dot_edit(request, did=0): if not request.user.is_superuser: return redirect('/') try: @@ -101,9 +101,9 @@ def remove(request, did): def get_dots(request): if not request.user.is_superuser: return HttpResponseForbidden('you have not super user') - dots = Dot.objects.prefetch_related('devices').annotate(devcount=Count('devices')).defer('attachment') + dots = Dot.objects.prefetch_related('devices').annotate(devcount=Count('devices')).defer('attachment').iterator() - def fill_dev(dev): + def fill_dev(dev: Device): return { 'status': dev.status, 'comment': dev.comment @@ -169,15 +169,15 @@ def modal_add_dot(request): def preload_devices(request): if not request.user.is_superuser: return HttpResponseForbidden('you have not super user') - grp = request.GET.get('grp') - dot = request.GET.get('dot') - all_devices = Device.objects.filter(group__id=grp) - dot_devices = Device.objects.filter(dot__id=dot) - - dot_devices_ids = [dev.pk for dev in dot_devices] + grp_id = request.GET.get('grp') + dot_id = request.GET.get('dot') + all_devices = Device.objects.filter(group__id=grp_id) + dot_devices = Device.objects.filter(dot__id=dot_id) + dot_devices_ids = tuple(dev.pk for dev in dot_devices.iterator()) + del dot_devices ret = render_to_text('maps/preload_devices_tmpl.html', { - 'all_devices': all_devices, + 'all_devices': all_devices.iterator(), 'dot_devices_ids': dot_devices_ids }) return HttpResponse(ret, content_type='text/html') @@ -195,7 +195,7 @@ def dot_tooltip(request): except Dot.DoesNotExist: pass return render_to_text('maps/map_tooltip.html', { - 'devs': devs, + 'devs': devs.iterator(), 'dot': dot }) @@ -213,8 +213,8 @@ def add_dev(request, did): selected_user_group = safe_int(request.POST.get('selected_user_group')) existing_devs = Device.objects.filter(group__id=selected_user_group or param_user_group) - if existing_devs.count() > 0: - dot.devices.remove(*[dev.pk for dev in existing_devs]) + if existing_devs.exists(): + dot.devices.remove(*(dev.pk for dev in existing_devs.iterator())) dot.devices.add(*selected_devs) url = resolve_url('mapapp:add_dev', did=dot.pk) @@ -222,9 +222,9 @@ def add_dev(request, did): else: existing_devs = Device.objects.filter(group=param_user_group) return render(request, 'maps/add_device.html', { - 'groups': groups, + 'groups': groups.iterator(), 'dot': dot, - 'existing_devs': existing_devs, + 'existing_devs': existing_devs.iterator(), 'grp': param_user_group, 'dot_devices_ids': [dev.pk for dev in Device.objects.filter(dot=dot)] }) diff --git a/tariff_app/base_intr.py b/tariff_app/base_intr.py index 970dbe9..db903ff 100644 --- a/tariff_app/base_intr.py +++ b/tariff_app/base_intr.py @@ -14,13 +14,17 @@ class TariffBase(metaclass=ABCMeta): """Calculate deadline date""" raise NotImplementedError - @staticmethod - def description() -> AnyStr: + @property + @abstractmethod + def description(self) -> AnyStr: """ Usage in djing.lib.MyChoicesAdapter for choices fields. :return: human readable description """ - raise NotImplementedError + + @classmethod + def get_description(cls): + return cls.description @staticmethod def manage_access(abon) -> bool: @@ -51,8 +55,12 @@ class PeriodicPayCalcBase(metaclass=ABCMeta): """ raise NotImplementedError - @staticmethod - def description() -> AnyStr: + @property + @abstractmethod + def description(self) -> AnyStr: """Return text description. Uses in djing.lib.MyChoicesAdapter for CHOICES fields""" - raise NotImplementedError + + @classmethod + def get_description(cls): + return cls.description diff --git a/tariff_app/custom_tariffs.py b/tariff_app/custom_tariffs.py index e9efd92..454260c 100644 --- a/tariff_app/custom_tariffs.py +++ b/tariff_app/custom_tariffs.py @@ -1,6 +1,5 @@ # -*- coding: utf-8 -*- from datetime import timedelta, datetime -from typing import AnyStr from django.utils import timezone from django.utils.translation import gettext as _ @@ -11,6 +10,8 @@ from random import uniform class TariffDefault(TariffBase): + description = _('Base calculate functionality') + def __init__(self, abon_tariff): # assert isinstance(abon_tariff, AbonTariff) self.abon_tariff = abon_tariff @@ -44,25 +45,20 @@ class TariffDefault(TariffBase): hour=23, minute=59, second=59) return last_month_date - @staticmethod - def description() -> AnyStr: - return _('Base calculate functionality') - class TariffDp(TariffDefault): + description = 'IS' # в IS снимается вся стоимость тарифа вне зависимости от времени использования # просто возвращаем всю стоимость тарифа def calc_amount(self) -> float: return float(self.abon_tariff.tariff.amount) - @staticmethod - def description() -> AnyStr: - return 'IS' - # Как в IS только не на время, а на 10 лет class TariffCp(TariffDp): + description = _('Private service') + def calc_deadline(self) -> datetime: # делаем время окончания услуги на 10 лет вперёд nw = timezone.now() @@ -70,10 +66,6 @@ class TariffCp(TariffDp): hour=23, minute=59, second=59) return long_long_time - @staticmethod - def description() -> AnyStr: - return _('Private service') - # Первый - всегда по умолчанию TARIFF_CHOICES = ( @@ -84,6 +76,8 @@ TARIFF_CHOICES = ( class PeriodicPayCalcDefault(PeriodicPayCalcBase): + description = _('Default periodic pay') + def calc_amount(self, model_object) -> float: return model_object.amount @@ -91,12 +85,10 @@ class PeriodicPayCalcDefault(PeriodicPayCalcBase): # TODO: решить какой будет расёт периодических платежей return datetime.now() + timedelta(days=30) - @staticmethod - def description() -> AnyStr: - return _('Default periodic pay') - class PeriodicPayCalcCustom(PeriodicPayCalcDefault): + description = _('Custom periodic pay') + def calc_amount(self, model_object) -> float: """ :param model_object: it is a instance of models.PeriodicPay model @@ -104,10 +96,6 @@ class PeriodicPayCalcCustom(PeriodicPayCalcDefault): """ return uniform(1, 10) - @staticmethod - def description() -> AnyStr: - return _('Custom periodic pay') - PERIODIC_PAY_CHOICES = ( ('df', PeriodicPayCalcDefault),