]> git.parisson.com Git - teleforma.git/commitdiff
Article 98
authorYoan Le Clanche <yoanl@pilotsystems.net>
Wed, 6 May 2026 11:13:39 +0000 (13:13 +0200)
committerYoan Le Clanche <yoanl@pilotsystems.net>
Wed, 6 May 2026 11:13:39 +0000 (13:13 +0200)
teleforma/management/commands/test_avis_list.py [new file with mode: 0644]
teleforma/migrations/0026_conference_secondary_subject.py [new file with mode: 0644]
teleforma/models/core.py
teleforma/models/pro.py

diff --git a/teleforma/management/commands/test_avis_list.py b/teleforma/management/commands/test_avis_list.py
new file mode 100644 (file)
index 0000000..f3e86d1
--- /dev/null
@@ -0,0 +1,26 @@
+import requests
+from django.core.management.base import BaseCommand
+import datetime
+
+class Command(BaseCommand):
+    help = "Fetch product reviews"
+
+    def handle(self, *args, **kwargs):
+        product_id = "CONF0124"
+        url = f'http://cl.avis-verifies.com/fr/cache/8/d/a/8da32059-03b0-23c4-1554-4f4b505a8c6a/AWS/PRODUCT_API/REVIEWS/{product_id}.json'
+        response = requests.get(url)
+        if response.status_code == 200:
+            avis_list = response.json()
+            for avis in avis_list:
+                print("*"*7)
+                print(avis)
+                print("*"*7)
+                date = avis['publish_date']
+                if date == 'Array':
+                    date = avis['review_date']
+                if date == 'Array':
+                    continue
+                avis['publish_date'] = datetime.datetime.strptime(
+                    date, '%Y-%m-%d %H:%M:%S')
+        else:
+            self.stderr.write(f"Failed to fetch reviews: {response.status_code}")
diff --git a/teleforma/migrations/0026_conference_secondary_subject.py b/teleforma/migrations/0026_conference_secondary_subject.py
new file mode 100644 (file)
index 0000000..88f6326
--- /dev/null
@@ -0,0 +1,33 @@
+# Generated for secondary_subject split between Seminar and Conference
+
+from django.db import migrations, models
+
+
+def clear_oral_blanc_on_seminars(apps, schema_editor):
+    Seminar = apps.get_model('teleforma', 'Seminar')
+    Seminar.objects.filter(secondary_subject='article_98_1_oral_blanc').update(secondary_subject='')
+
+
+def noop_reverse(apps, schema_editor):
+    pass
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('teleforma', '0025_auto_20251105_1331'),
+    ]
+
+    operations = [
+        migrations.RunPython(clear_oral_blanc_on_seminars, noop_reverse),
+        migrations.AlterField(
+            model_name='seminar',
+            name='secondary_subject',
+            field=models.CharField(blank=True, choices=[('', 'Aucune'), ('article_98_1', 'Article 98-1')], default='', max_length=50, verbose_name='Matière secondaire'),
+        ),
+        migrations.AddField(
+            model_name='conference',
+            name='secondary_subject',
+            field=models.CharField(blank=True, choices=[('', 'Aucune'), ('article_98_1_oral_blanc', 'Article 98-1 - Oral blanc')], default='', max_length=50, verbose_name='Matière secondaire'),
+        ),
+    ]
index db517c6d05cc2d95db056b5c7e5e4a0bf13c28dd..37af82307b04b128e73e54425e064e605f94cda1 100755 (executable)
@@ -114,6 +114,12 @@ TRUE_FALSE_CHOICES = (
     (False, 'Non')
 )
 
+
+class ConferenceSecondarySubjectChoices(models.TextChoices):
+    NONE = '', _("Aucune")
+    ARTICLE_98_1_ORAL_BLANC = 'article_98_1_oral_blanc', _("Article 98-1 - Oral blanc")
+
+
 class MetaCore:
     app_label = app_label
 
@@ -927,10 +933,28 @@ class WebclassMixin(Model):
 
 class Conference(Displayable, WebclassMixin, ProductCodeMixin, SuggestionsMixin):
 
