diff --git a/lemarche/templates/tenders/_detail_card.html b/lemarche/templates/tenders/_detail_card.html index 26e0a6464..25fc853a0 100644 --- a/lemarche/templates/tenders/_detail_card.html +++ b/lemarche/templates/tenders/_detail_card.html @@ -40,7 +40,7 @@

{% elif user_partner_can_display_tender_contact_details %}
{% include "tenders/_detail_contact.html" with tender=tender %} - {% elif user_siae_has_detail_contact_click_date and not tender.deadline_date_outdated %} + {% elif siae_has_detail_contact_click_date and not tender.deadline_date_outdated %} {% endif %} {% endif %} diff --git a/lemarche/templates/tenders/_detail_cta.html b/lemarche/templates/tenders/_detail_cta.html index 9891c8a7b..3fa063cba 100644 --- a/lemarche/templates/tenders/_detail_cta.html +++ b/lemarche/templates/tenders/_detail_cta.html @@ -1,13 +1,20 @@

{{ tender.cta_card_title_text|safe }}

- +

{{ tender.cta_card_paragraph_text|safe }}

{% if user_can_click %} - + {% if tender_is_anonymous %} +
+ {% csrf_token %} + +
+ {% else %} + + {% endif %} {% else %} {{ tender.cta_card_button_text|safe }} diff --git a/lemarche/templates/tenders/_detail_side_infos_author.html b/lemarche/templates/tenders/_detail_side_infos_author.html new file mode 100644 index 000000000..d98847e18 --- /dev/null +++ b/lemarche/templates/tenders/_detail_side_infos_author.html @@ -0,0 +1,52 @@ +{% if tender.is_draft %} +
+
+
+ +
+
+

Votre {{ tender_kind_display|default:tender.get_kind_display }} est encore en brouillon. Modifiez-le pour le publier.

+
+
+
+
+ + Modifier + +{% endif %} +{% if tender.is_pending_validation %} +
+
+
+ +
+
+

Votre {{ tender_kind_display|default:tender.get_kind_display }} est en cours de validation.

+
+
+
+{% endif %} +{% if tender.is_validated %} +
+
+
+ +
+
+

Votre {{ tender_kind_display|default:tender.get_kind_display }} est validé et envoyé !

+
+
+
+ + + {{ tender.siae_email_send_date_count }} prestataire{{ tender.siae_email_send_date_count|pluralize }} ciblé{{ tender.siae_email_send_date_count|pluralize }} + + + + {{ tender.siae_email_link_click_date_or_detail_display_date_count }} prestataire{{ tender.siae_email_link_click_date_or_detail_display_date_count|pluralize }} qui {{ tender.siae_email_link_click_date_or_detail_display_date_count|pluralize:'a,ont' }} vu + + + + {{ tender.siae_detail_contact_click_date_count }} prestataire{{ tender.siae_detail_contact_click_date_count|pluralize }} intéressé{{ tender.siae_detail_contact_click_date_count|pluralize }} + +{% endif %} \ No newline at end of file diff --git a/lemarche/templates/tenders/_detail_success_contact.html b/lemarche/templates/tenders/_detail_success_contact.html new file mode 100644 index 000000000..9ab89b439 --- /dev/null +++ b/lemarche/templates/tenders/_detail_success_contact.html @@ -0,0 +1,8 @@ +
+
+

Votre intérêt a été signalé au client

+ +

S’il est intéressé, il vous recontactera via vos coordonnées de contact présentes sur votre fiche commerciale (Vérifier votre fiche).

