示例#1
0
class Filter(PolymorphicModel):
    name = I18nCharField(max_length=60)
    order = django_models.PositiveIntegerField(default=0)

    objects = PolymorphicManager.from_queryset(FilterQuerySet)()

    def __str__(self):
        return f'{self.name} filter'

    class Meta:
        ordering = ['order']
示例#2
0
class BaseTask(PolymorphicModel):
    """
    An external task to be processed by work units.

    Use this as the base class for process-engine specific task definitions.
    """

    topic_name = models.CharField(
        _("topic name"),
        max_length=255,
        help_text=_(
            "Topics determine which functions need to run for a task."),
    )
    variables = JSONField(default=dict)
    status = models.CharField(
        _("status"),
        max_length=50,
        choices=Statuses.choices,
        default=Statuses.initial,
        help_text=_("The current status of task processing"),
    )
    result_variables = JSONField(default=dict)
    execution_error = models.TextField(
        _("execution error"),
        blank=True,
        help_text=_("The error that occurred during execution."),
    )
    logs = GenericRelation(TimelineLog, related_query_name="task")

    objects = PolymorphicManager.from_queryset(BaseTaskQuerySet)()

    def get_variables(self) -> dict:
        """
        return input variables formatted for work_unit
        """
        return self.variables

    def request_logs(self) -> models.QuerySet:
        return self.logs.filter(
            extra_data__has_key="request").order_by("-timestamp")

    def status_logs(self) -> models.QuerySet:
        return self.logs.filter(
            extra_data__has_key="status").order_by("-timestamp")

    def __str__(self):
        return f"{self.polymorphic_ctype}: {self.topic_name} / {self.id}"
示例#3
0
class BaseLesson(PolymorphicModel):
    name = CharField(max_length=64)
    course_section = ForeignKey(CourseSection, on_delete=CASCADE, related_name="lessons")
    description = TextField(blank=True)

    objects = PolymorphicManager.from_queryset(BaseLessonQuerySet)()

    class Meta:
        order_with_respect_to = "course_section"

    def is_completed_by(self, user: User) -> bool:
        # is_completed is annotated in with_completed_annotations queryset method.
        # However, if the queryset was not annotated, there's a fallback that performs this check.
        # Keep in mind it is far less efficient.
        if (completed := getattr(self, "is_completed", None)) is not None:
            return completed
        return CompletedLesson.objects.filter(lesson=self, user=user).exists()
示例#4
0
文件: orm.py 项目: uktrade/tamato
class GoodsNomenclatureDescription(DescriptionMixin, TrackedModel):
    record_code = "400"
    subrecord_code = "15"
    period_record_code = "400"
    period_subrecord_code = "10"

    identifying_fields = ("sid", )

    objects = PolymorphicManager.from_queryset(DescriptionQueryset)()

    sid = NumericSID()
    described_goods_nomenclature = models.ForeignKey(
        GoodsNomenclature,
        on_delete=models.PROTECT,
        related_name="descriptions",
    )
    description = LongDescription()

    indirect_business_rules = (business_rules.NIG12, )
    business_rules = (UniqueIdentifyingFields, UpdateValidity)

    class Meta:
        ordering = ("validity_start", )
