Exemplo n.º 1
0
class MeasureConditionComponent(TrackedModel):
    """Contains the duty information or part of the duty information of the
    measure condition."""

    record_code = "430"
    subrecord_code = "11"

    condition = models.ForeignKey(
        MeasureCondition,
        on_delete=models.PROTECT,
        related_name="components",
    )
    duty_expression = models.ForeignKey("DutyExpression",
                                        on_delete=models.PROTECT)
    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,
    )
    component_measurement = models.ForeignKey(
        Measurement,
        on_delete=models.PROTECT,
        null=True,
        blank=True,
    )

    identifying_fields = ("condition__sid", "duty_expression__sid")

    class Meta:
        ordering = [
            "condition__sid",
            "duty_expression__sid",
        ]

    indirect_business_rules = (
        business_rules.ME109,
        business_rules.ME110,
        business_rules.ME111,
        business_rules.ME40,
    )
    business_rules = (
        business_rules.ME53,
        business_rules.ME105,
        business_rules.ME106,
        business_rules.ME108,
        UpdateValidity,
    )

    objects = TrackedModelManager.from_queryset(ComponentQuerySet)()
Exemplo n.º 2
0
class DescriptionMixin(ValidityStartMixin):
    objects = TrackedModelManager.from_queryset(DescriptionQueryset)()

    business_rules = (
        NoBlankDescription,
        UniqueIdentifyingFields,
        UpdateValidity,
    )

    @classproperty
    def described_object_field(cls) -> Field:
        for rel in get_relations(cls).keys():
            if rel.name.startswith("described_"):
                return rel
        raise TypeError(f"{cls} should have a described field.")

    @classproperty
    def validity_over(cls):
        return cls.described_object_field.name

    def get_described_object(self):
        return getattr(self, self.described_object_field.name)

    def get_url(self, action="detail"):
        kwargs = {}
        if action != "list":
            kwargs = self.get_identifying_fields()
            described_object = self.get_described_object()
            if action == "detail":
                url = described_object.get_url()
                if url:
                    return url + "#descriptions"
                return

            for field, value in described_object.get_identifying_fields(
            ).items():
                kwargs[f"{self.described_object_field.name}__{field}"] = value
        try:
            return reverse(
                f"{self.get_url_pattern_name_prefix()}-ui-{action}",
                kwargs=kwargs,
            )
        except NoReverseMatch:
            return

    def __str__(self):
        return self.identifying_fields_to_string(identifying_fields=(
            self.described_object_field.name,
            "validity_start",
        ), )

    class Meta:
        abstract = True
