Пример #1
0
    def create(
        self,
        duty_sentence: str,
        geographical_area: GeographicalArea,
        goods_nomenclature: GoodsNomenclature,
        measure_type: MeasureType,
        validity_start: date,
        validity_end: date,
        exclusions: Sequence[GeographicalArea] = [],
        order_number: Optional[QuotaOrderNumber] = None,
        authorised_use: bool = False,
        additional_code: AdditionalCode = None,
        footnotes: Sequence[Footnote] = [],
        proofs_of_origin: Sequence[Certificate] = [],
        condition_sentence: Optional[str] = None,
    ) -> Iterator[TrackedModel]:
        """
        Create a new measure linking the passed data and any defaults. The
        measure is saved as part of a single transaction.

        If `exclusions` are passed, measure exclusions will be created for those
        geographical areas on the created measures. If a group is passed as an
        exclusion, all of its members at of the start date of the measure will
        be excluded.

        If `authorised_use` is `True`, measure conditions requiring the N990
        authorised use certificate will be added to the measure.

        If `footnotes` are passed, footnote associations will be added to the
        measure.

        If `proofs_of_origin` are passed, measure conditions requiring the
        proofs will be added to the measure.
        """

        assert goods_nomenclature.suffix == "80", "ME7 – must be declarable"

        actual_start = maybe_max(validity_start,
                                 goods_nomenclature.valid_between.lower)
        actual_end = maybe_min(goods_nomenclature.valid_between.upper,
                               validity_end)

        new_measure_sid = self.measure_sid_counter()

        if actual_end != validity_end:
            logger.warning(
                "Measure {} end date capped by {} end date: {:%Y-%m-%d}".
                format(
                    new_measure_sid,
                    goods_nomenclature.item_id,
                    actual_end,
                ), )

        measure_data: Dict[str, Any] = {
            "update_type": UpdateType.CREATE,
            "transaction": self.workbasket.new_transaction(),
            **self.defaults,
            **{
                "sid":
                new_measure_sid,
                "measure_type":
                measure_type,
                "geographical_area":
                geographical_area,
                "goods_nomenclature":
                goods_nomenclature,
                "order_number":
                order_number or self.defaults.get("order_number"),
                "additional_code":
                additional_code or self.defaults.get("additional_code"),
                "valid_between":
                TaricDateRange(actual_start, actual_end),
            },
        }

        if actual_end is not None:
            measure_data["terminating_regulation"] = measure_data[
                "generating_regulation"]

        new_measure = Measure.objects.create(**measure_data)
        yield new_measure

        # If there are any geographical exclusions, output them attached to
        # the measure. If a group is passed as an exclusion, the members of
        # that group will be excluded instead.
        # TODO: create multiple measures if memberships come to an end.
        for exclusion in exclusions:
            yield from self.get_measure_excluded_geographical_areas(
                new_measure,
                exclusion,
            )

        # Output any footnote associations required.
        yield from self.get_measure_footnotes(new_measure, footnotes)

        # If this is a measure under authorised use, we need to add
        # some measure conditions with the N990 certificate.
        if authorised_use:
            yield from self.get_authorised_use_measure_conditions(new_measure)

        # If this is a measure for an origin quota, we need to add
        # some measure conditions with the passed proof of origin.
        if any(proofs_of_origin):
            yield from self.get_proof_of_origin_condition(
                new_measure, proofs_of_origin)

        # If we have a condition sentence, parse and add to the measure.
        if condition_sentence:
            yield from self.get_conditions(new_measure, condition_sentence)

        # Now generate the duty components for the passed duty rate.
        yield from self.get_measure_components_from_duty_rate(
            new_measure,
            duty_sentence,
        )
