Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions RELEASE.rst
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
Release Notes
=============

Version 1.143.4
---------------

- add certificate_available field to programs that mirrors courses (#3421)
- Add send-to-Google-Sheets support for B2B enrollment codes (#3363)
- Remove department as required field (#3407)

Version 1.143.2 (Released March 25, 2026)
---------------

Expand Down
3 changes: 3 additions & 0 deletions b2b/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
OrganizationPage,
UserOrganization,
)
from b2b.tasks import queue_contract_sheet_update_post_save
from cms.api import get_home_page
from courses.constants import UAI_COURSEWARE_ID_PREFIX
from courses.models import Course, CourseRun, Department, EnrollmentMode
Expand Down Expand Up @@ -933,6 +934,8 @@ def ensure_enrollment_codes_exist(contract: ContractPage):
total_updated += updated
total_errors += errors

queue_contract_sheet_update_post_save.delay(contract.id)

return (total_created, total_updated, total_errors)


Expand Down
33 changes: 33 additions & 0 deletions b2b/migrations/0020_add_google_sheets_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Generated by Django 5.1.15 on 2026-03-13 20:16

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("b2b", "0019_backfill_org_key_prefixes"),
]

operations = [
migrations.AddField(
model_name="contractpage",
name="google_sheet_target",
field=models.CharField(
blank=True,
default="",
help_text="The URL for the Google Sheet to use to send codes and updates.",
max_length=1024,
null=True,
),
),
migrations.AddField(
model_name="contractpage",
name="google_sheet_target_tab",
field=models.CharField(
blank=True,
default="Sheet1",
help_text="The index or title of the worksheet in the Google Sheet to put the codes.",
max_length=100,
),
),
]
32 changes: 27 additions & 5 deletions b2b/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,19 @@ class ContractPage(Page, ClusterableModel):
null=True,
help_text="The fixed price for enrollment under this contract. (Set to zero or leave blank for free.)",
)
google_sheet_target = models.CharField( # noqa: DJ001
blank=True,
null=True,
default="",
max_length=1024,
help_text="The URL for the Google Sheet to use to send codes and updates.",
)
google_sheet_target_tab = models.CharField(
blank=True,
default="Sheet1",
max_length=100,
help_text="The index or title of the worksheet in the Google Sheet to put the codes.",
)

@property
def programs(self):
Expand Down Expand Up @@ -346,6 +359,8 @@ def programs(self):
FieldPanel("membership_type"),
FieldPanel("max_learners"),
FieldPanel("enrollment_fixed_price"),
FieldPanel("google_sheet_target"),
FieldPanel("google_sheet_target_tab"),
],
heading="Learner Provisioning",
icon="user",
Expand Down Expand Up @@ -468,19 +483,26 @@ def get_products(self):
object_id__in=self.get_course_runs().values_list("id", flat=True),
).all()

def get_discounts(self):
"""Get the discounts associated with the contract."""
@property
def discounts_qs(self):
"""Get the discount queryset associated with the contract."""

from ecommerce.models import Discount # noqa: PLC0415

return Discount.objects.filter(products__product__in=self.get_products()).all()
return Discount.objects.filter(products__product__in=self.get_products())

def get_discounts(self):
"""Get the discounts associated with the contract."""

return self.discounts_qs.all()

def get_unused_discounts(self):
"""Get discounts that haven't been used yet."""

return (
self.get_discounts()
.annotate(order_redemptions_count=models.Count("order_redemptions"))
self.discounts_qs.annotate(
order_redemptions_count=models.Count("order_redemptions")
)
.annotate(contract_redemptions_count=models.Count("contract_redemptions"))
.exclude(
models.Q(order_redemptions_count__gt=0)
Expand Down
Loading
Loading