+    # secondary_subject -> {pack_size, pack_price, extra_unit_price}:
+    # from pack_size units the price drops to pack_price; each additional
+    # unit beyond the pack is billed at extra_unit_price.
+    PACK_PRICING = {
+        ConferenceSecondarySubjectChoices.ARTICLE_98_1_ORAL_BLANC: {
+            'pack_size': 3,
+            'pack_price': 490,
+            'extra_unit_price': 190,
+        },
+    }
+
     fif_pl          = models.BooleanField(_('Eligible FIF-PL'), default=False)
     new             = models.BooleanField(_('Nouveau'), default=False)
     private         = models.BooleanField(_('private'), help_text="Hide in shop", default=False)
     upcoming        = models.BooleanField("A venir (shop)", default=False)
+    secondary_subject = models.CharField(
+        _("Matière secondaire"),
+        max_length=50,
+        choices=ConferenceSecondarySubjectChoices.choices,
+        blank=True,
+        default=ConferenceSecondarySubjectChoices.NONE
+    )
     sub_title       = models.CharField(_('sub title'), max_length=1024, blank=True)
     public_id       = models.CharField(_('public id'), max_length=255, blank=True)
     department      = models.ForeignKey('Department', related_name='conference', verbose_name=_('department'),
@@ -1002,6 +1026,52 @@ class Conference(Displayable, WebclassMixin, ProductCodeMixin, SuggestionsMixin)
     def get_absolute_url(self):
         return reverse('conference-view', kwargs={"pk": self.id})
 
+    @property
+    def pack_pricing(self):
+        """The pack pricing rule for this conference, or None."""
+        return self.PACK_PRICING.get(self.secondary_subject)
+
+    def get_price_for_quantity(self, quantity, unit_price=None):
+        """
+        Total price for the given quantity, applying any pack rule from
+        PACK_PRICING. Pass unit_price (e.g. CartItem's locked-in price)
+        to override self.price.
+        """
+        if unit_price is None:
+            unit_price = self.price or 0
+        rule = self.pack_pricing
+        if not rule or quantity < rule['pack_size']:
+            return unit_price * quantity
+        cast = type(unit_price)
+        pack_price = cast(rule['pack_price'])
+        extra_price = cast(rule['extra_unit_price'])
+        return pack_price + (quantity - rule['pack_size']) * extra_price
+
+    def get_invoice_lines(self, unit_price, quantity, description):
+        """
+        Yield invoice line dicts for this conference at the given quantity,
+        splitting into a pack line + extras line when a pack rule applies.
+        """
+        rule = self.pack_pricing
+        if not rule or quantity < rule['pack_size']:
+            yield {'designation': description, 'VAT': 0, 'qte': quantity, 'PUHT': unit_price}
+            return
+        cast = type(unit_price)
+        yield {
+            'designation': "Pack de %d - %s" % (rule['pack_size'], description),
+            'VAT': 0,
+            'qte': 1,
+            'PUHT': cast(rule['pack_price']),
+        }
+        extra = quantity - rule['pack_size']
+        if extra > 0:
+            yield {
+                'designation': description,
+                'VAT': 0,
+                'qte': extra,
+                'PUHT': cast(rule['extra_unit_price']),
+            }
+
     def __str__(self):
         if self.professor:
             list = [self.title, self.course.title,
index 38e06cf485b4d5c675d8eeb8be3bab9ea78b20da..b8c38871325d508aa22f18551cdd793e559d8f9a 100644 (file)
@@ -65,10 +65,9 @@ class SeminarType(models.Model):
         verbose_name = _('Seminar type')
 
 
-class SecondarySubjectChoices(models.TextChoices):
+class SeminarSecondarySubjectChoices(models.TextChoices):
     NONE = '', _("Aucune")
     ARTICLE_98_1 = 'article_98_1', _("Article 98-1")
-    ARTICLE_98_1_ORAL_BLANC = 'article_98_1_oral_blanc', _("Article 98-1 - Oral blanc")
 
 
 class Seminar(ClonableMixin, Displayable, ProductCodeMixin, SuggestionsMixin):
@@ -80,9 +79,9 @@ class Seminar(ClonableMixin, Displayable, ProductCodeMixin, SuggestionsMixin):
     secondary_subject = models.CharField(
         _("Matière secondaire"),
         max_length=50,
-        choices=SecondarySubjectChoices.choices,
+        choices=SeminarSecondarySubjectChoices.choices,
         blank=True,
-        default=SecondarySubjectChoices.NONE
+        default=SeminarSecondarySubjectChoices.NONE
     )
     private         = models.BooleanField(_('private'), default=False)
     upcoming        = models.BooleanField("A venir (shop)", default=False)