Пример #2
0
    def save(self, data: dict):
        depth = self.extra_data.pop("indent")
        data.update(**self.extra_data)
        if "indented_goods_nomenclature_id" in data:
            data["indented_goods_nomenclature"] = models.GoodsNomenclature.objects.get(
                pk=data.pop("indented_goods_nomenclature_id"),
            )
        item_id = data["indented_goods_nomenclature"].item_id

        indent = super().save(data)
        node_data = {
            "indent": indent,
            "valid_between": data["valid_between"],
            "creating_transaction_id": data["transaction_id"],
        }

        if depth == 0 and item_id[2:] == "00000000":
            # This is a root indent (i.e. a chapter heading)
            # Race conditions are common, so reduce the chance of it.
            time.sleep(random.choice([x * 0.05 for x in range(0, 200)]))
            models.GoodsNomenclatureIndentNode.add_root(**node_data)
            return indent

        chapter_heading = item_id[:2]

        parent_depth = depth + 1

        # In some cases, where there are phantom headers at the 4 digit level
        # in a chapter, the depth is shifted by + 1.
        # A phantom header is any good with a suffix != "80". In the real world
        # this represents a good that does not appear in any legislature and is
        # non-declarable. i.e. it does not exist outside of the database and is
        # purely for "convenience". This algorithm doesn't apply to chapter 99.
        extra_headings = (
            models.GoodsNomenclature.objects.filter(
                item_id__startswith=chapter_heading,
                item_id__endswith="000000",
            )
            .exclude(suffix="80")
            .exists()
        ) and chapter_heading != "99"

        if extra_headings and (
            item_id[-6:] != "000000"
            or data["indented_goods_nomenclature"].suffix == "80"
        ):
            parent_depth += 1

        start_date = data["valid_between"].lower
        end_date = maybe_min(
            data["valid_between"].upper,
            data["indented_goods_nomenclature"].valid_between.upper,
        )

        while start_date and ((start_date < end_date) if end_date else True):
            defn = (indent.sid, start_date.year, start_date.month, start_date.day)
            if defn in self.overrides:
                next_indent = models.GoodsNomenclatureIndent.objects.get(
                    sid=self.overrides[defn],
                )
                next_parent = next_indent.nodes.filter(
                    valid_between__contains=start_date,
                ).get()
                logger.info("Using manual override for indent %s", defn)
            else:
                next_parent = (
                    models.GoodsNomenclatureIndentNode.objects.filter(
                        indent__indented_goods_nomenclature__item_id__lte=item_id,
                        indent__indented_goods_nomenclature__item_id__startswith=chapter_heading,
                        indent__indented_goods_nomenclature__valid_between__contains=start_date,
                        indent__valid_between__contains=start_date,
                        valid_between__contains=start_date,
                        depth=parent_depth,
                    )
                    .order_by("-indent__indented_goods_nomenclature__item_id")
                    .first()
                )
            if not next_parent:
                raise InvalidIndentError(
                    f"Parent indent not found for {item_id} for date {start_date}",
                )

            indent_start = start_date
            indent_end = maybe_min(
                next_parent.valid_between.upper,
                next_parent.indent.valid_between.upper,
                next_parent.indent.indented_goods_nomenclature.valid_between.upper,
                end_date,
            )

            node_data["valid_between"] = TaricDateRange(indent_start, indent_end)
            next_parent.add_child(**node_data)

            start_date = (
                indent_end + relativedelta(days=+1) if indent_end else indent_end
            )
        return indent
Пример #3
0
def test_maybe_min(values, expected):
    assert util.maybe_min(*values) is expected
Пример #4
0
    def create_measure_tracked_models(
        self,
        duty_sentence: str,
        goods_nomenclature: GoodsNomenclature,
        validity_start: date,
        validity_end: date,
        exclusions: Sequence[GeographicalArea] = [],
        order_number: Optional[QuotaOrderNumber] = None,
        footnotes: Sequence[Footnote] = [],
        condition_sentence: Optional[str] = None,
        **data,
    ) -> Iterator[TrackedModel]:
        """
        Create a new measure linking the passed data and any defaults. The
        measure is saved as part of a single transaction.

        If `exclusions` are passed, measure exclusions will be created for those
        geographical areas on the created measures. If a group is passed as an
        exclusion, all of its members at of the start date of the measure will
        be excluded.

        If the measure type is one of the `self.authorised_use_measure_types`,
        measure conditions requiring the N990 authorised use certificate will be
        added to the measure.

        If `footnotes` are passed, footnote associations will be added to the
        measure.

        If an `order_number` with `required_conditions` is passed, measure
        conditions requiring the certificates will be added to the measure.

        Return an Iterator over all the TrackedModels created, starting with the
        Measure.
        """

        assert goods_nomenclature.suffix == "80", "ME7 – must be declarable"

        actual_start = maybe_max(validity_start,
                                 goods_nomenclature.valid_between.lower)
        actual_end = maybe_min(goods_nomenclature.valid_between.upper,
                               validity_end)

        new_measure_sid = self.measure_sid_counter()

        if actual_end != validity_end:
            logger.warning(
                "Measure {} end date capped by {} end date: {:%Y-%m-%d}".
                format(
                    new_measure_sid,
                    goods_nomenclature.item_id,
                    actual_end,
                ), )

        measure_data: Dict[
            str, Any] = {
                "update_type": UpdateType.CREATE,
                "transaction": self.workbasket.new_transaction(),
                **self.defaults,
                **{
                    "sid":
                    new_measure_sid,
                    "goods_nomenclature":
                    goods_nomenclature,
                    "order_number":
                    order_number or self.defaults.get("order_number"),
                    "valid_between":
                    TaricDateRange(actual_start, actual_end),
                },
                **data,
            }

        new_measure = Measure.objects.create(**measure_data)
        yield new_measure

        # If there are any geographical exclusions, output them attached to
        # the measure. If a group is passed as an exclusion, the members of
        # that group will be excluded instead.
        # TODO: create multiple measures if memberships come to an end.
        for exclusion in exclusions:
            yield from self.create_measure_excluded_geographical_areas(
                new_measure,
                exclusion,
            )

        # Output any footnote associations required.
        yield from self.create_measure_footnotes(new_measure, footnotes)

        # If this is a measure under authorised use, we need to add
        # some measure conditions with the N990 certificate.
        if new_measure.measure_type in self.authorised_use_measure_types:
            yield from self.create_measure_authorised_use_measure_conditions(
                new_measure, )

        # If this is a measure for an origin quota, we need to add
        # some measure conditions with the origin quota required certificates.
        if order_number and order_number.required_certificates.exists():
            yield from self.create_measure_origin_quota_conditions(
                new_measure,
                order_number.required_certificates.all(),
            )

        # If we have a condition sentence, parse and add to the measure.
        if condition_sentence:
            yield from self.create_measure_conditions(new_measure,
                                                      condition_sentence)

        # If there is a duty_sentence parse it and generate the duty components from the duty rate.
        if duty_sentence:
            yield from self.create_measure_components_from_duty_rate(
                new_measure,
                duty_sentence,
            )