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']
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}"
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()
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", )
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), ), )
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
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())
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)
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)