示例#5
0
文件: models.py 项目: uktrade/tamato
class GeographicalArea(TrackedModel, ValidityMixin, DescribedMixin):
    """
    A Geographical Area covers three distinct types of object:

        1) A Country
        2) A Region (a trading area which is not recognised as a country)
        3) A Grouping of the above

    These objects are generally used when linked to data structures such as measures.
    As a measure does not care to distinguish between a country, region or group,
    the 3 types are stored as one for relational purposes.

    As a country or region can belong to a group there is a self-referential many-to-many
    field which is restricted. Yet groups can also have parent groups - in which case
    measures must of the parent must also apply to a child. To accomodate this there is a
    separate foreign key for group to group relations.
    """

    record_code = "250"
    subrecord_code = "00"

    identifying_fields = ("sid", )

    url_pattern_name_prefix = "geo_area"

    sid = SignedIntSID(db_index=True)
    area_id = models.CharField(max_length=4, validators=[area_id_validator])
    area_code = models.PositiveSmallIntegerField(choices=AreaCode.choices)

    # This deals with countries and regions belonging to area groups
    memberships = models.ManyToManyField("self",
                                         through="GeographicalMembership")

    # This deals with subgroups of other groups
    parent = models.ForeignKey("self",
                               on_delete=models.PROTECT,
                               null=True,
                               blank=True)

    objects = PolymorphicManager.from_queryset(GeographicalAreaQuerySet)()

    indirect_business_rules = (
        business_rules.GA14,
        business_rules.GA16,
        business_rules.GA17,
        measures_business_rules.ME1,
        measures_business_rules.ME65,
        measures_business_rules.ME66,
        measures_business_rules.ME67,
        quotas_business_rules.ON13,
        quotas_business_rules.ON14,
        quotas_business_rules.ON6,
    )
    business_rules = (
        business_rules.GA1,
        business_rules.GA3,
        business_rules.GA4,
        business_rules.GA5,
        business_rules.GA6,
        business_rules.GA7,
        business_rules.GA10,
        business_rules.GA11,
        business_rules.GA21,
        business_rules.GA22,
        UniqueIdentifyingFields,
        UpdateValidity,
    )

    def get_current_memberships(self):
        return (GeographicalMembership.objects.filter(
            Q(geo_group__sid=self.sid)
            | Q(member__sid=self.sid), ).current().select_related(
                "member", "geo_group"))

    def is_single_region_or_country(self):
        return self.area_code == AreaCode.COUNTRY or self.area_code == AreaCode.REGION

    def is_all_countries(self):
        return self.area_code == AreaCode.GROUP and self.area_id == "1011"

    def is_group(self):
        return self.area_code == AreaCode.GROUP

    def __str__(self):
        return f"{self.get_area_code_display()} {self.area_id}"

    class Meta:
        constraints = (CheckConstraint(
            name="only_groups_have_parents",
            check=Q(area_code=1) | Q(parent__isnull=True),
        ), )
