From 9c32349e3761b0f23e539b7decbb034fb5cb229e Mon Sep 17 00:00:00 2001 From: SebastienReuiller Date: Thu, 5 Dec 2024 10:52:57 +0100 Subject: [PATCH] feat(Siae): Permettre aux structures de changer leur nom commercial (#1552) --- .../commands/sync_with_emplois_inclusion.py | 59 ++- ...r_historicalsiae_brand_alter_siae_brand.py | 22 + lemarche/siaes/models.py | 17 +- lemarche/siaes/tests/test_commands.py | 298 +++++++++++++- .../templates/dashboard/siae_edit_info.html | 388 +++++++++--------- lemarche/templates/siaes/_card_detail.html | 8 +- lemarche/templates/siaes/_useful_infos.html | 87 ++-- lemarche/utils/apis/api_emplois_inclusion.py | 1 + lemarche/www/dashboard_siaes/forms.py | 2 + lemarche/www/dashboard_siaes/tests.py | 51 +++ 10 files changed, 687 insertions(+), 246 deletions(-) create mode 100644 lemarche/siaes/migrations/0078_alter_historicalsiae_brand_alter_siae_brand.py diff --git a/lemarche/siaes/management/commands/sync_with_emplois_inclusion.py b/lemarche/siaes/management/commands/sync_with_emplois_inclusion.py index 930f1fa41..8cdd26ca4 100644 --- a/lemarche/siaes/management/commands/sync_with_emplois_inclusion.py +++ b/lemarche/siaes/management/commands/sync_with_emplois_inclusion.py @@ -1,9 +1,11 @@ +import logging import os import re from django.conf import settings from django.contrib.gis.geos import GEOSGeometry from django.core.management.base import CommandError +from django.db.models import Q from django.utils import timezone from stdnum.fr import siret @@ -15,10 +17,13 @@ from lemarche.utils.data import rename_dict_key +logger = logging.getLogger(__name__) + + UPDATE_FIELDS = [ # "name", # what happens to the slug if the name is updated? - "brand", - # "kind" + # "brand", # see UPDATE_FIELDS_IF_EMPTY + "kind", "siret", "siret_is_valid", "naf", @@ -39,6 +44,8 @@ "c1_last_sync_date", ] +UPDATE_FIELDS_IF_EMPTY = ["brand"] + C1_EXTRA_KEYS = ["convention_is_active", "convention_asp_id"] @@ -206,9 +213,13 @@ def filter_c1_export(self, c1_list): c1_list_filtered = [] for c1_siae in c1_list: - if c1_siae["kind"] not in ("RESERVED",): - c1_list_filtered.append(c1_siae) - + if c1_siae["kind"] not in ("RESERVED",): # do nothing if kind is filtered as reserved + if c1_siae["kind"] in siae_constants.KIND_INSERTION_LIST + siae_constants.KIND_HANDICAP_LIST: + c1_list_filtered.append(c1_siae) + else: + logger.error( + f"Kind not supported: {c1_siae['kind']}/{c1_siae['id']}/{c1_siae['name']}/{c1_siae['siret']}" + ) return c1_list_filtered def c4_update(self, c1_list, dry_run): @@ -243,11 +254,21 @@ def c4_create_siae(self, c1_siae, dry_run): c1_siae["contact_email"] = c1_siae["admin_email"] or c1_siae["email"] c1_siae["contact_phone"] = c1_siae["phone"] - # create object + # create object if brand is empty or not already used if not dry_run: - siae = Siae.objects.create(**c1_siae) - self.add_siae_to_contact_list(siae) - self.stdout_info(f"New Siae created / {siae.id} / {siae.name} / {siae.siret}") + if ( + "brand" not in c1_siae + or c1_siae["brand"] == "" + or not Siae.objects.filter(Q(name=c1_siae["brand"]) | Q(brand=c1_siae["brand"])).exists() + ): + siae = Siae.objects.create(**c1_siae) + + self.add_siae_to_contact_list(siae) + self.stdout_info(f"New Siae created / {siae.id} / {siae.name} / {siae.siret}") + else: + logger.error( + f"Brand name is already used by another SIAE: '{c1_siae['brand']}' / name: '{c1_siae['name']}'" + ) def add_siae_to_contact_list(self, siae: Siae): if siae.kind != "OPCS" and siae.is_active: @@ -272,5 +293,23 @@ def c4_update_siae(self, c1_siae, c4_siae, dry_run): if key in c1_siae: c1_siae_filtered[key] = c1_siae[key] - Siae.objects.filter(c1_id=c4_siae.c1_id).update(**c1_siae_filtered) # avoid updated_at change + # update fields only if empty + for key in UPDATE_FIELDS_IF_EMPTY: + if key in c1_siae and not getattr(c4_siae, key, None): + c1_siae_filtered[key] = c1_siae[key] + + # update siae only if brand is empty or not already used + if ( + "brand" not in c1_siae_filtered + or c1_siae_filtered["brand"] == "" + or not Siae.objects.exclude(c1_id=c4_siae.c1_id) + .filter(Q(name=c1_siae_filtered["brand"]) | Q(brand=c1_siae_filtered["brand"])) + .exists() + ): + Siae.objects.filter(c1_id=c4_siae.c1_id).update(**c1_siae_filtered) # avoid updated_at change + else: + logger.error( + f"Brand name is already used by another SIAE: '{c1_siae['brand']}' / name: '{c1_siae['name']}'" + ) + # self.stdout_info(f"Siae updated / {c4_siae.id} / {c4_siae.siret}") diff --git a/lemarche/siaes/migrations/0078_alter_historicalsiae_brand_alter_siae_brand.py b/lemarche/siaes/migrations/0078_alter_historicalsiae_brand_alter_siae_brand.py new file mode 100644 index 000000000..7fac2e2c1 --- /dev/null +++ b/lemarche/siaes/migrations/0078_alter_historicalsiae_brand_alter_siae_brand.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.15 on 2024-11-27 16:41 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("siaes", "0077_remove_siaeactivity_location_siaeactivity_locations_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="historicalsiae", + name="brand", + field=models.CharField(blank=True, max_length=255, verbose_name="Nom commercial"), + ), + migrations.AlterField( + model_name="siae", + name="brand", + field=models.CharField(blank=True, max_length=255, verbose_name="Nom commercial"), + ), + ] diff --git a/lemarche/siaes/models.py b/lemarche/siaes/models.py index 73673de74..646fdcbd0 100644 --- a/lemarche/siaes/models.py +++ b/lemarche/siaes/models.py @@ -6,6 +6,7 @@ from django.contrib.gis.db.models.functions import Distance from django.contrib.gis.measure import D from django.contrib.postgres.search import TrigramSimilarity # SearchVector +from django.core.exceptions import ValidationError from django.db import IntegrityError, models, transaction from django.db.models import ( BooleanField, @@ -536,7 +537,7 @@ class Siae(models.Model): FIELDS_FROM_C1 = [ "name", "slug", # generated from 'name' - "brand", + # "brand", # see UPDATE_FIELDS_IF_EMPTY in management/commands/sync_with_emplois_inclusion.py "siret", "naf", "website", @@ -617,7 +618,7 @@ class Siae(models.Model): name = models.CharField(verbose_name="Raison sociale", max_length=255) slug = models.SlugField(verbose_name="Slug", max_length=255, unique=True) - brand = models.CharField(verbose_name="Enseigne", max_length=255, blank=True) + brand = models.CharField(verbose_name="Nom commercial", max_length=255, blank=True) kind = models.CharField( verbose_name="Type de structure", max_length=6, @@ -1245,6 +1246,18 @@ def set_super_badge(self): self.save(update_fields=update_fields_list) + def clean(self): + """ + Validate that brand is not used as a brand or name by another Siae + Does not use a unique constraint on model because it allows blank values and checks two fields simultaneously. + """ + super().clean() + if self.brand: + # Check if brand is used as name by another Siae + name_exists = Siae.objects.exclude(id=self.id).filter(Q(name=self.brand) | Q(brand=self.brand)).exists() + if name_exists: + raise ValidationError({"brand": "Ce nom commercial est déjà utilisé par une autre structure."}) + @receiver(post_save, sender=Siae) def siae_post_save(sender, instance, **kwargs): diff --git a/lemarche/siaes/tests/test_commands.py b/lemarche/siaes/tests/test_commands.py index 60c7acd86..b78cc1541 100644 --- a/lemarche/siaes/tests/test_commands.py +++ b/lemarche/siaes/tests/test_commands.py @@ -1,4 +1,8 @@ import factory +import logging +import os +from unittest.mock import patch + from django.core.management import call_command from django.db.models import signals from django.test import TransactionTestCase @@ -8,10 +12,302 @@ from lemarche.sectors.factories import SectorFactory from lemarche.siaes import constants as siae_constants from lemarche.siaes.factories import SiaeActivityFactory, SiaeFactory -from lemarche.siaes.models import SiaeActivity +from lemarche.siaes.models import Siae, SiaeActivity from lemarche.users.factories import UserFactory +class SyncWithEmploisInclusionCommandTest(TransactionTestCase): + def setUp(self): + logging.disable(logging.DEBUG) # setting in tests disables all logging by default + + @patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list") + def test_sync_with_emplois_inclusion_create_new_siae(self, mock_get_siae_list): + # Mock API response for a new SIAE + mock_get_siae_list.return_value = [ + { + "id": 123, + "siret": "12345678901234", + "naf": "8899B", + "kind": "EI", + "name": "New SIAE", + "brand": "", + "phone": "", + "email": "", + "website": "", + "description": "", + "address_line_1": "1 rue Test", + "address_line_2": "", + "post_code": "37000", + "city": "Tours", + "department": "37", + "source": "ASP", + "latitude": 0, + "longitude": 0, + "convention_is_active": True, + "convention_asp_id": 0, + "admin_name": "", + "admin_email": "", + } + ] + + # Verify SIAE doesn't exist + self.assertEqual(Siae.objects.count(), 0) + + # Run command + os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test" + with self.assertNoLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion"): + call_command("sync_with_emplois_inclusion") + + # Verify SIAE was created + self.assertEqual(Siae.objects.count(), 1) + siae = Siae.objects.first() + self.assertEqual(siae.siret, "12345678901234") + self.assertEqual(siae.name, "New SIAE") + self.assertEqual(siae.brand, "") + + @patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list") + def test_sync_with_emplois_inclusion_update_existing_siae(self, mock_get_siae_list): + # Create existing SIAE + existing_siae = SiaeFactory(c1_id=123, siret="12345678901234", kind=siae_constants.KIND_EI) + + return_value = [ + { + "id": 123, + "siret": "12345678901234", + "naf": "8899B", + "kind": "EI", + "name": "New SIAE", + "brand": "Updated Name", + "phone": "", + "email": "", + "website": "", + "description": "", + "address_line_1": "2 rue Test", + "address_line_2": "", + "post_code": "69001", + "city": "Lyon", + "department": "69", + "source": "ASP", + "latitude": 0, + "longitude": 0, + "convention_is_active": True, + "convention_asp_id": 0, + "admin_name": "", + "admin_email": "", + } + ] + + # Mock API response with updated data + mock_get_siae_list.return_value = return_value + + # Run command + os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test" + with self.assertNoLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion"): + call_command("sync_with_emplois_inclusion") + + # Verify SIAE was updated + self.assertEqual(Siae.objects.count(), 1) + updated_siae = Siae.objects.get(id=existing_siae.id) + self.assertEqual(updated_siae.brand, "Updated Name") # update first time + self.assertEqual(updated_siae.address, "2 rue Test") + self.assertEqual(updated_siae.post_code, "69001") + self.assertEqual(updated_siae.city, "Lyon") + self.assertEqual(updated_siae.department, "69") + + # Mock API response with updated data for the same SIAE with different brand name + return_value[0]["brand"] = "Other Name" + mock_get_siae_list.return_value = return_value + + # Run command + with self.assertNoLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion"): + call_command("sync_with_emplois_inclusion") + + # Verify SIAE was updated + self.assertEqual(Siae.objects.count(), 1) + updated_siae.refresh_from_db() + self.assertEqual(updated_siae.brand, "Updated Name") # Brand name can only be updated once + + @patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list") + def test_sync_with_emplois_inclusion_with_duplicate_brand_name_on_create(self, mock_get_siae_list): + # Create existing SIAE with the same brand name + SiaeFactory(siret="98765432101233", brand="Duplicate Brand", kind=siae_constants.KIND_EI) + + # Mock API response with duplicate brand name + mock_get_siae_list.return_value = [ + { + "id": 123, + "siret": "12345678901234", + "naf": "8899B", + "kind": "EI", + "name": "New SIAE", + "brand": "Duplicate Brand", + "phone": "", + "email": "", + "website": "", + "description": "", + "address_line_1": "1 rue Test", + "address_line_2": "", + "post_code": "37000", + "city": "Tours", + "department": "37", + "source": "ASP", + "latitude": 0, + "longitude": 0, + "convention_is_active": True, + "convention_asp_id": 0, + "admin_name": "", + "admin_email": "", + } + ] + + # Run command (should not raise exception) + os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test" + with self.assertLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion", level="ERROR") as log: + call_command("sync_with_emplois_inclusion") + + # Verify warning was logged + self.assertIn("Brand name is already used by another SIAE", log.output[0]) + + # Verify both SIAEs exist + self.assertEqual(Siae.objects.count(), 1) + + @patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list") + def test_sync_with_emplois_inclusion_with_duplicate_brand_name_on_update(self, mock_get_siae_list): + # Create existing SIAE with the same brand name + SiaeFactory(siret="98765432101233", brand="Duplicate Brand", kind=siae_constants.KIND_EI) + SiaeFactory(siret="98765432101234", c1_id=123, kind=siae_constants.KIND_EI) + + self.assertEqual(Siae.objects.count(), 2) + + # Mock API response with duplicate brand name + mock_get_siae_list.return_value = [ + { + "id": 123, + "siret": "12345678901234", + "naf": "8899B", + "kind": "EI", + "name": "New SIAE", + "brand": "Duplicate Brand", + "phone": "", + "email": "", + "website": "", + "description": "", + "address_line_1": "1 rue Test", + "address_line_2": "", + "post_code": "37000", + "city": "Tours", + "department": "37", + "source": "ASP", + "latitude": 0, + "longitude": 0, + "convention_is_active": True, + "convention_asp_id": 0, + "admin_name": "", + "admin_email": "", + }, + { + "id": 124, + "siret": "12345678901235", + "naf": "8899B", + "kind": "EI", + "name": "Other New SIAE", + "brand": "", + "phone": "", + "email": "", + "website": "", + "description": "", + "address_line_1": "1 rue Test", + "address_line_2": "", + "post_code": "37000", + "city": "Tours", + "department": "37", + "source": "ASP", + "latitude": 0, + "longitude": 0, + "convention_is_active": True, + "convention_asp_id": 0, + "admin_name": "", + "admin_email": "", + }, + ] + + # Run command (should not raise exception) + os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test" + with self.assertLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion", level="ERROR") as log: + call_command("sync_with_emplois_inclusion") + + # Verify warning was logged + self.assertIn("Brand name is already used by another SIAE: 'Duplicate Brand'", log.output[0]) + + # Verify both SIAEs exist + self.assertEqual(Siae.objects.count(), 3) + self.assertEqual(Siae.objects.filter(brand="Duplicate Brand").count(), 1) + + self.assertEqual(Siae.objects.filter(name="Other New SIAE").count(), 1) # error logged but sync continued + + @patch("lemarche.utils.apis.api_emplois_inclusion.get_siae_list") + def test_sync_with_emplois_inclusion_with_kind_not_supported(self, mock_get_siae_list): + mock_get_siae_list.return_value = [ + { + "id": 123, + "siret": "12345678901234", + "kind": "FAKE", + "name": "Fake SIAE", + "naf": "8899B", + "brand": "", + "phone": "", + "email": "", + "website": "", + "description": "", + "address_line_1": "1 rue Test", + "address_line_2": "", + "post_code": "37000", + "city": "Tours", + "department": "37", + "source": "ASP", + "latitude": 0, + "longitude": 0, + "convention_is_active": True, + "convention_asp_id": 0, + "admin_name": "", + "admin_email": "", + }, + { + "id": 124, + "siret": "12345678901235", + "naf": "8899B", + "kind": "EI", + "name": "Other SIAE", + "brand": "", + "phone": "", + "email": "", + "website": "", + "description": "", + "address_line_1": "1 rue Test", + "address_line_2": "", + "post_code": "37000", + "city": "Tours", + "department": "37", + "source": "ASP", + "latitude": 0, + "longitude": 0, + "convention_is_active": True, + "convention_asp_id": 0, + "admin_name": "", + "admin_email": "", + }, + ] + os.environ["API_EMPLOIS_INCLUSION_TOKEN"] = "test" + with self.assertLogs("lemarche.siaes.management.commands.sync_with_emplois_inclusion", level="ERROR") as log: + call_command("sync_with_emplois_inclusion") + + self.assertIn("Kind not supported: FAKE", log.output[0]) + + # Verify only one SIAE was created to check if the sync was not interrupted + self.assertEqual(Siae.objects.count(), 1) + self.assertEqual(Siae.objects.first().name, "Other SIAE") + + class SiaeActivitiesCreateCommandTest(TransactionTestCase): def setUp(self): self.sector1 = SectorFactory() diff --git a/lemarche/templates/dashboard/siae_edit_info.html b/lemarche/templates/dashboard/siae_edit_info.html index ec6417c80..c875bf82c 100644 --- a/lemarche/templates/dashboard/siae_edit_info.html +++ b/lemarche/templates/dashboard/siae_edit_info.html @@ -1,235 +1,245 @@ {% extends "dashboard/siae_edit_base.html" %} {% load static dsfr_tags get_verbose_name %} - {% block extra_css %} - + {% endblock extra_css %} - {% block content_siae_form %} -
- {% csrf_token %} - {% if form.non_field_errors %} -
- {{ form.non_field_errors }} -
- {% endif %} -
-
- {% dsfr_form_field form.description %} - -
-
-
- {{ form.logo_url.as_hidden }} - - {% include "storage/s3_upload_form.html" with dropzone_form_id="logo_form" %} + + {% csrf_token %} + {% if form.non_field_errors %} +
+ {{ form.non_field_errors }} +
+ {% endif %} +
+
+ {% dsfr_form_field form.brand %} + {% dsfr_form_field form.description %} + +
+
+
+ {{ form.logo_url.as_hidden }} + + {% include "storage/s3_upload_form.html" with dropzone_form_id="logo_form" %} +
-
-
-
- - {% if form.logo_url.value %} -
- -
- {% else %} -

Aucun

- {% endif %} +
+
+ + {% if form.logo_url.value %} +
+ +
+ {% else %} +

Aucun

+ {% endif %} +
+ {% if last_3_siae_content_filled_full_annotated %} +
+
+

+ Conseil +

+

+ Inspirez-vous des fiches commerciales des prestataires inclusifs + {{ last_3_siae_content_filled_full_annotated.0.name_display }} + et {{ last_3_siae_content_filled_full_annotated.1.name_display }}. +
+ Une fiche commerciale bien complétée c'est davantage de chances d'être sollicité par des clients. +

+
+
+ {% endif %}
- {% if last_3_siae_content_filled_full_annotated %} +
+
{% dsfr_form_field form.ca %}
-

Conseil

+

+ Conseil +

- Inspirez-vous des fiches commerciales des prestataires inclusifs - {{ last_3_siae_content_filled_full_annotated.0.name_display }} - et {{ last_3_siae_content_filled_full_annotated.1.name_display }}. -
- Une fiche commerciale bien complétée c'est davantage de chances d'être sollicité par des clients. + Votre chiffre d'affaires est un élément d'information important aux yeux des acheteurs. + Il permet de rendre compte de votre dimension et de votre capacité à répondre à certains marchés et appels d'offres.

- {% endif %} -
- -
-
- {% dsfr_form_field form.ca %} -
-
-
-

Conseil

-

- Votre chiffre d'affaires est un élément d'information important aux yeux des acheteurs. - Il permet de rendre compte de votre dimension et de votre capacité à répondre à certains marchés et appels d'offres. -

-
-
-
- -
-
- {% dsfr_form_field form.year_constitution %}
-
- -
-
- {% dsfr_form_field form.employees_insertion_count %} +
+
{% dsfr_form_field form.year_constitution %}
-
-
-

Conseil

-

- Le nombre de {{ siae.etp_count_label_display | lower }} démontre à la fois votre capacité de production et l'impact social de votre structure. -

+
+
{% dsfr_form_field form.employees_insertion_count %}
+
+
+

+ Conseil +

+

+ Le nombre de {{ siae.etp_count_label_display | lower }} démontre à la fois votre capacité de production et l'impact social de votre structure. +

+
-
- -
-
- {% dsfr_form_field form.employees_permanent_count %} +
+
{% dsfr_form_field form.employees_permanent_count %}
-
- -
-
- {% include "includes/forms/_dsfr_formset.html" with formset_title="Labels et certifications" formset=label_formset %} -
-
- {% dsfr_form label_formset.empty_form %} +
+
+ {% include "includes/forms/_dsfr_formset.html" with formset_title="Labels et certifications" formset=label_formset %} +
+
{% dsfr_form label_formset.empty_form %}
+
- -
-
-
-

Pourquoi mettre des labels ?

-

- Certains labels sont recherchés par nos acheteurs, c'est donc un plus de les rendre visible rapidement.
- Exemples : RSEI, ISO 14001, Ecocert… -

+
+
+

+ Pourquoi mettre des labels ? +

+

+ Certains labels sont recherchés par nos acheteurs, c'est donc un plus de les rendre visible rapidement. +
+ Exemples : RSEI, ISO 14001, Ecocert… +

+
-
-
-
- -
-
-
-

{{ siae.name_display }}

-
-
-
-
-
- - {{ siae.siret_display }} +
+
+
+
+
+

{{ siae.name_display }}

+
+
+
+
+
+ + {{ siae.siret_display }} +
-
-
-
- - {{ siae.get_kind_display }} +
+
+ + {{ siae.get_kind_display }} +
-
-
-
- - {{ siae.address }}, - {{ siae.post_code }}, - {{ siae.city }}, - {{ siae.department }}, - {{ siae.region }} +
+
+ + {{ siae.address }}, + {{ siae.post_code }}, + {{ siae.city }}, + {{ siae.department }}, + {{ siae.region }} +
-
-
-
-
- Année de création : - {{ siae.api_entreprise_date_constitution|date:"Y"|default:"" }} +
+
+
+ Année de création : + {{ siae.api_entreprise_date_constitution|date:"Y"|default:"" }} +
-
-
-
- {% if siae.kind == 'SEP' %}Travailleurs détenus{% else %}Salariés{% endif %} : - {{ siae.api_entreprise_employees|default:"non disponible" }} +
+
+ + {% if siae.kind == 'SEP' %} + Travailleurs détenus + {% else %} + Salariés + {% endif %} + : + {{ siae.api_entreprise_employees|default:"non disponible" }} +
-
-
-
- Chiffre d'affaires : - {{ siae.api_entreprise_ca|default:"non disponible" }} +
+
+ Chiffre d'affaires : + {{ siae.api_entreprise_ca|default:"non disponible" }} +
-
- {% if siae.is_qpv or siae.is_zrr %} -
-
- {% if siae.is_qpv %} -
-
- QPV : {{ siae.qpv_name }} ({{ siae.qpv_code }}) + {% if siae.is_qpv or siae.is_zrr %} +
+
+ {% if siae.is_qpv %} +
+
+ QPV : {{ siae.qpv_name }} ({{ siae.qpv_code }}) +
-
- {% endif %} - {% if siae.is_zrr %} -
-
- ZRR : {{ siae.zrr_name }} ({{ siae.zrr_code }}) + {% endif %} + {% if siae.is_zrr %} +
+
+ ZRR : {{ siae.zrr_name }} ({{ siae.zrr_code }}) +
-
- {% endif %} + {% endif %} +
-
- {% endif %} + {% endif %} +
-
-
-
-

Conseil

-

- Toutes les informations affichées ici sont en provenance de {{ siae.source_display }} et de données ouvertes (API Entreprise, API QPV et API ZRR). -

+
+
+

+ Conseil +

+

+ Toutes les informations affichées ici sont en provenance de {{ siae.source_display }} et de données ouvertes (API Entreprise, API QPV et API ZRR). +

+
-
- -
-
-
    -
  • - {% dsfr_button label="Enregistrer mes modifications" extra_classes="fr-mt-4v" %} - {% comment %}The following tooltip is triggered in s3_upload.js{% endcomment %} - -
  • -
+
+
+
    +
  • + {% dsfr_button label="Enregistrer mes modifications" extra_classes="fr-mt-4v" %} + {% comment %}The following tooltip is triggered in s3_upload.js{% endcomment %} + +
  • +
+
+
-
-
- + {% endblock content_siae_form %} - {% block extra_js %} - - -{{ s3_form_values_siae_logo|json_script:"s3-form-values-siae-logo" }} -{{ s3_upload_config_siae_logo|json_script:"s3-upload-config-siae-logo" }} - + + {{ s3_form_values_siae_logo|json_script:"s3-form-values-siae-logo" }} + {{ s3_upload_config_siae_logo|json_script:"s3-upload-config-siae-logo" }} + - + + {% endblock extra_js %} diff --git a/lemarche/templates/siaes/_card_detail.html b/lemarche/templates/siaes/_card_detail.html index 11674db45..2d0f583a9 100644 --- a/lemarche/templates/siaes/_card_detail.html +++ b/lemarche/templates/siaes/_card_detail.html @@ -9,12 +9,12 @@ {% if siae.logo_url %} Logo de la structure {{ siae.name }} {% else %} {{ siae.name }} {% endif %}
@@ -72,12 +72,12 @@

  • - + {{ siae.get_kind_display }}
  • {% if siae.legal_form %}
  • - + {{ siae.get_legal_form_display }}
  • {% endif %} diff --git a/lemarche/templates/siaes/_useful_infos.html b/lemarche/templates/siaes/_useful_infos.html index 7287fa8d2..c800c89fd 100644 --- a/lemarche/templates/siaes/_useful_infos.html +++ b/lemarche/templates/siaes/_useful_infos.html @@ -1,94 +1,101 @@ {% load get_verbose_name array_choices_display %} -
    -
      -
    • - +
        +
      • + + Raison sociale : + {{ siae.name }} +
      • +
      • + Année de création : {{ siae.year_constitution_display }}
      • -
      • - +
      • + SIRET : {{ siae.siret_display }}
      • -
      • - +
      • + Chiffre d'affaires : {{ siae.ca_display }}
      • {% if siae.etablissement_count > 1 %} -
      • - +
      • + Nombre d'établissements : - {{ siae.etablissement_count }} + {{ siae.etablissement_count }}
      • {% endif %} -
      • +
      • {% if siae.presta_type %} - + {% array_choices_display siae 'presta_type' %} {% endif %}
      • -
      • - +
      • + Salariés permanents : {{ siae.employees_permanent_count|default:"non disponible" }}
      • -
      • - +
      • + {{ siae.etp_count_label_display }} : {{ siae.etp_count_display|floatformat:0|default:"non disponible" }}
      • {% if inbound_email_is_activated %} {% if siae.contact_website %} -
      • - - Site internet +
      • + + Site internet
      • {% endif %} {% if siae.contact_social_website %} -
      • - - Réseau social +
      • + + Réseau social
      • {% endif %} {% if siae.is_missing_contact %} -
      • - - Google +
      • + + Google
      • {% endif %} {% endif %} -
      • - {% include "siaes/_annuaire_entreprises_button.html" with siret=siae.siret %} -
      • +
      • {% include "siaes/_annuaire_entreprises_button.html" with siret=siae.siret %}
    -
    -
    +
      -
    • - +
    • + Situé à : {{ siae.city }}
    • -
    • - +
    • + Adresse : {{ siae.address }} {{ siae.post_code }} {{ siae.city }}
    • -
    • - - Intervient sur : - {{ siae.geo_range_pretty_display }} -
    diff --git a/lemarche/utils/apis/api_emplois_inclusion.py b/lemarche/utils/apis/api_emplois_inclusion.py index fa8aa4a01..593795b07 100644 --- a/lemarche/utils/apis/api_emplois_inclusion.py +++ b/lemarche/utils/apis/api_emplois_inclusion.py @@ -8,6 +8,7 @@ logger = logging.getLogger(__name__) +# Doc : https://emplois.inclusion.beta.gouv.fr/api/v1/redoc/#tag/marche/operation/marche_list API_ENDPOINT = f"{settings.API_EMPLOIS_INCLUSION_URL}/marche" API_HEADERS = {"Authorization": f"Token {settings.API_EMPLOIS_INCLUSION_TOKEN}"} diff --git a/lemarche/www/dashboard_siaes/forms.py b/lemarche/www/dashboard_siaes/forms.py index 807af632a..c9561db65 100644 --- a/lemarche/www/dashboard_siaes/forms.py +++ b/lemarche/www/dashboard_siaes/forms.py @@ -115,6 +115,7 @@ class SiaeEditInfoForm(forms.ModelForm, DsfrBaseForm): class Meta: model = Siae fields = [ + "brand", "description", "logo_url", "ca", @@ -126,6 +127,7 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self.fields["brand"].widget.attrs.update({"placeholder": self.instance.name}) self.fields["description"].label = "Présentation commerciale de votre structure" self.fields["description"].widget.attrs.update( { diff --git a/lemarche/www/dashboard_siaes/tests.py b/lemarche/www/dashboard_siaes/tests.py index e74126afb..4346c968e 100644 --- a/lemarche/www/dashboard_siaes/tests.py +++ b/lemarche/www/dashboard_siaes/tests.py @@ -155,6 +155,57 @@ def test_only_siae_user_can_access_siae_edit_tabs(self): self.assertEqual(response.status_code, 302) self.assertEqual(response.url, "/profil/") + def test_siae_edit_info_form(self): + self.client.force_login(self.user_siae) + url = reverse("dashboard_siaes:siae_edit_info", args=[self.siae_with_user.slug]) + response = self.client.get(url) + self.assertEqual(response.status_code, 200) + + data = { + "description": "Nouvelle description de l'activité", + "ca": 1000000, + "year_constitution": 2024, + "employees_insertion_count": 10, + "employees_permanent_count": 5, + "labels_old-0-name": "Label 1", + "labels_old-1-name": "Label 2", + "labels_old-TOTAL_FORMS": 2, + "labels_old-INITIAL_FORMS": 0, + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("dashboard_siaes:siae_edit_info", args=[self.siae_with_user.slug])) + + # Check that the data has been updated + self.siae_with_user.refresh_from_db() + self.assertEqual(self.siae_with_user.name_display, self.siae_with_user.name) + self.assertEqual(self.siae_with_user.description, "Nouvelle description de l'activité") + self.assertEqual(self.siae_with_user.ca, 1000000) + self.assertEqual(self.siae_with_user.year_constitution, 2024) + self.assertEqual(self.siae_with_user.employees_insertion_count, 10) + self.assertEqual(self.siae_with_user.employees_permanent_count, 5) + + data["brand"] = "Nouveau nom commercial" + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 302) + self.assertEqual(response.url, reverse("dashboard_siaes:siae_edit_info", args=[self.siae_with_user.slug])) + self.siae_with_user.refresh_from_db() + self.assertEqual(self.siae_with_user.brand, "Nouveau nom commercial") + self.assertEqual(self.siae_with_user.name_display, "Nouveau nom commercial") + + def test_siae_edit_info_form_brand_unique(self): + SiaeFactory(brand="Nouveau nom commercial") + + self.client.force_login(self.user_siae) + url = reverse("dashboard_siaes:siae_edit_info", args=[self.siae_with_user.slug]) + + data = { + "brand": "Nouveau nom commercial", + } + response = self.client.post(url, data=data) + self.assertEqual(response.status_code, 200) + self.assertContains(response, "Ce nom commercial est déjà utilisé par une autre structure.") + class DashboardSiaeUserViewTest(TestCase): @classmethod