Exemplo n.º 3
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: VersionGroup = 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 = TrackedModelManager.from_queryset(
        TrackedModelQuerySet, )()
    current_objects: TrackedModelQuerySet = CurrentTrackedModelManager.from_queryset(
        TrackedModelQuerySet, )()
    """
    The `current_objects` model manager provides a default queryset that, by
    default, filters to the 'current' transaction.
    """

    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: Sequence[str] = ("pk", )
    """
    The fields which together form a composite unique key for each model.

    The system ID (or SID) field, 'sid' 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 multiple versions of each model will exist this does not
    actually equate to a ``UNIQUE`` constraint in the database.)

    TrackedModel itself defaults to ("pk",) as it does not have an SID.
    """

    url_suffix = ""
    """
    This is to add a link within a page for get_url() e.g. for linking to a
    Measure's conditions tab. If url_suffix is set to '#conditions' the output
    detail url will be /measures/12345678/#conditions
    """
    def new_version(
        self: Cls,
        workbasket,
        transaction=None,
        update_type: UpdateType = UpdateType.UPDATE,
        **overrides,
    ) -> Cls:
        """
        Create and return a new version of the object. Callers can override
        existing data by passing in keyword args.

        The new version is added to a transaction which is created and added to the passed in workbasket
        (or may be supplied as a keyword arg).

        `update_type` must be UPDATE or DELETE, with UPDATE as the default.
        """
        if update_type not in (
                validators.UpdateType.UPDATE,
                validators.UpdateType.DELETE,
        ):
            raise ValueError("update_type must be UPDATE or DELETE")

        cls = self.__class__

        new_object_kwargs = {
            field.name: getattr(self, field.name)
            for field in self._meta.fields
            if field is self._meta.get_field("version_group")
            or field.name not in self.system_set_field_names
        }

        new_object_overrides = {
            name: value
            for name, value in overrides.items()
            if name not in [f.name for f in get_deferred_set_fields(self)]
        }

        new_object_kwargs["update_type"] = update_type
        new_object_kwargs.update(new_object_overrides)

        if transaction is None:
            transaction = workbasket.new_transaction()
        new_object_kwargs["transaction"] = transaction

        new_object = cls(**new_object_kwargs)
        new_object.save()

        deferred_kwargs = {
            field.name: field.value_from_object(self)
            for field in get_deferred_set_fields(self)
        }
        deferred_overrides = {
            name: value
            for name, value in overrides.items()
            if name in [f.name for f in get_deferred_set_fields(self)]
        }
        deferred_kwargs.update(deferred_overrides)
        for field in deferred_kwargs:
            getattr(new_object, field).set(deferred_kwargs[field])

        return new_object

    def get_versions(self):
        """Find all versions of this model."""
        if hasattr(self, "version_group"):
            query = Q(version_group_id=self.version_group_id)
        else:
            query = Q(**self.get_identifying_fields())
        return type(self).objects.filter(query)

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

        latest_version = self.get_versions().latest_approved().last()

        if not latest_version:
            # An object may be created and deleted/updated in the same workbasket.
            # If the workbasket status is not WorkflowStatus.PUBLISHED,
            # then latest_approved() in the above line of code will return None.
            # Trying to get the version group off that will throw an exception.
            # The extra bit of logic below deals with such cases
            # It will attempt to find the corresponding CREATE record
            # in the current workbasket and return that as the latest_version.
            try:
                latest_version = [
                    record for transaction in
                    self.transaction.workbasket.transactions.all()
                    for record in transaction.tracked_models.all()
                    if type(record) == type(self)
                    if record.update_type == UpdateType.CREATE
                    if record.get_identifying_fields() ==
                    self.get_identifying_fields()
                ][0]
            except IndexError:
                return

        return latest_version.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]:
        """
        Get a name/value mapping of the fields that identify this model.

        :param identifying_fields Optional[Iterable[str]]: Optionally override
            the fields to retrieve
        :rtype dict[str, Any]: A dict of field names to values
        """

        identifying_fields = identifying_fields or self.identifying_fields
        fields = {}

        for field in identifying_fields:
            _, fields[field] = get_field_tuple(self, field)

        return fields

    def identifying_fields_to_string(
        self,
        identifying_fields: Optional[Iterable[str]] = None,
    ) -> str:
        """
        Constructs a comma separated string of the identifying fields of the
        model with field name and value pairs delimited by "=", eg: "field1=1,
        field2=2".

        :param identifying_fields: Optionally override the fields to use in the
            string
        :rtype str: The constructed string
        """
        field_list = [
            f"{field}={str(value)}" for field, value in
            self.get_identifying_fields(identifying_fields).items()
        ]

        return ", ".join(field_list)

    @property
    def structure_code(self):
        """
        A string used to describe the model instance.

        Used as the displayed value in an AutocompleteWidget dropdown, and in
        the "Your tariff changes" list.
        """
        return str(self)

    @property
    def structure_description(self) -> Optional[str]:
        """
        The current description of the model, if it has related description
        models or a description field.

        :rtype Optional[str]: The current description
        """
        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 None

    @property
    def record_identifier(self) -> str:
        """Returns the record identifier as defined in TARIC3 records
        specification."""
        return f"{self.record_code}{self.subrecord_code}"

    @property
    def update_type_str(self) -> str:
        return dict(UpdateType.choices)[self.update_type]

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

    def version_at(self: Cls, transaction) -> Cls:
        """
        The latest version of this model that was approved as of the given
        transaction.

        :param transaction Transaction: Limit versions to this transaction
        :rtype TrackedModel:
        """
        return self.get_versions().approved_up_to_transaction(
            transaction).get()

    @classproperty
    def copyable_fields(cls):
        """
        Return the set of fields that can have their values copied from one
        model to another. This is anything that is:

        - a native value
        - a foreign key to some other model
        """
        return {
            field
            for field in cls._meta.get_fields()
            if not any((field.many_to_many, field.one_to_many))
            and field.name not in cls.system_set_field_names
        }

    _meta: Options

    @classproperty
    def auto_value_fields(cls) -> Set[Field]:
        """Returns the set of fields on this model that should have their value
        set automatically on save, excluding any primary keys."""
        return {
            f
            for f in cls._meta.get_fields()
            if isinstance(f, (SignedIntSID, NumericSID))
        }

    # Fields that we don't want to copy from one object to a new one, either
    # because they will be set by the system automatically or because we will
    # always want to override them.
    system_set_field_names = {
        "is_current",
        "version_group",
        "polymorphic_ctype",
        "id",
        "update_type",
        "trackedmodel_ptr",
        "transaction",
    }

    def copy(
        self: Cls,
        transaction,
        **overrides: Any,
    ) -> Cls:
        """
        Create a copy of the model as a new logical domain object – i.e. with a
        new version group, new SID (if present) and update type of CREATE.

        Any dependent models that are TARIC subrecords of this model will be
        copied as well. Any many-to-many relationships will also be duplicated
        if they do not have an explicit through model. Any overrides passed in
        as keyword arguments will be applied to the new model. If the model uses
        SIDs, they will be automatically set to the next highest available SID.
        Models with other identifying fields should have thier new IDs passed in
        through overrides.
        """

        # Remove any fields from the basic data that are overriden, because
        # otherwise when we convert foreign keys to IDs (below) Django will
        # ignore the object from the overrides and just take the ID from the
        # basic data.
        basic_fields = self.copyable_fields
        subrecord_fields = {}
        for field_name in overrides:
            field = None
            # Check for fields on related model
            if not "__" in field_name:
                field = self._meta.get_field(field_name)
            # Check for non-basic fields e.g. related models
            if field and field in basic_fields:
                basic_fields.remove(field)
            # Add non-basic fields from overrides to subrecord_fields dict
            else:
                subrecord_fields.update({field_name: overrides[field_name]})

        # Remove related models and related model fields from overrides before generating object_data below
        overrides = {
            k: v
            for (k, v) in overrides.items()
            if k not in subrecord_fields.keys()
        }

        # Remove any SIDs from the copied data. This allows them to either
        # automatically pick the next highest value or to be passed in.
        for field in self.auto_value_fields:
            basic_fields.remove(field)

        # Build the dictionary of data that the new model will have. Convert any
        # foreign keys into ids because ``value_from_object`` returns PKs.
        model_data = {
            f.name + ("_id" if any((f.many_to_one, f.one_to_one)) else ""):
            f.value_from_object(self)
            for f in basic_fields
        }

        new_object_data = {
            **model_data,
            "transaction": transaction,
            "update_type": validators.UpdateType.CREATE,
            **overrides,
        }

        new_object = type(self).objects.create(**new_object_data)

        # Now copy any many-to-many fields with an auto-created through model.
        # These must be handled after creation of the new model. We only need to
        # do this for auto-created models because others will be handled below.
        for field in get_deferred_set_fields(self):
            getattr(new_object, field.name).set(field.value_from_object(self))

        # Now go and create copies of all of the models that reference this one
        # with a foreign key that are part of the same record family. Find all
        # of the related models and then recursively call copy on them, but with
        # the new model substituted in place of this one. It's done this way to
        # give these related models a chance to increment SIDs, etc.
        for field in get_subrecord_relations(self.__class__):
            ignore = False
            # Check if user passed related model into overrides argument
            if field.name in subrecord_fields.keys():
                # If user passed a new unsaved model, set the remote field value equal to new_object for each model passed
                # e.g. if a Measure is copied and a MeasureCondition is passed, update `dependent_measure` field to `new_object`
                if subrecord_fields[field.name]:
                    for subrecord in subrecord_fields[field.name]:
                        remote_field = [
                            f for f in self._meta.get_fields()
                            if f.name == field.name
                        ][0].remote_field.name
                        if not subrecord.pk:
                            setattr(subrecord, remote_field, new_object)
                            subrecord.save()
                        else:
                            # If user passed a saved object, create a copy of that object with remote_field pointing at the new copied object
                            # set ignore to True, so that duplicate copies are not made below
                            subrecord.copy(transaction,
                                           **{remote_field: new_object})
                            ignore = True
                # Else, if an empty or None value is passed, set ignore to True, so that related models are not copied
                # e.g. if an existing Measure with two conditions is copied with conditions=[], the copy will have no conditions
                else:
                    ignore = True

            queryset = getattr(self, field.get_accessor_name())
            reverse_field_name = field.field.name
            kwargs = {reverse_field_name: new_object}
            nested_fields = {
                k.split("__", 1)[1]: v
                for (k, v) in subrecord_fields.items()
                if field.name in k and field.name != k
            }
            kwargs.update(nested_fields)

            if not ignore:
                for model in queryset.approved_up_to_transaction(transaction):
                    model.copy(transaction, **kwargs)

        return new_object

    def in_use_by(self,
                  via_relation: str,
                  transaction=None) -> QuerySet[TrackedModel]:
        """
        Returns all of the models that are referencing this one via the
        specified relation and exist as of the passed transaction.

        ``via_relation`` should be the name of a relation, and a ``KeyError``
        will be raised if the relation name is not valid for this model.
        Relations are accessible via get_relations helper method.
        """
        relation = {r.name: r
                    for r in get_relations(self.__class__).keys()
                    }[via_relation]
        remote_model = relation.remote_field.model
        remote_field_name = get_accessor(relation.remote_field)

        return remote_model.objects.filter(
            **{
                f"{remote_field_name}__version_group": self.version_group
            }).approved_up_to_transaction(transaction)

    def in_use(self, transaction=None, *relations: str) -> bool:
        """
        Returns True if there are any models that are using this one as of the
        specified transaction.

        This can be any model this model is related to, but ignoring any
        subrecords (because e.g. a footnote is not considered "in use by" its
        own description) and then filtering for only things that link _to_ this
        model.

        The list of relations can be filtered by passing in the name of a
        relation. If a name is passed in that does not refer to a relation on
        this model, ``ValueError`` will be raised.
        """
        # Get the list of models that use models of this type.
        class_ = self.__class__
        using_models = set(
            relation.name
            for relation in (get_relations(class_).keys() -
                             get_subrecord_relations(class_) -
                             get_models_linked_to(class_).keys()))

        # If the user has specified names, check that they are sane
        # and then filter the relations to them,
        if relations:
            bad_names = set(relations) - set(using_models)
            if any(bad_names):
                raise ValueError(
                    f"{bad_names} are unknown relations; use one of {using_models}",
                )

            using_models = {
                relation
                for relation in using_models if relation in relations
            }

        # If this model doesn't have any using relations, it cannot be in use.
        if not any(using_models):
            return False

        # If we find any objects for any relation, then the model is in use.
        for relation_name in using_models:
            relation_queryset = self.in_use_by(relation_name, transaction)
            if relation_queryset.exists():
                return True

        return False

    @atomic
    def save(self, *args, force_write=False, **kwargs):
        """
        Save the model to the database.

        :param force_write bool: Ignore append-only restrictions and write to
            the database even if the model already exists
        """
        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()

        auto_fields = {
            field
            for field in self.auto_value_fields
            if field.attname in self.__dict__
            and isinstance(self.__dict__.get(field.attname), (Expression, F))
        }

        # If the model contains any fields that are built in the database, the
        # fields will still contain the expression objects. So remove them now
        # and Django will lazy fetch the real values if they are accessed.
        for field in auto_fields:
            delattr(self, field.name)

        return return_value

    def __str__(self):
        return self.identifying_fields_to_string()

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

    def get_url(self, action: str = "detail") -> Optional[str]:
        """
        Generate a URL to a representation of the model in the webapp.

        Callers should handle the case where no URL is returned.

        :param action str: The view type to generate a URL for (default
            "detail"), eg: "list" or "edit"
        :rtype Optional[str]: The generated URL
        """
        kwargs = {}
        if action not in ["list", "create"]:
            kwargs = self.get_identifying_fields()
        try:
            url = reverse(
                f"{self.get_url_pattern_name_prefix()}-ui-{action}",
                kwargs=kwargs,
            )
            return f"{url}{self.url_suffix}"
        except NoReverseMatch:
            return None

    @classmethod
    def get_url_pattern_name_prefix(cls):
        """
        Get the prefix string for a view name for this model.

        By default, this is the verbose name of the model with spaces replaced by
        underscores, but this method allows this to be overridden.

        :rtype str: The prefix
        """
        prefix = getattr(cls, "url_pattern_name_prefix", None)
        if not prefix:
            prefix = cls._meta.verbose_name.replace(" ", "_")
        return prefix