示例#6
0
class TrackedModel(PolymorphicModel):
    transaction = models.ForeignKey(
        "common.Transaction",
        on_delete=models.PROTECT,
        related_name="tracked_models",
        editable=False,
    )

    update_type: validators.UpdateType = models.PositiveSmallIntegerField(
        choices=validators.UpdateType.choices,
        db_index=True,
    )
    """
    The change that was made to the model when this version of the model was
    authored.

    The first version should always have :data:`~validators.UpdateType.CREATE`,
    subsequent versions will have :data:`~validators.UpdateType.UPDATE` and the
    final version will have :data:`~validators.UpdateType.DELETE`. Deleted
    models that reappear for the same :attr:`identifying_fields` will have a new
    :attr:`version_group` created.
    """

    version_group = models.ForeignKey(
        VersionGroup,
        on_delete=models.PROTECT,
        related_name="versions",
    )
    """
    Each version group contains all of the versions of the same logical model.

    When a new version of a model is authored (e.g. to
    :data:`~validators.UpdateType.DELETE` it) a new model row is created and
    added to the same version group as the existing model being changed.

    Models are identified logically by their :attr:`identifying_fields`, so
    within one version group all of the models should have the same values for
    these fields.
    """

    objects: TrackedModelQuerySet = PolymorphicManager.from_queryset(
        TrackedModelQuerySet,
    )()

    business_rules: Iterable = ()
    indirect_business_rules: Iterable = ()

    record_code: int
    """
    The type id of this model's type family in the TARIC specification.

    This number groups together a number of different models into 'records'.
    Where two models share a record code, they are conceptually expressing
    different properties of the same logical model.

    In theory each :class:`~common.transactions.Transaction` should only contain
    models with a single :attr:`record_code` (but differing
    :attr:`subrecord_code`.)
    """

    subrecord_code: int
    """
    The type id of this model in the TARIC specification. The
    :attr:`subrecord_code` when combined with the :attr:`record_code` uniquely
    identifies the type within the specification.

    The subrecord code gives the intended order for models in a transaction,
    with comparatively smaller subrecord codes needing to come before larger
    ones.
    """

    identifying_fields: Iterable[str] = ("sid",)
    """
    The fields which together form a composite unique key for each model.

    The system ID (or SID) field is normally the unique identifier of a TARIC
    model, but in places where this does not exist models can declare their own.
    (Note that because mutliple versions of each model will exist this does not
    actually equate to a ``UNIQUE`` constraint in the database.)
    """

    taric_template = None

    def get_taric_template(self):
        """
        Generate a TARIC XML template name for the given class.

        Any TrackedModel must be representable via a TARIC compatible XML
        record.
        """

        if self.taric_template:
            return self.taric_template
        class_name = self.__class__.__name__

        # replace namesLikeThis to names_Like_This
        name = re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", class_name)
        # replace names_LIKEthis to names_like_this
        name = re.sub(r"([A-Z]{2,})([a-z0-9_])", r"\1_\2", name).lower()

        template_name = f"taric/{name}.xml"
        try:
            loader.get_template(template_name)
        except loader.TemplateDoesNotExist as e:
            raise loader.TemplateDoesNotExist(
                f"Taric template does not exist for {class_name}. All classes that "
                "inherit TrackedModel must either:\n"
                "    1) Have a matching taric template with a snake_case name matching "
                'the class at "taric/{snake_case_class_name}.xml". In this case it '
                f'should be: "{template_name}".\n'
                "    2) A taric_template attribute, pointing to the correct template.\n"
                "    3) Override the get_taric_template method, returning an existing "
                "template.",
            ) from e

        return template_name

    def new_draft(self, workbasket, save=True, **kwargs):
        cls = self.__class__

        new_object_kwargs = {
            field.name: getattr(self, field.name)
            for field in self._meta.fields
            if field.name
            not in (
                self._meta.pk.name,
                "transaction",
                "polymorphic_ctype",
                "trackedmodel_ptr",
                "id",
            )
        }

        new_object_kwargs["update_type"] = validators.UpdateType.UPDATE
        new_object_kwargs.update(kwargs)

        if "transaction" not in new_object_kwargs:
            # Only create a transaction if the user didn't specify one.
            new_object_kwargs["transaction"] = workbasket.new_transaction()

        new_object = cls(**new_object_kwargs)

        if save:
            new_object.save()

        return new_object

    def get_versions(self):
        if hasattr(self, "version_group"):
            return self.version_group.versions.all()
        query = Q(**self.get_identifying_fields())
        return self.__class__.objects.filter(query)

    def get_description(self):
        return self.get_descriptions().last()

    def get_descriptions(self, transaction=None) -> TrackedModelQuerySet:
        """
        Get the latest descriptions related to this instance of the Tracked
        Model.

        If there is no Description relation existing a `NoDescriptionError` is raised.

        If a transaction is provided then all latest descriptions that are either approved
        or in the workbasket of the transaction up to the transaction will be provided.
        """
        try:
            descriptions_model = self.descriptions.model
        except AttributeError as e:
            raise NoDescriptionError(
                f"Model {self.__class__.__name__} has no descriptions relation.",
            ) from e

        for field, model in descriptions_model.get_relations():
            if isinstance(self, model):
                field_name = field.name
                break
        else:
            raise NoDescriptionError(
                f"No foreign key back to model {self.__class__.__name__} "
                f"found on description model {descriptions_model.__name__}.",
            )

        filter_kwargs = {
            f"{field_name}__{key}": value
            for key, value in self.get_identifying_fields().items()
        }

        query = descriptions_model.objects.filter(**filter_kwargs).order_by(
            "valid_between",
        )

        if transaction:
            return query.approved_up_to_transaction(transaction=transaction)

        return query.latest_approved()

    def identifying_fields_unique(
        self,
        identifying_fields: Optional[Iterable[str]] = None,
    ) -> bool:
        return (
            self.__class__.objects.filter(
                **self.get_identifying_fields(identifying_fields)
            )
            .latest_approved()
            .count()
            <= 1
        )

    def identifying_fields_to_string(
        self,
        identifying_fields: Optional[Iterable[str]] = None,
    ) -> str:
        field_list = [
            f"{field}={str(value)}"
            for field, value in self.get_identifying_fields(identifying_fields).items()
        ]

        return ", ".join(field_list)

    def _get_version_group(self) -> VersionGroup:
        if self.update_type == validators.UpdateType.CREATE:
            return VersionGroup.objects.create()
        return self.get_versions().latest_approved().last().version_group

    def _can_write(self):
        return not (
            self.pk
            and self.transaction.workbasket.status in WorkflowStatus.approved_statuses()
        )

    def get_identifying_fields(
        self,
        identifying_fields: Optional[Iterable[str]] = None,
    ) -> dict[str, Any]:
        identifying_fields = identifying_fields or self.identifying_fields
        fields = {}

        for field in identifying_fields:
            value = self
            for layer in field.split("__"):
                value = getattr(value, layer)
                if value is None:
                    break
            fields[field] = value

        return fields

    @property
    def structure_code(self):
        return str(self)

    @property
    def structure_description(self):
        description = None
        if hasattr(self, "descriptions"):
            description = self.get_descriptions().last()
            if description:
                # Get the actual description, not just the object
                description = description.description
        if hasattr(self, "description"):
            description = self.description
        return description or "-"

    @property
    def current_version(self) -> TrackedModel:
        current_version = self.version_group.current_version
        if current_version is None:
            raise self.__class__.DoesNotExist("Object has no current version")
        return current_version

    @classmethod
    def get_relations(cls) -> list[tuple[Field, type[TrackedModel]]]:
        """Find all foreign key and one-to-one relations on an object and return
        a list containing tuples of the field instance and the related model it
        links to."""
        return [
            (f, f.related_model)
            for f in cls._meta.get_fields()
            if (f.many_to_one or f.one_to_one)
            and not f.auto_created
            and f.concrete
            and f.model == cls
            and issubclass(f.related_model, TrackedModel)
        ]

    def __getattr__(self, item: str):
        """
        Add the ability to get the current instance of a related object through
        an attribute. For example if a model is like so:

        .. code:: python
            class ExampleModel(TrackedModel):
                # must be a TrackedModel
                other_model = models.ForeignKey(OtherModel, on_delete=models.PROTECT)

        The latest version of the relation can be accessed via:

        .. code:: python
            example_model = ExampleModel.objects.first()
            example_model.other_model_current  # Gets the latest version
        """

        if item.endswith("_current"):
            field_name = item[:-8]
            if field_name in [field.name for field, _ in self.get_relations()]:
                return getattr(self, field_name).current_version

        return self.__getattribute__(item)

    @atomic
    def save(self, *args, force_write=False, **kwargs):
        if not force_write and not self._can_write():
            raise IllegalSaveError(
                "TrackedModels cannot be updated once written and approved. "
                "If writing a new row, use `.new_draft` instead",
            )

        if not hasattr(self, "version_group"):
            self.version_group = self._get_version_group()

        return_value = super().save(*args, **kwargs)

        if self.transaction.workbasket.status in WorkflowStatus.approved_statuses():
            self.version_group.current_version = self
            self.version_group.save()

        return return_value

    def __str__(self):
        return ", ".join(
            f"{field}={getattr(self, field, None)}" for field in self.identifying_fields
        )

    def __hash__(self):
        return hash(f"{__name__}.{self.__class__.__name__}")

    def get_url(self, action="detail"):
        kwargs = {}
        if action != "list":
            kwargs = self.get_identifying_fields()
        try:
            return reverse(
                f"{self.get_url_pattern_name_prefix()}-ui-{action}",
                kwargs=kwargs,
            )
        except NoReverseMatch:
            return

    def get_url_pattern_name_prefix(self):
        prefix = getattr(self, "url_pattern_name_prefix", None)
        if not prefix:
            prefix = self._meta.verbose_name.replace(" ", "_")
        return prefix
