def conditions_clean(self, cleaned_data, measure_start_date): """ We get the reference_price from cleaned_data and the measure_start_date from the form's initial data. If both are present, we call validate_duties with measure_start_date. Then, if reference_price is provided, we use DutySentenceParser with measure_start_date, if present, or the current_date, to check that we are dealing with a simple duty (i.e. only one component). We then update cleaned_data with key-value pairs created from this single, unsaved component. """ price = cleaned_data.get("reference_price") if price and measure_start_date is not None: validate_duties(price, measure_start_date) if price: parser = DutySentenceParser.get(measure_start_date) components = parser.parse(price) if len(components) > 1: raise ValidationError( "A MeasureCondition cannot be created with a compound reference price (e.g. 3.5% + 11 GBP / 100 kg)", ) cleaned_data["duty_amount"] = components[0].duty_amount cleaned_data["monetary_unit"] = components[0].monetary_unit cleaned_data["condition_measurement"] = components[0].component_measurement # The JS autocomplete does not allow for clearing unnecessary certificates # In case of a user changing data, the information is cleared here. condition_code = cleaned_data.get("condition_code") if condition_code and not condition_code.accepts_certificate: cleaned_data["required_certificate"] = None return cleaned_data
def duty_sentence_parser_test( duty_sentence_parser: DutySentenceParser, duty_sentence_data: Tuple[str, List[Dict]], ): duty_sentence, expected_results = duty_sentence_data components = list(duty_sentence_parser.parse(duty_sentence)) assert len(expected_results) == len(components) for expected, actual in zip(expected_results, components): assert_attributes(expected, actual)
def duty_sentence_parser( duty_expressions: Dict[int, DutyExpression], monetary_units: Dict[str, MonetaryUnit], measurements: Dict[Tuple[str, Optional[str]], Measurement], ) -> DutySentenceParser: return DutySentenceParser( duty_expressions.values(), monetary_units.values(), measurements.values(), )
def validate_duties(duties, measure_start_date): """Validate duty sentence by parsing it.""" from measures.parsers import DutySentenceParser duty_sentence_parser = DutySentenceParser.get(measure_start_date, ) try: duty_sentence_parser.parse(duties) except ParseError as e: # More helpful errors could be emitted here - # for example if an amount or currency is missing # it may be possible to highlight that. logger.error("Error parse duty sentence %s", e) raise ValidationError("Enter a valid duty sentence.")
def __init__( self, workbasket: WorkBasket, base_date: date, defaults: Dict[str, Any] = {}, duty_sentence_parser: DutySentenceParser = None, condition_sentence_parser: ConditionSentenceParser = None, ) -> None: self.workbasket = workbasket self.defaults = defaults self.duty_sentence_parser = duty_sentence_parser or DutySentenceParser.get( base_date, ) self.condition_sentence_parser = (condition_sentence_parser or ConditionSentenceParser.get( base_date, ))
def create_conditions(self, obj): """ Gets condition formset from context data, loops over these forms and validates the data, checking for the condition_sid field in the data to indicate whether an existing condition is being updated or a new one created from scratch. Then deletes any existing conditions that are not being updated, before calling the MeasureCreationPattern.create_condition_and_components with the appropriate parser and condition data. """ formset = self.get_context_data()["conditions_formset"] excluded_sids = [] conditions_data = [] workbasket = WorkBasket.current(self.request) existing_conditions = obj.conditions.approved_up_to_transaction( workbasket.get_current_transaction(self.request), ) for f in formset.forms: f.is_valid() condition_data = f.cleaned_data # If the form has changed and "condition_sid" is in the changed data, # this means that the condition is preexisting and needs to updated # so that its dependent_measure points to the latest version of measure if f.has_changed() and "condition_sid" in f.changed_data: excluded_sids.append(f.initial["condition_sid"]) update_type = UpdateType.UPDATE condition_data["version_group"] = existing_conditions.get( sid=f.initial["condition_sid"], ).version_group condition_data["sid"] = f.initial["condition_sid"] # If changed and condition_sid not in changed_data, then this is a newly created condition elif f.has_changed() and "condition_sid" not in f.changed_data: update_type = UpdateType.CREATE condition_data["update_type"] = update_type conditions_data.append(condition_data) workbasket = WorkBasket.current(self.request) # Delete all existing conditions from the measure instance, except those that need to be updated for condition in existing_conditions.exclude(sid__in=excluded_sids): condition.new_version( workbasket=workbasket, update_type=UpdateType.DELETE, transaction=obj.transaction, ) if conditions_data: measure_creation_pattern = MeasureCreationPattern( workbasket=workbasket, base_date=obj.valid_between.lower, ) parser = DutySentenceParser.get( obj.valid_between.lower, component_output=MeasureConditionComponent, ) # Loop over conditions_data, starting at 1 because component_sequence_number has to start at 1 for component_sequence_number, condition_data in enumerate( conditions_data, start=1, ): # Create conditions and measure condition components, using instance as `dependent_measure` measure_creation_pattern.create_condition_and_components( condition_data, component_sequence_number, obj, parser, workbasket, )
def create_measures(self, data): """Returns a list of the created measures.""" measure_start_date = data["valid_between"].lower workbasket = WorkBasket.current(self.request) measure_creation_pattern = MeasureCreationPattern( workbasket=workbasket, base_date=measure_start_date, defaults={ "generating_regulation": data["generating_regulation"], }, ) measures_data = [] for commodity_data in data.get("formset-commodities", []): if not commodity_data.get("DELETE"): for geo_area in data["geo_area_list"]: measure_data = { "measure_type": data["measure_type"], "geographical_area": geo_area, "exclusions": data.get("geo_area_exclusions", None) or [], "goods_nomenclature": commodity_data["commodity"], "additional_code": data["additional_code"], "order_number": data["order_number"], "validity_start": measure_start_date, "validity_end": data["valid_between"].upper, "footnotes": [ item["footnote"] for item in data.get("formset-footnotes", []) if not item.get("DELETE") ], # condition_sentence here, or handle separately and duty_sentence after? "duty_sentence": commodity_data["duties"], } measures_data.append(measure_data) created_measures = [] for measure_data in measures_data: measure = measure_creation_pattern.create(**measure_data) parser = DutySentenceParser.get( measure.valid_between.lower, component_output=MeasureConditionComponent, ) for component_sequence_number, condition_data in enumerate( data.get("formset-conditions", []), start=1, ): if not condition_data.get("DELETE"): measure_creation_pattern.create_condition_and_components( condition_data, component_sequence_number, measure, parser, workbasket, ) created_measures.append(measure) return created_measures
def diff_components( instance, duty_sentence: str, start_date: date, workbasket: WorkBasket, transaction: Type[Transaction], component_output: Type[TrackedModel] = MeasureComponent, reverse_attribute: str = "component_measure", ): """ Takes a start_date and component_output (MeasureComponent is the default) and creates an instance of DutySentenceParser. Expects a duty_sentence string and passes this to parser to generate a list of new components. Then compares this list with existing components on the model instance (either a Measure or a MeasureCondition) and determines whether existing components are to be updated, created, or deleted. Optionally accepts a Transaction, which should be passed when the method is called during the creation of a measure or condition, to minimise the number of transactions and avoid business rule violations (e.g. ActionRequiresDuty). """ from measures.parsers import DutySentenceParser parser = DutySentenceParser.get( start_date, component_output=component_output, ) new_components = parser.parse(duty_sentence) old_components = instance.components.approved_up_to_transaction( workbasket.current_transaction, ) new_by_id = {c.duty_expression.id: c for c in new_components} old_by_id = {c.duty_expression.id: c for c in old_components} all_ids = set(new_by_id.keys()) | set(old_by_id.keys()) update_transaction = transaction if transaction else None for id in all_ids: new = new_by_id.get(id) old = old_by_id.get(id) if new and old: # Component is having amount/unit changed – UPDATE it new.update_type = UpdateType.UPDATE new.version_group = old.version_group setattr(new, reverse_attribute, instance) if not update_transaction: update_transaction = workbasket.new_transaction() new.transaction = update_transaction new.save() elif new: # Component exists only in new set - CREATE it new.update_type = UpdateType.CREATE setattr(new, reverse_attribute, instance) new.transaction = ( transaction if transaction else workbasket.new_transaction() ) new.save() elif old: # Component exists only in old set – DELETE it old = old.new_version( workbasket, update_type=UpdateType.DELETE, transaction=workbasket.new_transaction(), )