Exemplo n.º 4
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"
    url_pattern_name_prefix = "measure"
    url_suffix = "#conditions"

    identifying_fields = ("sid", )

    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 = TrackedModelManager.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,
        business_rules.ActionRequiresDuty,
        business_rules.ConditionCodeAcceptance,
        UniqueIdentifyingFields,
        UpdateValidity,
    )

    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 hasattr(self,
                   "reference_price_string") and 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)

    @property
    def duty_sentence(self) -> str:
        return MeasureConditionComponent.objects.duty_sentence(self)
Exemplo n.º 5
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.ME27,
        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,
        UniqueIdentifyingFields,
        UpdateValidity,
    )

    objects = TrackedModelManager.from_queryset(MeasuresQuerySet)()

    @property
    def footnote_application_codes(
            self) -> Set[footnote_validators.ApplicationCode]:
        codes = {footnote_validators.ApplicationCode.DYNAMIC_FOOTNOTE}
        if self.goods_nomenclature:
            codes.add(footnote_validators.ApplicationCode.OTHER_MEASURES)
        if not self.goods_nomenclature.is_taric_code:
            codes.add(footnote_validators.ApplicationCode.CN_MEASURES)
        return codes

    validity_field_name = "db_effective_valid_between"

    @property
    def effective_end_date(self) -> date:
        """Measure end dates may be overridden by regulations."""
        if not hasattr(self, self.validity_field_name):
            effective_valid_between = (
                type(self).objects.with_validity_field().filter(
                    pk=self.pk).get().db_effective_valid_between)
            setattr(self, self.validity_field_name, effective_valid_between)

        return getattr(self, self.validity_field_name).upper

    def __str__(self):
        return str(self.sid)

    @property
    def effective_valid_between(self) -> TaricDateRange:
        if hasattr(self, self.validity_field_name):
            return getattr(self, self.validity_field_name)

        return TaricDateRange(self.valid_between.lower,
                              self.effective_end_date)

    @property
    def duty_sentence(self) -> str:
        return MeasureComponent.objects.duty_sentence(self)

    @classproperty
    def auto_value_fields(cls):
        """Remove export refund SID because we don't want to auto-increment it –
        it should really be a foreign key to an ExportRefundNomenclature model
        but as we don't use them in the UK Tariff we don't store them."""
        counters = super().auto_value_fields
        counters.remove(cls._meta.get_field("export_refund_nomenclature_sid"))
        return counters

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

    def has_condition_components(self, transaction):
        return (MeasureConditionComponent.objects.approved_up_to_transaction(
            transaction).filter(
                condition__dependent_measure__sid=self.sid).exists())