示例#7
0
class BaseAccount(ShowFieldTypeAndContent, PolymorphicModel):
    user = models.ForeignKey(settings.AUTH_USER_MODEL,
                             null=True,
                             on_delete=models.CASCADE,
                             related_name='accounts_for_user')
    type = models.CharField(max_length=100)
    account_id = models.CharField(max_length=100)
    taxable = models.BooleanField(default=True)
    display_name = models.CharField(max_length=100, default='')
    joint_share = models.DecimalField(default=1,
                                      decimal_places=3,
                                      max_digits=6)
    creation_date = models.DateField(default='2009-01-01')

    objects = PolymorphicManager.from_queryset(BaseAccountQuerySet)()

    activitySyncDateRange = 30

    class Meta:
        ordering = ['account_id']

    def __repr__(self):
        return "BaseAccount({},{},{})".format(self.user, self.account_id,
                                              self.type)

    def __str__(self):
        return self.display_name

    @cached_property
    def cur_cash_balance(self):
        query = self.holdingdetail_set.cash().today().total_values()
        if query:
            return query.first()[1]
        return 0

    @cached_property
    def cur_balance(self):
        return self.GetValueToday()

    @cached_property
    def yesterday_balance(self):
        return self.GetValueAtDate(datetime.date.today() -
                                   datetime.timedelta(days=1))

    @cached_property
    def today_balance_change(self):
        return self.cur_balance - self.yesterday_balance

    @cached_property
    def sync_from_date(self):
        last_activity = self.activities.newest_date()
        if last_activity:
            return last_activity + datetime.timedelta(days=1)
        return self.creation_date

    def import_activities(self, csv_file):
        activity_count = self.activities.all().count()

        self.import_from_csv(csv_file)
        if self.activities.all().count() > activity_count:
            self.RegenerateHoldings()

        Security.objects.Sync(False)

    def import_from_csv(self, csv_file):
        """
        Override this to enable transaction uploading from csv.
        Subclasses are expected to parse the csv and create the necessary BaseRawActivity subclasses.
        """
        pass

    def SyncAndRegenerate(self):
        activity_count = self.activities.all().count()

        if self.activitySyncDateRange:
            date_range = utils.dates.day_intervals(self.activitySyncDateRange,
                                                   self.sync_from_date)
            print('Syncing all activities for {} in {} chunks.'.format(
                self, len(date_range)))

            for period in date_range:
                self.CreateActivities(period.start, period.end)

        if self.activities.all().count() > activity_count:
            self.RegenerateHoldings()

    def RegenerateActivities(self):
        self.activities.all().delete()
        with transaction.atomic():
            for raw in self.rawactivities.all():
                raw.CreateActivity()
        self.RegenerateHoldings()

    def RegenerateHoldings(self):
        self.holding_set.all().delete()
        for activity in self.activities.all():
            for security, qty_delta in activity.GetHoldingEffects().items():
                self.holding_set.add_effect(self, security, qty_delta,
                                            activity.trade_date)
        self.holding_set.filter(qty=0).delete()

    def CreateActivities(self, start, end):
        """
        Retrieve raw activity data for the specified account and start/end period.
        Store it in the DB as a subclass of BaseRawActivity.
        Return the newly created raw instances.
        """
        return []

    def GetValueAtDate(self, date):
        result = self.holdingdetail_set.at_date(date).total_values().first()
        if result:
            return result[1]
        return 0

    def GetValueToday(self):
        return self.GetValueAtDate(datetime.date.today())
