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/entrypoint.sh b/entrypoint.sh index 6361dd014..fc32baf24 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -6,19 +6,19 @@ set -e # If not will, the container will fail and restart. python3 manage.py shell -c "import django; django.db.connection.ensure_connection();" -# On PROD, we run migrations at startup unless explicitly disabled. -# If disabled, this command must be run manually for the application to function correctly after a model update. -if [ "$ENV" == "PROD" ] && [ "${DISABLE_MIGRATION_SCRIPT_ON_PRODUCTION}" != "true" ]; then - python3 manage.py migrate -fi - -# On PROD, we always collect statics if [ "$ENV" == "PROD" ]; then + # On PROD, we run migrations at startup unless explicitly disabled. + # If disabled, this command must be run manually for the application to function correctly after a model update. + if [ "${DISABLE_MIGRATION_SCRIPT_ON_PRODUCTION}" != "true" ]; then + python3 manage.py migrate + fi python3 manage.py collectstatic --no-input +elif [ "$ENV" == "DEV" ]; then + python3 manage.py migrate fi -python3 manage.py update_integrator_permissions python3 manage.py compilemessages -l fr +python3 manage.py update_integrator_permissions # Run the command exec $@ diff --git a/geocity/apps/accounts/admin.py b/geocity/apps/accounts/admin.py index af99ded5c..611bd9cac 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 ServiceFeeType 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: @@ -702,6 +699,7 @@ class Meta: "signature_sheet", "signature_sheet_description", "additional_searchtext_for_address_field", + "services_fees_hourly_rate", "geom", "integrator", ] @@ -760,9 +758,15 @@ class SubmissionWorkflowStatusInline(admin.TabularInline): verbose_name_plural = _("Flux (complet par défaut)") +class ServiceFeeInline(admin.TabularInline): + model = ServiceFeeType + extra = 3 + verbose_name = _("Type de prestation") + verbose_name_plural = _("Types de prestation") + + @admin.register(models.AdministrativeEntityForAdminSite) class AdministrativeEntityAdmin(IntegratorFilterMixin, admin.ModelAdmin): - fieldsets = ( ( None, @@ -822,7 +826,17 @@ class AdministrativeEntityAdmin(IntegratorFilterMixin, admin.ModelAdmin): ), }, ), + ( + _("Tarification des prestations"), + { + "fields": ("services_fees_hourly_rate",), + "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) @@ -837,6 +851,7 @@ def __new__(cls, *args, **kwargs): change_form_template = "accounts/admin/administrative_entity_change.html" form = AdministrativeEntityAdminForm inlines = [ + ServiceFeeInline, SubmissionWorkflowStatusInline, ] list_filter = [ @@ -854,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): @@ -882,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, @@ -890,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/0017_service_fee.py b/geocity/apps/accounts/migrations/0017_service_fee.py new file mode 100644 index 000000000..926a606e4 --- /dev/null +++ b/geocity/apps/accounts/migrations/0017_service_fee.py @@ -0,0 +1,24 @@ +# Generated by Django 4.2.9 on 2024-01-24 08:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("accounts", "0016_administrativeentity_reply_to_email"), + ] + + operations = [ + migrations.AddField( + model_name="administrativeentity", + name="services_fees_hourly_rate", + field=models.DecimalField( + decimal_places=2, + default=0.0, + help_text="Tarif horaire des prestations de l'entité administrative", + max_digits=12, + verbose_name="Tarif horaire", + ), + ), + ] diff --git a/geocity/apps/accounts/models.py b/geocity/apps/accounts/models.py index 6c1a15c61..1296960f5 100644 --- a/geocity/apps/accounts/models.py +++ b/geocity/apps/accounts/models.py @@ -334,7 +334,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", @@ -357,6 +356,14 @@ class AdministrativeEntity(models.Model): signature_sheet_description = models.TextField( _("Texte explicatif relatif au volet de transmission"), blank=True ) + services_fees_hourly_rate = models.DecimalField( + decimal_places=2, + max_digits=12, + default=settings.DEFAULT_SERVICES_FEES_RATE, + verbose_name=_("Tarif horaire"), + help_text=_("Tarif horaire des prestations de l'entité administrative"), + ) + objects = AdministrativeEntityManager() class Meta: @@ -611,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..500ffb5c6 100644 --- a/geocity/apps/accounts/permissions_groups.py +++ b/geocity/apps/accounts/permissions_groups.py @@ -20,6 +20,7 @@ "submissions": [ "submissionamendfield", "submissionworkflowstatus", + "servicefeetype", ], "reports": [ "report", @@ -28,6 +29,7 @@ "section", "headerfooter", ], + # FIXME: get nested submissions.payments } # define permissions required by integrator role @@ -62,6 +64,7 @@ "can_generate_pdf", "can_refund_transactions", "can_revert_refund_transactions", + "can_manage_service_fee", ] DEFAULT_PILOT_PERMISSION_CODENAMES = [ @@ -69,9 +72,11 @@ "amend_submission", "classify_submission", "can_generate_pdf", + "can_manage_service_fee", ] DEFAULT_VALIDATOR_PERMISSION_CODENAMES = [ "read_submission", "validate_submission", + "can_manage_service_fee", ] diff --git a/geocity/apps/accounts/users.py b/geocity/apps/accounts/users.py index e7cc8a111..0d4901bd7 100644 --- a/geocity/apps/accounts/users.py +++ b/geocity/apps/accounts/users.py @@ -114,4 +114,5 @@ 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 diff --git a/geocity/apps/api/serializers.py b/geocity/apps/api/serializers.py index a4e468c0e..86c8ac035 100644 --- a/geocity/apps/api/serializers.py +++ b/geocity/apps/api/serializers.py @@ -24,7 +24,7 @@ SubmissionGeoTime, SubmissionInquiry, ) -from geocity.apps.submissions.payments.models import SubmissionPrice +from geocity.apps.submissions.payments.models import ServiceFee, SubmissionPrice from geocity.apps.submissions.payments.postfinance.models import PostFinanceTransaction @@ -291,6 +291,23 @@ class Meta: ) +class SubmissionServiceFeeSerializer(serializers.ModelSerializer): + + permit_department = serializers.SlugRelatedField( + read_only=True, slug_field="shortname" + ) + service_fee_type = serializers.SlugRelatedField(read_only=True, slug_field="name") + + class Meta: + model = ServiceFee + fields = ( + "permit_department", + "service_fee_type", + "time_spent_on_task", + "monetary_amount", + ) + + class PostFinanceTransactionPrintSerializer(serializers.ModelSerializer): def to_representation(self, instance): repr = super(PostFinanceTransactionPrintSerializer, self).to_representation( @@ -338,6 +355,7 @@ class Meta: "forms_names", "current_inquiry", "price", + "service_fees_total_price", "sent_date", ) @@ -638,6 +656,7 @@ class SubmissionPrintSerializer(gis_serializers.GeoFeatureModelSerializer): read_only=True, source="author.userprofile", default=None ) validations = SubmissionValidationSerializer(source="*", read_only=True) + service_fee = SubmissionServiceFeeSerializer(many=True, read_only=True) def get_creditor_type(self, obj): if obj.creditor_type is not None: @@ -673,6 +692,7 @@ class Meta: "author", "geo_envelop", "validations", + "service_fee", ) @classmethod @@ -771,6 +791,7 @@ def get_agenda_form_fields(value, detailed, available_filters): obj = value.get_selected_forms().all() form_fields = obj.values( "submission__featured_agenda", + "submission__status_agenda", "field_values__field__name", "field_values__field__api_name", "field_values__value__val", @@ -869,6 +890,11 @@ def get_agenda_form_fields(value, detailed, available_filters): ] result["properties"]["featured"] = field["submission__featured_agenda"] + result["properties"]["status"] = ( + field["submission__status_agenda"] + if field["submission__status_agenda"] != "null" + else None + ) # Custom way to retrieve starts_at and ends_at for both light and detailed geo_time_qs = value.geo_time.all() diff --git a/geocity/apps/api/views.py b/geocity/apps/api/views.py index fe116ba35..806276563 100644 --- a/geocity/apps/api/views.py +++ b/geocity/apps/api/views.py @@ -249,7 +249,6 @@ def get_queryset(self, geom_type=None): ), to_attr="current_inquiries_filtered", ) - qs = ( Submission.objects.filter(base_filter) .filter( @@ -263,6 +262,9 @@ def get_queryset(self, geom_type=None): .prefetch_related(geotime_prefetch) .prefetch_related(current_inquiry_prefetch) .prefetch_related("selected_forms", "contacts") + .prefetch_related("service_fee") + .prefetch_related("service_fee__permit_department") + .prefetch_related("service_fee__service_fee_type") .select_related( "administrative_entity", "author", diff --git a/geocity/apps/core/static/css/main.css b/geocity/apps/core/static/css/main.css index 26438d6d5..39be27d6b 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..d8cece13a --- /dev/null +++ b/geocity/apps/core/static/js/submission_prestation.js @@ -0,0 +1,21 @@ +const updateFormMonetaryAmount = () => { + var serviceFeeType = document.getElementById("select2-id_service_fee_type-container"); + let monetaryAmount = document.getElementById("id_monetary_amount"); + let jsonstr = JSON.parse(document.getElementById("get-data").textContent); + if (!jsonstr) { + return; + } + let json = JSON.parse(jsonstr); + if (monetaryAmount) { + json.forEach((item) => { + if (item.name === serviceFeeType.title) { + monetaryAmount.value = item.fix_price + if (item.fix_price_editable ) { + monetaryAmount.readOnly = false; + } else { + monetaryAmount.readOnly = true; + } + } + }); + } +}; 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 %}