Exemplo n.º 6
0
class QuotaOrderNumber(TrackedModel, ValidityMixin):
    """
    The order number is the identification of a quota.

    It is defined for tariff quotas and surveillances. If an operator wants to
    benefit from a tariff quota, they must refer to it via the order number in
    the customs declaration. An order number may have multiple associated quota
    definitions, for example to divide a quota over several time periods.
    """

    record_code = "360"
    subrecord_code = "00"

    identifying_fields = ("sid", )

    sid = SignedIntSID(db_index=True)
    order_number = models.CharField(
        max_length=6,
        validators=[validators.quota_order_number_validator],
        db_index=True,
    )
    mechanism = models.PositiveSmallIntegerField(
        choices=validators.AdministrationMechanism.choices, )
    category = models.PositiveSmallIntegerField(
        choices=validators.QuotaCategory.choices, )

    origins = models.ManyToManyField(
        "geo_areas.GeographicalArea",
        through="QuotaOrderNumberOrigin",
        related_name="quotas",
    )

    required_certificates = models.ManyToManyField(
        "certificates.Certificate",
        related_name="quotas",
    )

    indirect_business_rules = (
        business_rules.ON7,
        business_rules.ON8,
        business_rules.QBP2,
        business_rules.QD1,
        business_rules.QD7,
        business_rules.CertificateValidityPeriodMustSpanQuotaOrderNumber,
        business_rules.CertificatesMustExist,
    )
    business_rules = (
        business_rules.ON1,
        business_rules.ON2,
        business_rules.ON9,
        business_rules.ON11,
        UniqueIdentifyingFields,
        UpdateValidity,
    )

    objects = TrackedModelManager.from_queryset(
        querysets.QuotaOrderNumberQuerySet)()

    def __str__(self):
        return self.order_number

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

    @property
    def is_origin_quota(self):
        return any(self.required_certificates.all())

    class Meta:
        verbose_name = "quota"