示例#8
0
class MeasureCondition(TrackedModel):
    """
    A measure may be dependent on conditions.

    These are expressed in a series of conditions, each having zero or more
    components. Conditions for the same condition type will have sequence
    numbers. Conditions of different types may be combined.
    """

    record_code = "430"
    subrecord_code = "10"

    sid = SignedIntSID(db_index=True)
    dependent_measure = models.ForeignKey(
        Measure,
        on_delete=models.PROTECT,
        related_name="conditions",
    )
    condition_code = models.ForeignKey(
        MeasureConditionCode,
        on_delete=models.PROTECT,
        related_name="conditions",
    )
    component_sequence_number = models.PositiveSmallIntegerField(
        validators=[validators.validate_component_sequence_number],
    )
    duty_amount = models.DecimalField(
        max_digits=10,
        decimal_places=3,
        null=True,
        blank=True,
    )
    monetary_unit = models.ForeignKey(
        MonetaryUnit,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )
    condition_measurement = models.ForeignKey(
        Measurement,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )
    action = models.ForeignKey(
        MeasureAction,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )
    required_certificate = models.ForeignKey(
        "certificates.Certificate",
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )

    objects = PolymorphicManager.from_queryset(MeasureConditionQuerySet)()

    indirect_business_rules = (
        business_rules.MA2,
        business_rules.MC4,
        business_rules.ME53,
    )
    business_rules = (
        business_rules.MC3,
        business_rules.MA4,
        business_rules.ME56,
        business_rules.ME57,
        business_rules.ME58,
        business_rules.ME59,
        business_rules.ME60,
        business_rules.ME61,
        business_rules.ME62,
        business_rules.ME63,
        business_rules.ME64,
    )

    class Meta:
        ordering = [
            "dependent_measure__sid",
            "condition_code__code",
            "component_sequence_number",
        ]

    def is_certificate_required(self):
        return self.condition_code.code in ("A", "B", "C", "H", "Q", "Y", "Z")

    @property
    def description(self) -> str:
        out: list[str] = []

        out.append(
            f"Condition of type {self.condition_code.code} - {self.condition_code.description}",
        )

        if self.required_certificate:
            out.append(
                f"On presentation of certificate {self.required_certificate.code},",
            )
        elif self.is_certificate_required():
            out.append("On presentation of no certificate,")

        if self.reference_price_string:
            out.append(f"If reference price > {self.reference_price_string},")

        out.append(f"perform action {self.action.code} - {self.action.description}")

        if self.condition_string:
            out.append(f"\n\nApplicable duty is {self.condition_string}")

        return " ".join(out)

    @property
    def condition_string(self) -> str:
        out: list[str] = []

        components = self.components.latest_approved()
        measures: set[str] = set()
        measure_types: set[str] = set()
        additional_codes: set[str] = set()

        for mcc in components:
            measures.add(mcc.condition.dependent_measure.sid)
            measure_types.add(mcc.condition.dependent_measure.measure_type.sid)
            if mcc.condition.dependent_measure.additional_code:
                additional_codes.add(
                    mcc.condition.dependent_measure.additional_code.sid,
                )

        if (
            len(measures) == len(measure_types) == len(additional_codes) == 1
            or len(measure_types) > 1
            or len(additional_codes) > 1
        ):
            out.append(self.duty_sentence)

        return "".join(out)
