Compare commits

..

28 Commits

Author SHA1 Message Date
7535f008b7 #86 Logs
* adds log detail support for compensation state and action
2022-02-04 16:56:08 +01:00
e5153ddf77 Merge pull request '# 108' (#109) from 108_Deleted_compensation_checked into master
Reviewed-on: SGD-Nord/konova#109
2022-02-04 16:00:20 +01:00
ee959809ad # 108
* fixes bug
2022-02-04 15:59:53 +01:00
b453256916 Merge pull request '86_User_suggestions_and_feedback' (#106) from 86_User_suggestions_and_feedback into master
Reviewed-on: SGD-Nord/konova#106
2022-02-04 14:47:23 +01:00
49aba49406 Revert accidental docker->master merge 2022-02-04 14:24:31 +01:00
746daf14f6 Merge branch 'Docker' into master 2022-02-04 14:07:56 +01:00
bd92b8e895 Docker update 2022-02-04 14:00:18 +01:00
37ed627025 Merge branch 'master' into Docker
# Conflicts:
#	konova/settings.py
#	konova/sub_settings/django_settings.py
2022-02-04 13:59:30 +01:00
9c9c9b7717 # HOTFIX
* fixes bug where errors occured once an email shall be sent. See issue #87 for details
2022-01-28 17:01:45 +01:00
5b7351e331 Merge branch 'master' into Docker
# Conflicts:
#	konova/sub_settings/django_settings.py
2022-01-28 16:41:21 +01:00
60fb22b6c7 Merge pull request 'master' (#85) from master into Docker
Reviewed-on: SGD-Nord/konova#85
2022-01-21 09:27:46 +01:00
cf984e4abe Merge pull request 'master' (#82) from master into Docker
Reviewed-on: SGD-Nord/konova#82
2022-01-20 12:32:56 +01:00
14c42217c2 Merge pull request '# Bugfix parcel updating' (#79) from master into Docker
Reviewed-on: SGD-Nord/konova#79
2022-01-19 17:20:53 +01:00
b41b3af690 Merge pull request '# Hotfix' (#78) from master into Docker
Reviewed-on: SGD-Nord/konova#78
2022-01-19 14:15:35 +01:00
d982c5c835 Merge pull request 'master' (#77) from master into Docker
Reviewed-on: SGD-Nord/konova#77
2022-01-19 13:21:31 +01:00
9ea47fcc83 # Docker readme
* fixes typo
* adds more info
2022-01-18 13:55:32 +01:00
44033c18fe # Docker
* optimizes configuration
* removes unused settings
* extends README.md with docker installation guid
2022-01-17 12:45:03 +01:00
4027629996 Merge branch 'master' into Docker 2022-01-14 14:19:34 +01:00
5dae6a7b91 # Docker 2022-01-13 17:22:29 +01:00
850a3914fd # Docker 2022-01-13 15:42:07 +01:00
a2d5c4ddaa Merge remote-tracking branch 'origin/Docker' into Docker
# Conflicts:
#	docker-compose.yml
2022-01-13 14:07:54 +01:00
2118b42ecb # Docker persistent storage
* adds named volume to docker-compose.yml to keep files uploaded on /konova_uploaded_files even on image rebuild
2022-01-13 14:07:18 +01:00
0c2d39ba96 # Docker
* minor adjustments
2022-01-13 09:29:38 +01:00
9cb9308f85 # Docker Production
* adds further settings to create a production-ready docker configuration
2022-01-13 09:29:38 +01:00
d9577fe6a1 # Docker
* adds docker related configurations
* directly working configuration provided
2022-01-13 09:29:38 +01:00
2fdf6606b3 # Docker
* minor adjustments
2022-01-11 08:32:04 +01:00
7075d7074c # Docker Production
* adds further settings to create a production-ready docker configuration
2022-01-10 15:27:19 +01:00
c9b54cd4c5 # Docker
* adds docker related configurations
* directly working configuration provided
2022-01-10 13:52:40 +01:00
14 changed files with 92 additions and 40 deletions

View File

@ -12,6 +12,7 @@ from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.managers import CompensationActionManager from compensation.managers import CompensationActionManager
from konova.models import BaseResource from konova.models import BaseResource
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED
class UnitChoices(models.TextChoices): class UnitChoices(models.TextChoices):
@ -75,3 +76,13 @@ class CompensationAction(BaseResource):
if choice[0] == self.unit: if choice[0] == self.unit:
return choice[1] return choice[1]
return None return None
def delete(self, user=None, *args, **kwargs):
from compensation.models import Compensation
if user:
comps = Compensation.objects.filter(
actions__id__in=[self.id]
).distinct()
for comp in comps:
comp.mark_as_edited(user, edit_comment=COMPENSATION_ACTION_REMOVED)
super().delete(*args, **kwargs)

View File

@ -21,7 +21,7 @@ from konova.models import BaseObject, AbstractDocument, Deadline, generate_docum
GeoReferencedMixin GeoReferencedMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \ from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_EDITED_TEMPLATE
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -61,7 +61,6 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
user = form.user user = form.user
with transaction.atomic(): with transaction.atomic():
created_action = UserActionLogEntry.get_created_action(user) created_action = UserActionLogEntry.get_created_action(user)
edited_action = UserActionLogEntry.get_edited_action(user, _("Added deadline"))
deadline = Deadline.objects.create( deadline = Deadline.objects.create(
type=form_data["type"], type=form_data["type"],
@ -70,9 +69,7 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
created=created_action, created=created_action,
) )
self.modified = edited_action
self.save() self.save()
self.log.add(edited_action)
self.deadlines.add(deadline) self.deadlines.add(deadline)
return deadline return deadline
@ -332,7 +329,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
Returns: Returns:
""" """
return self.intervention.mark_as_edited(user, request, edit_comment, reset_recorded) self.intervention.unrecord(user, request)
action = super().mark_as_edited(user, edit_comment)
return action
def is_ready_for_publish(self) -> bool: def is_ready_for_publish(self) -> bool:
""" Not inherited by RecordableObjectMixin """ Not inherited by RecordableObjectMixin

View File

@ -273,5 +273,5 @@ class EcoAccountDeduction(BaseResource):
def delete(self, user=None, *args, **kwargs): def delete(self, user=None, *args, **kwargs):
if user is not None: if user is not None:
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED, reset_recorded=False) self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
super().delete(*args, **kwargs) super().delete(*args, **kwargs)

View File

@ -6,11 +6,13 @@ Created on: 16.11.21
""" """
from django.db import models from django.db import models
from django.db.models import Q
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
from compensation.managers import CompensationStateManager from compensation.managers import CompensationStateManager
from konova.models import UuidModel from konova.models import UuidModel
from konova.utils.message_templates import COMPENSATION_STATE_REMOVED
class CompensationState(UuidModel): class CompensationState(UuidModel):
@ -45,3 +47,14 @@ class CompensationState(UuidModel):
def __str__(self): def __str__(self):
return f"{self.biotope_type} | {self.surface}" return f"{self.biotope_type} | {self.surface}"
def delete(self, user=None, *args, **kwargs):
from compensation.models import Compensation
if user:
comps = Compensation.objects.filter(
Q(before_states__id__in=[self.id]) |
Q(after_states__id__in=[self.id])
).distinct()
for comp in comps:
comp.mark_as_edited(user, edit_comment=COMPENSATION_STATE_REMOVED)
super().delete(*args, **kwargs)

View File

@ -18,7 +18,8 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION, \ from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION, \
CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -347,7 +348,7 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=comp, request=request) form = NewStateModalForm(request.POST or None, instance=comp, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State added"), msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -369,7 +370,7 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=comp, request=request) form = NewActionModalForm(request.POST or None, instance=comp, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action added"), msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -437,7 +438,7 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
form = RemoveModalForm(request.POST or None, instance=state, request=request) form = RemoveModalForm(request.POST or None, instance=state, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State removed"), msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )
@ -460,7 +461,7 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
form = RemoveModalForm(request.POST or None, instance=action, request=request) form = RemoveModalForm(request.POST or None, instance=action, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action removed"), msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data"
) )

View File

@ -30,7 +30,8 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \ from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -359,7 +360,7 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=acc, request=request) form = NewStateModalForm(request.POST or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State added"), msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -381,7 +382,7 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=acc, request=request) form = NewActionModalForm(request.POST or None, instance=acc, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action added"), msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -404,7 +405,7 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
form = RemoveModalForm(request.POST or None, instance=state, request=request) form = RemoveModalForm(request.POST or None, instance=state, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State removed"), msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )
@ -427,7 +428,7 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
form = RemoveModalForm(request.POST or None, instance=action, request=request) form = RemoveModalForm(request.POST or None, instance=action, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action removed"), msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data"
) )

View File

@ -21,7 +21,8 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \ from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
DOCUMENT_ADDED DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \
COMPENSATION_ACTION_ADDED
from konova.utils.user_checks import in_group from konova.utils.user_checks import in_group
@ -292,7 +293,7 @@ def state_new_view(request: HttpRequest, id: str):
form = NewStateModalForm(request.POST or None, instance=ema, request=request) form = NewStateModalForm(request.POST or None, instance=ema, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State added"), msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -314,7 +315,7 @@ def action_new_view(request: HttpRequest, id: str):
form = NewActionModalForm(request.POST or None, instance=ema, request=request) form = NewActionModalForm(request.POST or None, instance=ema, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action added"), msg_success=COMPENSATION_ACTION_ADDED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -428,7 +429,7 @@ def state_remove_view(request: HttpRequest, id: str, state_id: str):
form = RemoveModalForm(request.POST or None, instance=state, request=request) form = RemoveModalForm(request.POST or None, instance=state, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("State removed"), msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@ -451,7 +452,7 @@ def action_remove_view(request: HttpRequest, id: str, action_id: str):
form = RemoveModalForm(request.POST or None, instance=action, request=request) form = RemoveModalForm(request.POST or None, instance=action, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Action removed"), msg_success=COMPENSATION_ACTION_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )

View File

@ -386,7 +386,7 @@ class NewDeductionModalForm(BaseModalForm):
def save(self): def save(self):
deduction = self.__create_deduction() deduction = self.__create_deduction()
self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED, reset_recorded=False) self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED)
return deduction return deduction

View File

@ -242,7 +242,9 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
Returns: Returns:
""" """
action = super().mark_as_edited(performing_user, request, edit_comment, reset_recorded) action = super().mark_as_edited(performing_user, edit_comment)
if reset_recorded:
self.unrecord(performing_user, request)
if self.checked: if self.checked:
self.set_unchecked() self.set_unchecked()
return action return action

View File

@ -89,7 +89,10 @@ class InterventionQualityChecker(AbstractQualityChecker):
Returns: Returns:
""" """
c_comps = self.obj.compensations.count() comps = self.obj.compensations.filter(
deleted=None
)
c_comps = comps.count()
c_pays = self.obj.payments.count() c_pays = self.obj.payments.count()
c_deducs = self.obj.deductions.count() c_deducs = self.obj.deductions.count()
c_all = c_comps + c_pays + c_deducs c_all = c_comps + c_pays + c_deducs

View File

@ -515,7 +515,9 @@ class RecordModalForm(BaseModalForm):
Returns: Returns:
""" """
comps = self.instance.compensations.all() comps = self.instance.compensations.filter(
deleted=None,
)
comps_valid = True comps_valid = True
for comp in comps: for comp in comps:
checker = comp.quality_check() checker = comp.quality_check()

View File

@ -132,6 +132,23 @@ class BaseObject(BaseResource):
self.save() self.save()
def mark_as_edited(self, performing_user: User, edit_comment: str = None):
""" In case the object or a related object changed the log history needs to be updated
Args:
performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action
edit_comment (str): Additional comment for the log entry
Returns:
"""
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment)
self.modified = edit_action
self.log.add(edit_action)
self.save()
return edit_action
def add_log_entry(self, action: UserAction, user: User, comment: str): def add_log_entry(self, action: UserAction, user: User, comment: str):
""" Wraps adding of UserActionLogEntry to log """ Wraps adding of UserActionLogEntry to log
@ -262,25 +279,18 @@ class RecordableObjectMixin(models.Model):
return action return action
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True): def unrecord(self, performing_user: User, request: HttpRequest = None):
""" In case the object or a related object changed, internal processes need to be started, such as """ Unrecords a dataset
unrecord and uncheck
Args: Args:
performing_user (User): The user which performed the editing action performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action request (HttpRequest): The used request for this action
edit_comment (str): Additional comment for the log entry
reset_recorded (bool): Whether the record-state of the object should be reset
Returns: Returns:
""" """
edit_action = UserActionLogEntry.get_edited_action(performing_user, edit_comment) action = None
self.modified = edit_action if self.recorded:
self.log.add(edit_action)
self.save()
if self.recorded and reset_recorded:
action = self.set_unrecorded(performing_user) action = self.set_unrecorded(performing_user)
self.log.add(action) self.log.add(action)
if request: if request:
@ -288,7 +298,7 @@ class RecordableObjectMixin(models.Model):
request, request,
CHECKED_RECORDED_RESET CHECKED_RECORDED_RESET
) )
return edit_action return action
@abstractmethod @abstractmethod
def is_ready_for_publish(self) -> bool: def is_ready_for_publish(self) -> bool:

View File

@ -24,6 +24,17 @@ CANCEL_ACC_RECORDED_OR_DEDUCTED = _("Action canceled. Eco account is recorded or
# COMPENSATION # COMPENSATION
COMPENSATION_ADDED_TEMPLATE = _("Compensation {} added") COMPENSATION_ADDED_TEMPLATE = _("Compensation {} added")
COMPENSATION_REMOVED_TEMPLATE = _("Compensation {} removed") COMPENSATION_REMOVED_TEMPLATE = _("Compensation {} removed")
COMPENSATION_EDITED_TEMPLATE = _("Compensation {} edited")
ADDED_COMPENSATION_ACTION = _("Added compensation action")
ADDED_COMPENSATION_STATE = _("Added compensation state")
# COMPENSATION STATE
COMPENSATION_STATE_REMOVED = _("State removed")
COMPENSATION_STATE_ADDED = _("State added")
# COMPENSATION ACTION
COMPENSATION_ACTION_ADDED = _("Action added")
COMPENSATION_ACTION_REMOVED = _("Action removed")
# DEDUCTIONS # DEDUCTIONS
DEDUCTION_ADDED = _("Deduction added") DEDUCTION_ADDED = _("Deduction added")
@ -43,9 +54,7 @@ DOCUMENT_ADDED = _("Document added")
# Edited # Edited
EDITED_GENERAL_DATA = _("Edited general data") EDITED_GENERAL_DATA = _("Edited general data")
ADDED_COMPENSATION_STATE = _("Added compensation state")
ADDED_DEADLINE = _("Added deadline") ADDED_DEADLINE = _("Added deadline")
ADDED_COMPENSATION_ACTION = _("Added compensation action")
# Geometry conflicts # Geometry conflicts
GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}") GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}")

View File

@ -47,4 +47,4 @@ wcwidth==0.2.5
webservices==0.7 webservices==0.7
wrapt==1.13.3 wrapt==1.13.3
xmltodict==0.12.0 xmltodict==0.12.0
zipp==3.4.1 zipp==3.4.1