+ +
+
diff --git a/lemarche/templates/tenders/create_step_contact.html b/lemarche/templates/tenders/create_step_contact.html index b3f631a46..563eeae07 100644 --- a/lemarche/templates/tenders/create_step_contact.html +++ b/lemarche/templates/tenders/create_step_contact.html @@ -23,6 +23,46 @@
{% endif %} {% bootstrap_field form.response_kind %} + {% bootstrap_field form.response_is_anonymous %}
{% endblock %} + +{% block extra_js %} + + +{% endblock %} diff --git a/lemarche/templates/tenders/detail.html b/lemarche/templates/tenders/detail.html index c17462c55..be39238f8 100644 --- a/lemarche/templates/tenders/detail.html +++ b/lemarche/templates/tenders/detail.html @@ -28,14 +28,16 @@
{# Afficher les contacts en haut + conseil #} - {% if not tender.deadline_date_outdated %} - {% if user_siae_has_detail_contact_click_date or siae_has_detail_contact_click_date %} + {% if not tender.deadline_date_outdated and not tender.response_is_anonymous %} + {% if siae_has_detail_contact_click_date %}
-
-
- {% include "tenders/_detail_contact.html" with tender=tender source="alert" %} + {% if not tender.response_is_anonymous %} +
+
+ {% include "tenders/_detail_contact.html" with tender=tender source="alert" %} +
-
+ {% endif %}
{# Sidebar with actions #}
+ {% comment %} admin info {% endcomment %} {% if is_admin %} {% include "tenders/_detail_admin_extra_info.html" with tender=tender %} {% endif %} + {% comment %} details author {% endcomment %} {% if user == tender.author %} - {% if tender.is_draft %} -
-
-
- -
-
-

Votre {{ tender_kind_display|default:tender.get_kind_display }} est encore en brouillon. Modifiez-le pour le publier.

-
-
-
- - - Modifier - - {% endif %} - {% if tender.is_pending_validation %} -
-
-
- -
-
-

Votre {{ tender_kind_display|default:tender.get_kind_display }} est en cours de validation.

-
-
-
- {% endif %} - {% if tender.is_validated %} -
-
-
- -
-
-

Votre {{ tender_kind_display|default:tender.get_kind_display }} est validé et envoyé !

-
-
-
- - - {{ tender.siae_email_send_date_count }} prestataire{{ tender.siae_email_send_date_count|pluralize }} ciblé{{ tender.siae_email_send_date_count|pluralize }} - - - - {{ tender.siae_email_link_click_date_or_detail_display_date_count }} prestataire{{ tender.siae_email_link_click_date_or_detail_display_date_count|pluralize }} qui {{ tender.siae_email_link_click_date_or_detail_display_date_count|pluralize:'a,ont' }} vu - - - - {{ tender.siae_detail_contact_click_date_count }} prestataire{{ tender.siae_detail_contact_click_date_count|pluralize }} intéressé{{ tender.siae_detail_contact_click_date_count|pluralize }} - - {% endif %} + {% include "tenders/_detail_side_infos_author.html" %} {% else %} {% if not tender.deadline_date_outdated %} + {% comment %} stats of views {% endcomment %} {% if tender.siae_email_link_click_date_or_detail_display_date_count > 0 %} {% endif %} - {% if user.is_authenticated %} + {% if user.is_authenticated and not tender.response_is_anonymous %} + {% comment %} infos for partner {% endcomment %} {% if user.kind == user.KIND_PARTNER %} {% if not user_partner_can_display_tender_contact_details %} {% endif %} + {% comment %} infos for siaes {% endcomment %} {% elif user.kind == user.KIND_SIAE %} {% if not user.has_siae %} - {% elif not user_siae_has_detail_contact_click_date %} + {% elif not siae_has_detail_contact_click_date %} {% include "tenders/_detail_cta.html" with tender=tender user_can_click=True %} {% include "tenders/_detail_cta_cocontracting.html" with tender=tender %} {% endif %} {% endif %} - {% elif siae_id %} + {% comment %} if siae_id {% endcomment %} + {% elif siae_id and not tender.response_is_anonymous %} {% if not siae_has_detail_contact_click_date %} {% include "tenders/_detail_cta.html" with tender=tender user_can_click=True siae_id=siae_id %} {% include "tenders/_detail_cta_cocontracting.html" with tender=tender siae_id=siae_id %} {% else %} {% include "tenders/_detail_contact.html" with tender=tender %} {% endif %} + {% comment %} for anonymous tenders {% endcomment %} + {% elif tender.response_is_anonymous and siae_id or tender.response_is_anonymous and user.is_authenticated %} + {% if not siae_has_detail_contact_click_date %} + {% include "tenders/_detail_cta.html" with tender=tender user_can_click=True tender_is_anonymous=True %} + {% else %} + {% include "tenders/_detail_success_contact.html" %} + {% endif %} {% else %} {% include "tenders/_detail_cta.html" with tender=tender user_can_click=False %} {% endif %} diff --git a/lemarche/tenders/admin.py b/lemarche/tenders/admin.py index 7580e8dd9..99197f88e 100644 --- a/lemarche/tenders/admin.py +++ b/lemarche/tenders/admin.py @@ -1,5 +1,6 @@ from ckeditor.widgets import CKEditorWidget from django import forms +from django.conf import settings from django.contrib import admin from django.contrib.contenttypes.admin import GenericTabularInline from django.db import models @@ -273,6 +274,7 @@ class TenderAdmin(FieldsetsInlineMixin, admin.ModelAdmin): "contact_email", "contact_phone", "response_kind", + "response_is_anonymous", ), }, ), @@ -487,7 +489,8 @@ def response_change(self, request, obj: Tender): if request.POST.get("_validate_tender"): update_and_send_tender_task(tender=obj) self.message_user(request, "Ce dépôt de besoin a été validé et envoyé aux structures") - api_hubspot.create_deal_from_tender(tender=obj) + if settings.BITOUBI_ENV == "prod": + api_hubspot.create_deal_from_tender(tender=obj) return HttpResponseRedirect(".") elif request.POST.get("_restart_tender"): diff --git a/lemarche/tenders/migrations/0061_tender_response_is_anonymous.py b/lemarche/tenders/migrations/0061_tender_response_is_anonymous.py new file mode 100644 index 000000000..040cadd15 --- /dev/null +++ b/lemarche/tenders/migrations/0061_tender_response_is_anonymous.py @@ -0,0 +1,17 @@ +# Generated by Django 4.2.5 on 2023-10-17 14:20 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("tenders", "0060_tender_survey_transactioned_amount_and_feedback"), + ] + + operations = [ + migrations.AddField( + model_name="tender", + name="response_is_anonymous", + field=models.BooleanField(default=False, verbose_name="Je souhaite rester anonyme"), + ), + ] diff --git a/lemarche/tenders/models.py b/lemarche/tenders/models.py index 4bafe953d..149d02be5 100644 --- a/lemarche/tenders/models.py +++ b/lemarche/tenders/models.py @@ -261,6 +261,8 @@ class Tender(models.Model): blank=True, default=list, ) + + response_is_anonymous = models.BooleanField(verbose_name="Je souhaite rester anonyme", blank=False, default=False) accept_cocontracting = models.BooleanField( verbose_name="Ouvert à la co-traitance", help_text="Ce besoin peut être répondu par plusieurs prestataires (co-traitance ou sous-traitance)", @@ -543,6 +545,8 @@ def cta_card_paragraph_text(self): return "Accéder à l'appel d'offres afin d'y répondre." elif self.kind == tender_constants.KIND_QUOTE: return "Accéder aux coordonnées du client afin de lui envoyer un devis." + elif self.kind == tender_constants.KIND_PROJECT and self.response_is_anonymous: + return "Manifestez votre intérêt au client. S’il est intéressé, le client vous recontactera via les coordonnées présentes sur votre fiche commerciale." # noqa elif self.kind == tender_constants.KIND_PROJECT: return "Accéder aux coordonnées du client afin de lui présenter vos services et produits." # just in case @@ -552,6 +556,8 @@ def cta_card_paragraph_text(self): def cta_card_button_text(self): if self.kind == tender_constants.KIND_TENDER: return "Voir l'appel d'offres" + elif self.kind == tender_constants.KIND_PROJECT and self.response_is_anonymous: + return "Je suis intéressé !" return "Accéder aux coordonnées" @cached_property diff --git a/lemarche/www/tenders/forms.py b/lemarche/www/tenders/forms.py index 61cc5e349..dc4e57de9 100644 --- a/lemarche/www/tenders/forms.py +++ b/lemarche/www/tenders/forms.py @@ -107,6 +107,10 @@ def __init__(self, kind, questions_list=None, *args, **kwargs): if self.kind != tender_constants.KIND_TENDER: self.fields["external_link"].label = "Lien à partager" self.fields["external_link"].help_text = None + + if self.kind == tender_constants.KIND_TENDER: + self.fields["external_link"].required = True + self.fields["amount"].label = "Montant € estimé de votre besoin" self.fields["accept_share_amount"].label = self.fields["accept_share_amount"].help_text self.fields["external_link"].widget.attrs["placeholder"] = "https://www.example.fr" @@ -116,10 +120,10 @@ def clean_questions_list(self): questions = self.cleaned_data["questions_list"] if questions is None: return questions - elif type(questions) is not list: + elif not isinstance(questions, list): raise ValueError("It's not a list") for index, question in enumerate(questions): - if type(question) is not dict: + if not isinstance(question, dict): raise ValueError("Bad format") if not question.get("text"): questions.pop(index) @@ -157,8 +161,8 @@ class TenderCreateStepContactForm(forms.ModelForm): label="Comment les prestataires doivent vous répondre", choices=Tender._meta.get_field("response_kind").base_field.choices, widget=forms.CheckboxSelectMultiple, + required=False, ) - contact_company_name = forms.CharField(label="Votre entreprise", widget=forms.HiddenInput(), required=False) class Meta: @@ -169,6 +173,7 @@ class Meta: "contact_email", "contact_phone", "response_kind", + "response_is_anonymous", ] labels = { "contact_first_name": "Prénom", @@ -183,8 +188,10 @@ def __init__(self, external_link, user: User, *args, **kwargs): self.user = user user_is_anonymous = not user.is_authenticated + # display response_is_anonymous if the tender is a project + if self.instance and self.instance.kind != tender_constants.KIND_PROJECT: + self.fields["response_is_anonymous"].widget = forms.HiddenInput() # required fields - self.fields["response_kind"].required = True if user_is_anonymous: self.fields["contact_first_name"].required = True self.fields["contact_last_name"].required = True @@ -205,6 +212,9 @@ def __init__(self, external_link, user: User, *args, **kwargs): def clean(self): super().clean() + if not self.cleaned_data.get("response_kind") and not self.cleaned_data.get("response_is_anonymous"): + self.add_error("response_kind", "Ce champ est obligatoire.") + if not self.user.is_authenticated: # contact_email must be filled if RESPONSE_KIND_EMAIL if self.cleaned_data.get("response_kind") and ( diff --git a/lemarche/www/tenders/tests.py b/lemarche/www/tenders/tests.py index cc7082027..f09436549 100644 --- a/lemarche/www/tenders/tests.py +++ b/lemarche/www/tenders/tests.py @@ -142,6 +142,22 @@ def test_tender_wizard_form_not_created(self): with self.assertRaises(AssertionError): self._check_every_step(tenders_step_data, final_redirect_page=reverse("siae:search_results")) + def test_tender_wizard_form_external_link_required_for_tender(self): + self.client.force_login(self.user_buyer) + tenders_step_data = self._generate_fake_data_form(_step_1={"general-kind": tender_constants.KIND_TENDER}) + # remove required field in survey + tenders_step_data[1].pop("detail-external_link") + with self.assertRaises(AssertionError): + self._check_every_step(tenders_step_data, final_redirect_page=reverse("siae:search_results")) + + def test_tender_wizard_form_contact_response_required_for_project(self): + self.client.force_login(self.user_buyer) + tenders_step_data = self._generate_fake_data_form(_step_1={"general-kind": tender_constants.KIND_PROJECT}) + # remove required field in survey + tenders_step_data[2].pop("contact-response_kind") + with self.assertRaises(AssertionError): + self._check_every_step(tenders_step_data, final_redirect_page=reverse("siae:search_results")) + def test_tender_wizard_form_all_good_anonymous(self): tenders_step_data = self._generate_fake_data_form() final_response = self._check_every_step(tenders_step_data, final_redirect_page=reverse("siae:search_results")) diff --git a/lemarche/www/tenders/views.py b/lemarche/www/tenders/views.py index d72129349..fa25fc823 100644 --- a/lemarche/www/tenders/views.py +++ b/lemarche/www/tenders/views.py @@ -376,7 +376,7 @@ def get_context_data(self, **kwargs): ).exists() if user.is_authenticated: if user.kind == User.KIND_SIAE: - context["user_siae_has_detail_contact_click_date"] = TenderSiae.objects.filter( + context["siae_has_detail_contact_click_date"] = TenderSiae.objects.filter( tender=self.object, siae__in=user.siaes.all(), detail_contact_click_date__isnull=False ).exists() if show_nps: