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, )
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
def test_maybe_min(values, expected): assert util.maybe_min(*values) is expected
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, )