diff --git a/lemarche/templates/tenders/_detail_cocontracting_click_confirm.html b/lemarche/templates/tenders/_detail_cocontracting_click_confirm.html
new file mode 100644
index 000000000..e7fbcba4f
--- /dev/null
+++ b/lemarche/templates/tenders/_detail_cocontracting_click_confirm.html
@@ -0,0 +1,6 @@
+
+
+ Nous avons bien pris en compte votre demande de mise en relation.
+ Notre équipe revient vers vous dans les plus brefs délais.
+
+
diff --git a/lemarche/templates/tenders/_detail_cocontracting_click_error.html b/lemarche/templates/tenders/_detail_cocontracting_click_error.html
new file mode 100644
index 000000000..9e6b60975
--- /dev/null
+++ b/lemarche/templates/tenders/_detail_cocontracting_click_error.html
@@ -0,0 +1,5 @@
+
+
+ N'ayant pu identifier votre structure, nous n'avons pas pu prendre en compte votre demande de mise en relation.
+
+
diff --git a/lemarche/templates/tenders/_detail_cta_cocontracting.html b/lemarche/templates/tenders/_detail_cta_cocontracting.html
new file mode 100644
index 000000000..af7d92e7d
--- /dev/null
+++ b/lemarche/templates/tenders/_detail_cta_cocontracting.html
@@ -0,0 +1,17 @@
+
+
+
+
Répondre en co-traitance ?
+
Notre équipe vous met en relation avec d’autres structures désireuses de répondre en co-traitance.
+ {% if user.is_authenticated %}
+
+ {% elif siae_id %}
+
+ {% endif %}
+
+
+
diff --git a/lemarche/templates/tenders/cocontracting_notification_email_admin_body.txt b/lemarche/templates/tenders/cocontracting_notification_email_admin_body.txt
new file mode 100644
index 000000000..278c3b98f
--- /dev/null
+++ b/lemarche/templates/tenders/cocontracting_notification_email_admin_body.txt
@@ -0,0 +1,8 @@
+La structure {{ siae_name|safe }} souhaite répondre en co-traitance
+
+Titre : {{ tender_title|safe }}
+Type : {{ tender_kind|safe }}
+Contact email de l'ESI: {{ siae_contact_email|safe }}
+SIRET : {{ siae_siret|safe }}
+
+Lien dans l'admin : {{ tender_admin_url }}
diff --git a/lemarche/templates/tenders/detail.html b/lemarche/templates/tenders/detail.html
index 927bdf45d..7187fbcb1 100644
--- a/lemarche/templates/tenders/detail.html
+++ b/lemarche/templates/tenders/detail.html
@@ -148,11 +148,13 @@
{% elif not user_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 %}
{% 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 %}
diff --git a/lemarche/www/tenders/tasks.py b/lemarche/www/tenders/tasks.py
index a3cd7445c..473540845 100644
--- a/lemarche/www/tenders/tasks.py
+++ b/lemarche/www/tenders/tasks.py
@@ -533,3 +533,27 @@ def send_tenders_author_30_days(tender: Tender, kind="feedback"):
}
tender.logs.append(log_item)
tender.save()
+
+
+def notify_admin_siae_wants_cocontracting(tender: Tender, siae: Siae):
+ email_subject = f"Marché de l'inclusion : la structure {siae.name} souhaite répondre en co-traitance"
+ tender_admin_url = get_admin_url_object(tender)
+ email_body = render_to_string(
+ "tenders/cocontracting_notification_email_admin_body.txt",
+ {
+ "tender_title": tender.title,
+ "tender_kind": tender.get_kind_display(),
+ "tender_admin_url": tender_admin_url,
+ "siae_name": siae.name,
+ "siae_contact_email": siae.contact_email,
+ "siae_siret": siae.siret,
+ },
+ )
+ send_mail_async(
+ email_subject=email_subject,
+ email_body=email_body,
+ recipient_list=[settings.NOTIFY_EMAIL],
+ )
+
+ if settings.BITOUBI_ENV == "prod":
+ api_slack.send_message_to_channel(text=email_body, service_id=settings.SLACK_WEBHOOK_C4_SUPPORT_CHANNEL)
diff --git a/lemarche/www/tenders/tests.py b/lemarche/www/tenders/tests.py
index bdd6598c2..844aa60a1 100644
--- a/lemarche/www/tenders/tests.py
+++ b/lemarche/www/tenders/tests.py
@@ -1,5 +1,6 @@
import json
from datetime import timedelta
+from unittest import mock
from django.conf import settings
from django.contrib.gis.geos import Point
@@ -849,8 +850,44 @@ def test_tender_contact_details_display(self):
self.assertNotContains(response, "Voir l'appel d'offres")
self.assertContains(response, "Lien partagé")
+ def test_tender_cocontracting_display(self):
+ # anonymous
+ url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
+ response = self.client.get(url)
+ self.assertNotContains(response, "Répondre en co-traitance ?")
+ # anonymous but with siae_id in url
+ url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
+ response = self.client.get(f"{url}?siae_id=15")
+ self.assertContains(response, "Répondre en co-traitance ?")
+ # siae user interested but has a detail_contact_click_date
+ self.client.force_login(self.siae_user_1)
+ url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
+ response = self.client.get(url)
+ self.assertNotContains(response, "Répondre en co-traitance ?")
+ # siae user not concerned
+ self.client.force_login(self.siae_user_2)
+ url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
+ response = self.client.get(url)
+ self.assertContains(response, "Répondre en co-traitance ?")
+ # siae user interested
+ TenderSiae.objects.create(tender=self.tender_1, siae=self.siae_2, email_send_date=timezone.now())
+ self.client.force_login(self.siae_user_2)
+ url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
+ response = self.client.get(url)
+ self.assertContains(response, "Répondre en co-traitance ?")
+ # siae user without siae
+ self.client.force_login(self.siae_user_3)
+ url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
+ response = self.client.get(url)
+ self.assertNotContains(response, "Répondre en co-traitance ?")
+ # author
+ self.client.force_login(self.user_buyer_1)
+ url = reverse("tenders:detail", kwargs={"slug": self.tender_1.slug})
+ response = self.client.get(url)
+ self.assertNotContains(response, "Répondre en co-traitance ?")
-class TenderDetailContactClickStatViewViewTest(TestCase):
+
+class TenderDetailContactClickStatViewTest(TestCase):
@classmethod
def setUpTestData(cls):
cls.siae = SiaeFactory(name="ZZ ESI")
@@ -962,6 +999,65 @@ def test_update_tendersiae_stats_on_tender_contact_click_with_siae_id_param(self
)
+class TenderDetailCocontractingClickView(TestCase):
+ @classmethod
+ def setUpTestData(self):
+ self.siae = SiaeFactory(name="ZZ ESI")
+ self.siae_user = UserFactory(kind=User.KIND_SIAE, siaes=[self.siae])
+ self.user_buyer = UserFactory(kind=User.KIND_BUYER, company_name="Entreprise Buyer")
+ self.tender = TenderFactory(
+ kind=tender_constants.KIND_TENDER,
+ author=self.user_buyer,
+ amount=tender_constants.AMOUNT_RANGE_100_150,
+ accept_share_amount=True,
+ response_kind=[Tender.RESPONSE_KIND_EMAIL],
+ )
+ self.tendersiae = TenderSiae.objects.create(
+ tender=self.tender,
+ siae=self.siae,
+ source="EMAIL",
+ email_send_date=timezone.now(),
+ email_link_click_date=timezone.now(),
+ detail_display_date=timezone.now(),
+ detail_contact_click_date=timezone.now(),
+ )
+ TenderQuestionFactory(tender=self.tender)
+
+ def test_user_can_notify_cocontracting_wish(self):
+ url = reverse("tenders:detail-cocontracting-click", kwargs={"slug": self.tender.slug})
+ with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
+ response = self.client.post(url, data={})
+ self.assertEqual(response.status_code, 403)
+ mock_send_mail_async.assert_not_called()
+
+ with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
+ response = self.client.post(f"{url}?siae_id=999999", data={})
+ self.assertContains(response, "nous n'avons pas pu prendre en compte votre demande de mise en relation")
+ mock_send_mail_async.assert_not_called()
+
+ with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
+ response = self.client.post(f"{url}?siae_id={self.siae.id}", data={})
+ self.assertContains(response, "Nous avons bien pris en compte votre demande de mise en relation")
+ mock_send_mail_async.assert_called_once()
+ email_body = mock_send_mail_async.call_args[1]["email_body"]
+ self.assertTrue(f"La structure {self.siae.name } souhaite répondre en co-traitance" in email_body)
+
+ self.client.force_login(self.siae_user)
+ with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
+ response = self.client.post(url, data={})
+ self.assertContains(response, "Nous avons bien pris en compte votre demande de mise en relation")
+ mock_send_mail_async.assert_called_once()
+ email_body = mock_send_mail_async.call_args[1]["email_body"]
+ self.assertTrue(f"La structure {self.siae.name } souhaite répondre en co-traitance" in email_body)
+
+ user_without_siae = UserFactory(kind=User.KIND_SIAE)
+ self.client.force_login(user_without_siae)
+ with mock.patch("lemarche.www.tenders.tasks.send_mail_async") as mock_send_mail_async:
+ response = self.client.post(url, data={})
+ self.assertContains(response, "nous n'avons pas pu prendre en compte votre demande de mise en relation")
+ mock_send_mail_async.assert_not_called()
+
+
# TODO: this test doesn't work anymore. find a way to test logging post-email in non-prod environments?
# class TenderTasksTest(TestCase):
# @classmethod
diff --git a/lemarche/www/tenders/urls.py b/lemarche/www/tenders/urls.py
index 87b81324d..b28c8b12f 100644
--- a/lemarche/www/tenders/urls.py
+++ b/lemarche/www/tenders/urls.py
@@ -3,6 +3,7 @@
from lemarche.www.tenders.views import (
TenderCreateMultiStepView,
+ TenderDetailCocontractingClickView,
TenderDetailContactClickStatView,
TenderDetailSurveyTransactionedView,
TenderDetailView,
@@ -32,6 +33,11 @@
path(
"