示例#9
0
class Measure(TrackedModel, ValidityMixin):
    """
    Defines the validity period in which a particular measure type is applicable
    to particular nomenclature for a particular geographical area.

    Measures in the TARIC database are stored against the nomenclature code
    which is at the highest level appropriate in the hierarchy. Thus, measures
    which apply to all the declarable codes in a complete chapter are stored
    against the nomenclature code for the chapter (i.e. at the 2-digit level
    only); those which apply to all sub-divisions of an HS code are stored
    against that HS code (i.e. at the 6-digit level only). The advantage of this
    system is that it reduces the number of measures stored in the database; the
    data capture workload (thus diminishing the possibility of introducing
    errors) and the transmission volumes.
    """

    record_code = "430"
    subrecord_code = "00"

    sid = SignedIntSID(db_index=True)
    measure_type = models.ForeignKey(MeasureType, on_delete=models.PROTECT)
    geographical_area = models.ForeignKey(
        "geo_areas.GeographicalArea",
        on_delete=models.PROTECT,
        related_name="measures",
    )
    goods_nomenclature = models.ForeignKey(
        "commodities.GoodsNomenclature",
        on_delete=models.PROTECT,
        related_name="measures",
        null=True,
        blank=True,
    )
    additional_code = models.ForeignKey(
        "additional_codes.AdditionalCode",
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )
    dead_additional_code = models.CharField(
        max_length=16,
        null=True,
        blank=True,
        db_index=True,
    )
    order_number = models.ForeignKey(
        "quotas.QuotaOrderNumber",
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )
    dead_order_number = models.CharField(
        max_length=6,
        validators=[quota_order_number_validator],
        null=True,
        blank=True,
        db_index=True,
    )
    reduction = models.PositiveSmallIntegerField(
        validators=[validators.validate_reduction_indicator],
        null=True,
        blank=True,
        db_index=True,
    )
    generating_regulation = models.ForeignKey(
        "regulations.Regulation",
        on_delete=models.PROTECT,
    )
    terminating_regulation = models.ForeignKey(
        "regulations.Regulation",
        on_delete=models.PROTECT,
        related_name="terminated_measures",
        null=True,
        blank=True,
    )
    stopped = models.BooleanField(default=False)
    export_refund_nomenclature_sid = SignedIntSID(null=True, blank=True, default=None)

    footnotes = models.ManyToManyField(
        "footnotes.Footnote",
        through="FootnoteAssociationMeasure",
    )

    identifying_fields = ("sid",)

    indirect_business_rules = (
        business_rules.MA4,
        business_rules.MC3,
        business_rules.ME42,
        business_rules.ME49,
        business_rules.ME61,
        business_rules.ME65,
        business_rules.ME66,
        business_rules.ME67,
        business_rules.ME71,
        business_rules.ME73,
    )
    business_rules = (
        business_rules.ME1,
        business_rules.ME2,
        business_rules.ME3,
        business_rules.ME4,
        business_rules.ME5,
        business_rules.ME6,
        business_rules.ME7,
        business_rules.ME8,
        business_rules.ME88,
        business_rules.ME16,
        business_rules.ME115,
        business_rules.ME25,
        business_rules.ME32,
        business_rules.ME10,
        business_rules.ME116,
        business_rules.ME119,
        business_rules.ME9,
        business_rules.ME12,
        business_rules.ME17,
        business_rules.ME24,
        business_rules.ME87,
        business_rules.ME33,
        business_rules.ME34,
        business_rules.ME40,
        business_rules.ME45,
        business_rules.ME46,
        business_rules.ME47,
        business_rules.ME109,
        business_rules.ME110,
        business_rules.ME111,
        business_rules.ME104,
    )

    objects = PolymorphicManager.from_queryset(MeasuresQuerySet)()

    validity_field_name = "db_effective_valid_between"

    @property
    def effective_end_date(self):
        """Measure end dates may be overridden by regulations."""

        # UK measures will have explicit end dates only
        # if self.national:
        #     return self.valid_between.upper

        reg = self.generating_regulation
        effective_end_date = (
            date(
                reg.effective_end_date.year,
                reg.effective_end_date.month,
                reg.effective_end_date.day,
            )
            if reg.effective_end_date
            else None
        )

        if self.valid_between.upper and reg and effective_end_date:
            if self.valid_between.upper > effective_end_date:
                return effective_end_date
            return self.valid_between.upper

        if self.valid_between.upper and self.terminating_regulation:
            return self.valid_between.upper

        if reg:
            return effective_end_date

        return self.valid_between.upper

    @property
    def effective_valid_between(self):
        return TaricDateRange(self.valid_between.lower, self.effective_end_date)

    @classmethod
    def objects_with_validity_field(cls):
        return super().objects_with_validity_field().with_effective_valid_between()

    def has_components(self):
        return (
            MeasureComponent.objects.approved_up_to_transaction(
                transaction=self.transaction,
            )
            .filter(component_measure__sid=self.sid)
            .exists()
        )

    def has_condition_components(self):
        return (
            MeasureConditionComponent.objects.approved_up_to_transaction(
                transaction=self.transaction,
            )
            .filter(condition__dependent_measure__sid=self.sid)
            .exists()
        )

    def get_conditions(self):
        return MeasureCondition.objects.filter(
            dependent_measure__sid=self.sid,
        ).latest_approved()

    def terminate(self, workbasket, when: date):
        """
        Returns a new version of the measure updated to end on the specified
        date.

        If the measure would not have started on that date, the measure is
        deleted instead. If the measure will already have ended by this date,
        then does nothing.
        """
        starts_after_date = self.valid_between.lower >= when
        ends_before_date = (
            not self.valid_between.upper_inf and self.valid_between.upper < when
        )

        if ends_before_date:
            return self

        update_params = {}
        if starts_after_date:
            update_params["update_type"] = UpdateType.DELETE
        else:
            update_params["update_type"] = UpdateType.UPDATE
            update_params["valid_between"] = TaricDateRange(
                lower=self.valid_between.lower,
                upper=when,
            )
            if not self.terminating_regulation:
                update_params["terminating_regulation"] = self.generating_regulation

        return self.new_draft(workbasket, **update_params)