diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 0e73509b3..ee5b74ea1 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -5,6 +5,9 @@ volumes: services: web: + # Uncomment those two lines for debug + #tty: true + #stdin_open: true build: args: ENV: DEV @@ -19,13 +22,20 @@ services: - ./_dev_volumes/private_documents:/private_documents - ./_dev_volumes/archive:/archive depends_on: - - "postgres" + postgres: + condition: service_healthy ports: - "${DJANGO_DOCKER_PORT}:9000" postgres: image: postgis/postgis:13-3.2 restart: unless-stopped + healthcheck: + test: [ "CMD", "pg_isready", "-q", "-U", "${PGUSER}" ] + interval: 5s + timeout: 10s + retries: 3 + start_period: 10s environment: - POSTGRES_USER=${PGUSER} - POSTGRES_PASSWORD=${PGPASSWORD} diff --git a/geocity/admin_jazzmin_settings.py b/geocity/admin_jazzmin_settings.py index 2ef7a55b1..159990fb7 100644 --- a/geocity/admin_jazzmin_settings.py +++ b/geocity/admin_jazzmin_settings.py @@ -104,6 +104,7 @@ "reports.ComplementaryDocumentTypeForAdminSite": "fas fa-copy", "submissions.Submission": "fas fa-search", "submissions.SubmissionAmendField": "fas fa-list-alt", + "submissions.ServicesFeesType": "fas fa-file-invoice-dollar", "submissions.SubmissionInquiry": "fas fa-calendar", "taggit.Tag": "fas fa-bookmark", }, diff --git a/geocity/apps/accounts/admin.py b/geocity/apps/accounts/admin.py index f5ca4495f..d0dd2317e 100644 --- a/geocity/apps/accounts/admin.py +++ b/geocity/apps/accounts/admin.py @@ -17,6 +17,7 @@ from geocity.apps.accounts.models import AdministrativeEntity, UserProfile from geocity.apps.reports.models import Report from geocity.apps.submissions.models import Submission, SubmissionWorkflowStatus +from geocity.apps.submissions.payments.models import ServicesFeesType from geocity.fields import GeometryWidget from . import models, permissions_groups @@ -67,6 +68,7 @@ """ + # Allow a user belonging to integrator group to see only objects created by this group def filter_for_user(user, qs): if not user.is_superuser: @@ -453,7 +455,6 @@ class Media: css = {"all": ("css/admin/admin.css",)} def clean_permissions(self): - permissions = self.cleaned_data["permissions"] permissions_for_trusted_users = Permission.objects.filter( codename__in=permissions_groups.AVAILABLE_FOR_INTEGRATOR_PERMISSION_CODENAMES @@ -603,7 +604,6 @@ def get__sites_number(self, obj): get__sites_number.short_description = _("Nombre de sites") def get_queryset(self, request): - if request.user.is_superuser: qs = Group.objects.all() else: @@ -627,14 +627,12 @@ def save_model(self, request, obj, form, change): def formfield_for_manytomany(self, db_field, request, **kwargs): # permissions that integrator role can grant to group if db_field.name == "permissions": - if ( not request.user.is_superuser and request.user.groups.get( permit_department__is_integrator_admin=True ).pk ): - integrator_permissions = Permission.objects.filter( codename__in=permissions_groups.AVAILABLE_FOR_INTEGRATOR_PERMISSION_CODENAMES ) @@ -649,7 +647,6 @@ def label_from_instance(self, obj): def get_sites_field(user): - qs = models.Site.objects.all() if not user.is_superuser: @@ -701,6 +698,7 @@ class Meta: "signature_sheet", "signature_sheet_description", "additional_searchtext_for_address_field", + "services_fees_hourly_rate", "geom", "integrator", ] @@ -759,9 +757,15 @@ class SubmissionWorkflowStatusInline(admin.TabularInline): verbose_name_plural = _("Flux (complet par défaut)") +class ServicesFeesInline(IntegratorFilterMixin, admin.TabularInline): + model = ServicesFeesType + extra = 1 + verbose_name = _("Type de prestation") + verbose_name_plural = _("Types de prestation") + + @admin.register(models.AdministrativeEntityForAdminSite) class AdministrativeEntityAdmin(IntegratorFilterMixin, admin.ModelAdmin): - fieldsets = ( ( None, @@ -820,7 +824,20 @@ class AdministrativeEntityAdmin(IntegratorFilterMixin, admin.ModelAdmin): ), }, ), + ( + _("Tarification des prestations"), + { + "fields": ( + "services_fees_hourly_rate", + "min_cfc2_price", + ), + "description": _( + "La tarification des prestations permet de saisir le tarif horaire de facturation des prestations pour l'entité administrative courante." + ), + }, + ), ) + # Pass the user from ModelAdmin to ModelForm def get_form(self, request, obj=None, **kwargs): Form = super().get_form(request, obj, **kwargs) @@ -835,6 +852,7 @@ def __new__(cls, *args, **kwargs): change_form_template = "accounts/admin/administrative_entity_change.html" form = AdministrativeEntityAdminForm inlines = [ + # ServicesFeesInline, # not working SubmissionWorkflowStatusInline, ] list_filter = [ @@ -851,6 +869,7 @@ def __new__(cls, *args, **kwargs): "get_tags", "get_is_single_form", "get_sites", + "services_fees_hourly_rate", ] def sortable_str(self, obj): @@ -879,7 +898,6 @@ def get_is_single_form(self, obj): get_is_single_form.admin_order_field = "is_single_form_submissions" def formfield_for_foreignkey(self, db_field, request, **kwargs): - if db_field.name == "integrator": kwargs["queryset"] = Group.objects.filter( permit_department__is_integrator_admin=True, @@ -887,7 +905,6 @@ def formfield_for_foreignkey(self, db_field, request, **kwargs): return super().formfield_for_foreignkey(db_field, request, **kwargs) def save_model(self, request, obj, form, change): - if not request.user.is_superuser: obj.integrator = request.user.groups.get( permit_department__is_integrator_admin=True diff --git a/geocity/apps/accounts/migrations/0016_administrativeentity_min_cfc2_price_and_more.py b/geocity/apps/accounts/migrations/0016_administrativeentity_min_cfc2_price_and_more.py new file mode 100644 index 000000000..243e6e01b --- /dev/null +++ b/geocity/apps/accounts/migrations/0016_administrativeentity_min_cfc2_price_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.2.8 on 2024-01-17 09:13 + +from decimal import Decimal + +import djmoney.models.fields +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0015_alter_siteprofile_integrator"), + ] + + operations = [ + migrations.AddField( + model_name="administrativeentity", + name="min_cfc2_price", + field=djmoney.models.fields.MoneyField( + decimal_places=2, + default=Decimal("4000000.0"), + help_text="Montant CFC 2 minimal pour les demandes faites à l'entité administrative courante", + max_digits=12, + verbose_name="Montant CFC 2 minimal", + ), + ), + migrations.AddField( + model_name="administrativeentity", + name="min_cfc2_price_currency", + field=djmoney.models.fields.CurrencyField( + choices=[("CHF", "CHF .-"), ("EUR", "EUR €"), ("USD", "USD $")], + default="CHF", + editable=False, + max_length=3, + ), + ), + migrations.AddField( + model_name="administrativeentity", + name="services_fees_hourly_rate", + field=djmoney.models.fields.MoneyField( + decimal_places=2, + default=Decimal("0.0"), + help_text="Tarif horaire des prestations de l'entité administrative", + max_digits=12, + verbose_name="Tarif horaire", + ), + ), + migrations.AddField( + model_name="administrativeentity", + name="services_fees_hourly_rate_currency", + field=djmoney.models.fields.CurrencyField( + choices=[("CHF", "CHF .-"), ("EUR", "EUR €"), ("USD", "USD $")], + default="CHF", + editable=False, + max_length=3, + ), + ), + ] diff --git a/geocity/apps/accounts/models.py b/geocity/apps/accounts/models.py index c3c51fa44..8c2eea702 100644 --- a/geocity/apps/accounts/models.py +++ b/geocity/apps/accounts/models.py @@ -15,6 +15,7 @@ from django.dispatch import receiver from django.utils.functional import cached_property from django.utils.translation import gettext_lazy as _ +from djmoney.models.fields import MoneyField from simple_history.models import HistoricalRecords from taggit.managers import TaggableManager @@ -322,7 +323,6 @@ class AdministrativeEntity(models.Model): default=True, help_text=_("Nécessaire pour l'utilisation du système de paiement en ligne"), ) - sites = models.ManyToManyField( Site, related_name="administrative_entity", @@ -345,6 +345,25 @@ class AdministrativeEntity(models.Model): signature_sheet_description = models.TextField( _("Texte explicatif relatif au volet de transmission"), blank=True ) + min_cfc2_price = MoneyField( + decimal_places=2, + max_digits=12, + default_currency="CHF", + default=4e6, + verbose_name=_("Montant CFC 2 minimal"), + help_text=_( + "Montant CFC 2 minimal pour les demandes faites à l'entité administrative courante" + ), + ) + services_fees_hourly_rate = MoneyField( + decimal_places=2, + max_digits=12, + default_currency="CHF", + default=settings.DEFAULT_SERVICES_FEES_RATE, + verbose_name=_("Tarif horaire"), + help_text=_("Tarif horaire des prestations de l'entité administrative"), + ) + objects = AdministrativeEntityManager() class Meta: @@ -599,7 +618,6 @@ class Meta: verbose_name_plural = _("3.2 Consultation des auteurs") def __str__(self): - return ( str(self.user.first_name) + " " + str(self.user.last_name) if self.user diff --git a/geocity/apps/accounts/permissions_groups.py b/geocity/apps/accounts/permissions_groups.py index f62c1c4cf..8015f5376 100644 --- a/geocity/apps/accounts/permissions_groups.py +++ b/geocity/apps/accounts/permissions_groups.py @@ -20,6 +20,7 @@ "submissions": [ "submissionamendfield", "submissionworkflowstatus", + "servicesfeestype", ], "reports": [ "report", @@ -28,6 +29,7 @@ "section", "headerfooter", ], + # FIXME: get nested submissions.payments } # define permissions required by integrator role @@ -62,6 +64,10 @@ "can_generate_pdf", "can_refund_transactions", "can_revert_refund_transactions", + "can_manage_service_fee", + # "create_service_fee", + # "update_service_fee", + # "delete_service_fee", ] DEFAULT_PILOT_PERMISSION_CODENAMES = [ @@ -69,9 +75,17 @@ "amend_submission", "classify_submission", "can_generate_pdf", + "can_manage_service_fee", + # "create_service_fee", + # "update_service_fee", + # "delete_service_fee", ] DEFAULT_VALIDATOR_PERMISSION_CODENAMES = [ "read_submission", "validate_submission", + "can_manage_service_fee", + # "create_service_fee", + # "update_service_fee", + # "delete_service_fee", ] diff --git a/geocity/apps/accounts/users.py b/geocity/apps/accounts/users.py index e7cc8a111..7f1d7f36e 100644 --- a/geocity/apps/accounts/users.py +++ b/geocity/apps/accounts/users.py @@ -114,4 +114,37 @@ def get_users_list_for_integrator_admin(user, remove_anonymous=False): ): anonymous_users.append(user.pk) qs = qs.exclude(pk__in=anonymous_users) + return qs + + +def is_backoffice_in_department(user, department): + """ + Check if user is backoffice for a given department (group) + a.k.a. administrative entity. + """ + current_user_groups_pk = user.groups.all().values_list("pk", flat=True) + # Find the group of this user and filter by is_backoffice + departments_of_the_current_user = models.PermitDepartment.objects.filter( + administrative_entity=department, + is_backoffice=True, + pk__in=current_user_groups_pk, + ) + + return any(current_user_groups_pk.intersection(departments_of_the_current_user)) + + +def is_validator_in_department(user, department): + """ + Check if user is validator for a given department (group) + a.k.a. administrative entity. + """ + current_user_groups_pk = user.groups.all().values_list("pk", flat=True) + # Find the group of this user and filter by is_validator + departments_of_the_current_user = models.PermitDepartment.objects.filter( + administrative_entity=department, + is_validator=True, + pk__in=current_user_groups_pk, + ) + + return any(current_user_groups_pk.intersection(departments_of_the_current_user)) diff --git a/geocity/apps/core/static/css/main.css b/geocity/apps/core/static/css/main.css index 1a52d05d9..c1a6b586e 100644 --- a/geocity/apps/core/static/css/main.css +++ b/geocity/apps/core/static/css/main.css @@ -159,6 +159,10 @@ input:focus { vertical-align: baseline; } +.table tfoot { + font-weight: bold; +} + .page-item.active .page-link { background-color: var(--primary-color); border-color: var(--primary-color); @@ -407,6 +411,10 @@ h5 { color: crimson; } +.fa-trash-white { + color: #ffffff; +} + .table-container { overflow-x: visible; } diff --git a/geocity/apps/core/static/js/messages_timer.js b/geocity/apps/core/static/js/messages_timer.js new file mode 100644 index 000000000..0c3d8f95d --- /dev/null +++ b/geocity/apps/core/static/js/messages_timer.js @@ -0,0 +1,8 @@ +// Get all messages of the alert-success class +let info_messages = document.getElementsByClassName('alert-success'); + +setTimeout(function(){ + for (let i = 0; i < info_messages.length; i ++) { + info_messages[i].setAttribute('style', 'display:none'); + } +}, 4000); diff --git a/geocity/apps/core/static/js/submission_prestation.js b/geocity/apps/core/static/js/submission_prestation.js new file mode 100644 index 000000000..5f66bc0ba --- /dev/null +++ b/geocity/apps/core/static/js/submission_prestation.js @@ -0,0 +1,11 @@ +const updateFormMonetaryAmount = () => { + var serviceFeeType = document.getElementById("select2-id_services_fees_type-container"); + let monetaryAmount = document.getElementById("id_monetary_amount_0"); + let jsonstr = JSON.parse(document.getElementById("get-data").textContent); + let json = JSON.parse(jsonstr); + json.forEach((item) => { + if (item.name === serviceFeeType.title) { + monetaryAmount.value = item.fix_price + } + }); +}; diff --git a/geocity/apps/core/templates/base_generic.html b/geocity/apps/core/templates/base_generic.html index 8aab353a1..e5b2887b9 100644 --- a/geocity/apps/core/templates/base_generic.html +++ b/geocity/apps/core/templates/base_generic.html @@ -16,9 +16,10 @@ - + + {% endblock %} {% block stylesheets %} @@ -32,7 +33,7 @@ --text-color: {{ config.TEXT_COLOR }}; --title-color: {{ config.TITLE_COLOR }}; --table-color: {{ config.TABLE_COLOR }}; - --login-background-color:{{ config.LOGIN_BACKGROUND_COLOR }}; + --login-background-color: {{ config.LOGIN_BACKGROUND_COLOR }}; } @@ -44,7 +45,7 @@ {% block navbar %}