Exemplo n.º 7
0
class GoodsNomenclatureIndent(TrackedModel, ValidityStartMixin):
    record_code = "400"
    subrecord_code = "05"

    identifying_fields = ("sid", )

    objects: GoodsNomenclatureIndentQuerySet = TrackedModelManager.from_queryset(
        GoodsNomenclatureIndentQuerySet, )()

    sid = NumericSID()

    indent = models.PositiveIntegerField(db_index=True)

    indented_goods_nomenclature = models.ForeignKey(
        GoodsNomenclature,
        on_delete=models.PROTECT,
        related_name="indents",
    )

    indirect_business_rules = (business_rules.NIG11, )
    business_rules = (business_rules.NIG2, UniqueIdentifyingFields,
                      UpdateValidity)

    validity_over = "indented_goods_nomenclature"

    @property
    def is_root(self) -> bool:
        """Returns True if this is a root indent."""
        item_id = self.indented_goods_nomenclature.item_id
        return self.indent == 0 and item_id[2:] == "00000000"

    def get_good_indents(
        self,
        as_of_transaction: Optional[Transaction] = None,
    ) -> QuerySet:
        """Return the related goods indents based on approval status."""
        good = self.indented_goods_nomenclature
        return good.indents.approved_up_to_transaction(
            as_of_transaction or self.transaction, )

    def get_preceding_indent(
        self,
        as_of_transaction: Optional[Transaction] = None,
    ) -> Optional[GoodsNomenclatureIndent]:
        """Returns the node indent's predecessor in time, if any."""
        return (self.get_good_indents(as_of_transaction).filter(
            validity_start__lt=self.validity_start, ).order_by(
                "validity_start").last())

    def get_succeeding_indent(
        self,
        as_of_transaction: Optional[Transaction] = None,
    ) -> Optional[GoodsNomenclatureIndent]:
        """Returns the node indent's successor in time, if any."""
        return (self.get_good_indents(as_of_transaction).filter(
            validity_start__gt=self.validity_start, ).order_by(
                "validity_start").first())

    def save(self, *args, **kwargs):
        return_value = super().save(*args, **kwargs)

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

        return return_value

    def __str__(self):
        return f"Goods Nomenclature Indent: {self.indent} - {self.indented_goods_nomenclature}"