class TimeSlots(models.Model): name_id = models.CharField( help_text="Unique and computer-friendly name of time_slots", max_length=100, unique=True, ) name = models.CharField( help_text="Human-readable name of the time_slots.", max_length=50 ) zero = models.DateTimeField( null=False, default=time_series_default_zero ) frequency = RelativeDeltaField( null=False, default=default_relative_delta_hour ) range_from = RelativeDeltaField( null=False, default=default_relative_delta_zero ) range_to = RelativeDeltaField( null=False, default=default_relative_delta_hour ) def clean(self): if self.frequency is None: raise forms.ValidationError('frequency cannot be null') if self.range_from is None: raise forms.ValidationError('range_from cannot be null') if self.range_to is None: raise forms.ValidationError('range_to cannot be null') if relative_delta_to_total_seconds(self.frequency) <= 0: raise forms.ValidationError('frequency must be positive interval ') if relative_delta_to_total_seconds(self.range_to) <= relative_delta_to_total_seconds(self.range_from): raise forms.ValidationError('range_to must be greater than range_from') def __str__(self): return self.name
class IntervalWithChoice(models.Model): CHOICES = [ (relativedelta(months=1), '1 month'), ('P3M', '3 months'), (relativedelta(months=6), '6 months'), ] value = RelativeDeltaField(null=True, blank=True, choices=CHOICES)
def test_simple_annotation(dummy_intervals): one_month = Cast( Value(relativedelta(months=1), output_field=RelativeDeltaField()), DurationField() ) q = Interval.objects.annotate(month_earlier=ExpressionWrapper(F('date') - one_month, output_field=DateField())) result = list(q.values_list('date', 'month_earlier')) assert result == [(date(2020, 3, 6), datetime(2020, 2, 6, 0, 0)), (date(2020, 10, 6), datetime(2020, 9, 6, 0, 0))] assert 1 == q.filter(month_earlier__lt='2020-09-06').count() assert 2 == q.filter(month_earlier__lte='2020-09-06').count()
class Interval(models.Model): value = RelativeDeltaField(null=True, blank=True)
class ResultaatType(models.Model): """ Het betreft de indeling of groepering van resultaten van zaken van hetzelfde ZAAKTYPE naar hun aard, zoals 'verleend', 'geweigerd', 'verwerkt', et cetera. Toelichting objecttype Elke zaak heeft een resultaat. In een aantal gevallen valt dit resultaat samen met een besluit: ‘Evenementenvergunning verleend’, ‘Energiesubsidie geweigerd’, et cetera. Het komt echter ook voor dat zaken worden afgehandeld zonder dat er een besluit wordt genomen. Dit is bijvoorbeeld het geval bij aangiften (geboorte, verhuizing), meldingen (openbare ruimte), maar ook bij het intrekken van een aanvraag. Het resultaat van een zaak is van groot belang voor de archivering: het resultaattype bepaalt mede of de zaak en het bijbehorende dossier moeten worden vernietigd (na enige termijn) of blijvend bewaard moeten worden (en na enige termijn ‘overgebracht’ worden naar een archiefbewaarplaats). Met RESULTAATTYPE worden de mogelijke resultaten benoemd bij het desbetreffende zaaktype. Daarmee is het archiefregime bepaald voor het gehele zaakdossier: alle informatie over en documenten bij de zaken van het ZAAKTYPE. In uitzonderingsgevallen kan er sprake van zijn dat documenten van een bepaald INFORMATIEOBJECTTYPE in zaakdossiers bij zaken van het ZAAKTYPE een afwijkend archiefregime hebben ten opzichte van het zaakdossier. Privacy-gevoeligheid kan er reden voor zijn om documenten van een ZAAKINFORMATIEOBJECTTYPE eerder te vernietigen dan het zaakdossier als geheel. Specifieke wetgeving, zoals die voor de BAG, leidt er daarentegen toe dat een Omgevingsvergunning (activiteit bouwen) ten eeuwige dage bewaard moet blijven terwijl het zaakdossier na 20 jaar vernietigd dient te worden. De relatiesoort ‘RESULTAATTYPE bepaalt afwijkend archiefregime van ZAAK-INFORMATIEOBJECT-TYPE’ geeft de mogelijkheid deze uitzonderingsgevallen te documenteren. """ uuid = models.UUIDField(unique=True, default=uuid.uuid4, help_text="Unieke resource identifier (UUID4)") zaaktype = models.ForeignKey( "catalogi.ZaakType", verbose_name=_("is relevant voor"), on_delete=models.CASCADE, related_name="resultaattypen", help_text= _("URL-referentie naar het ZAAKTYPE van ZAAKen waarin resultaten van dit RESULTAATTYPE bereikt kunnen worden." ), ) # core data - used by ZRC to calculate archival-related dates omschrijving = models.CharField( _("omschrijving"), max_length=20, help_text=_( "Omschrijving van de aard van resultaten van het RESULTAATTYPE."), ) resultaattypeomschrijving = models.URLField( _("resultaattypeomschrijving"), max_length=1000, help_text= _("Algemeen gehanteerde omschrijving van de aard van resultaten van het RESULTAATTYPE. " "Dit moet een URL-referentie zijn naar de referenlijst van generieke " "resultaattypeomschrijvingen. Im ImZTC heet dit 'omschrijving generiek'" ), ) omschrijving_generiek = models.CharField( _("omschrijving generiek"), max_length=20, blank=True, editable=False, help_text= _("Gecachete tekstuele waarde van de generieke resultaattypeomschrijving." ), ) # TODO: validate that this matches the Zaaktype.procestype selectielijstklasse = models.URLField( _("selectielijstklasse"), max_length=1000, help_text= _("URL-referentie naar de, voor het archiefregime bij het RESULTAATTYPE relevante, " "categorie in de Selectielijst Archiefbescheiden (RESULTAAT in de Selectielijst API) " "van de voor het ZAAKTYPE verantwoordelijke overheidsorganisatie."), ) # derived fields from selectielijstklasse archiefnominatie = models.CharField( _("archiefnominatie"), default="", choices=Archiefnominatie.choices, max_length=20, blank=True, help_text=_( "Aanduiding die aangeeft of ZAAKen met een resultaat van " "dit RESULTAATTYPE blijvend moeten worden bewaard of " "(op termijn) moeten worden vernietigd. Indien niet expliciet " "opgegeven wordt dit gevuld vanuit de selectielijst."), ) archiefactietermijn = RelativeDeltaField( _("archiefactietermijn"), null=True, blank=True, help_text=_( "De termijn, na het vervallen van het bedrjfsvoeringsbelang, " "waarna het zaakdossier (de ZAAK met alle bijbehorende " "INFORMATIEOBJECTen) van een ZAAK met een resultaat van dit " "RESULTAATTYPE vernietigd of overgebracht (naar een " "archiefbewaarplaats) moet worden. Voor te vernietigen " "dossiers betreft het de in die Selectielijst genoemde " "bewaartermjn. Voor blijvend te bewaren zaakdossiers " "betreft het de termijn vanaf afronding van de zaak tot " "overbrenging (de procestermijn is dan nihil)."), ) # TODO: validate dependencies between fields brondatum_archiefprocedure_afleidingswijze = models.CharField( _("afleidingswijze brondatum"), max_length=20, choices=Afleidingswijze.choices, help_text=_("Wijze van bepalen van de brondatum."), ) # TODO: this could/should be validated against a remote OAS 3.0! brondatum_archiefprocedure_datumkenmerk = models.CharField( _("datumkenmerk"), max_length=80, blank=True, help_text=_("Naam van de attribuutsoort van het procesobject dat " "bepalend is voor het einde van de procestermijn."), ) brondatum_archiefprocedure_einddatum_bekend = models.BooleanField( _("einddatum bekend"), default=False, help_text=_( "Indicatie dat de einddatum van het procesobject gedurende " "de uitvoering van de zaak bekend moet worden. Indien deze " "nog niet bekend is en deze waarde staat op `true`, dan " "kan de zaak (nog) niet afgesloten worden."), ) brondatum_archiefprocedure_objecttype = models.CharField( _("objecttype"), max_length=80, blank=True, choices=ZaakobjectTypes.choices, help_text= _("Het soort object in de registratie dat het procesobject representeert." ), ) # TODO: standardize content so that consumers understand this? brondatum_archiefprocedure_registratie = models.CharField( _("registratie"), max_length=80, blank=True, help_text= _("De naam van de registratie waarvan het procesobject deel uit maakt." ), ) brondatum_archiefprocedure_procestermijn = RelativeDeltaField( _("procestermijn"), null=True, blank=True, help_text=_( "De periode dat het zaakdossier na afronding van de zaak " "actief gebruikt en/of geraadpleegd wordt ter ondersteuning " "van de taakuitoefening van de organisatie. Enkel relevant " "indien de afleidingswijze 'termijn' is."), ) brondatum_archiefprocedure = GegevensGroepType( { "afleidingswijze": brondatum_archiefprocedure_afleidingswijze, "datumkenmerk": brondatum_archiefprocedure_datumkenmerk, "einddatum_bekend": brondatum_archiefprocedure_einddatum_bekend, "objecttype": brondatum_archiefprocedure_objecttype, "registratie": brondatum_archiefprocedure_registratie, "procestermijn": brondatum_archiefprocedure_procestermijn, }, optional=("datumkenmerk", "einddatum_bekend", "objecttype", "registratie"), none_for_empty=True, ) # meta-information - this is mostly informative toelichting = models.TextField( _("toelichting"), blank=True, help_text=_( "Een toelichting op dit RESULTAATTYPE en het belang hiervan " "voor ZAAKen waarin een resultaat van dit RESULTAATTYPE wordt geselecteerd." ), ) # 'old' fields, not actively used at the moment bepaalt_afwijkend_archiefregime_van = models.ManyToManyField( "catalogi.ZaakInformatieObjectType", verbose_name=_("bepaalt afwijkend archiefregime van"), through="catalogi.ZaakInformatieobjectTypeArchiefregime", blank=True, related_name="resultaattypes", help_text= _("Informatieobjecten van een ZAAKINFORMATIEOBJECTTYPE bij zaken van een ZAAKTYPE waarvan, op grond van " "resultaten van een RESULTAATTYPE bij dat ZAAKTYPE, de archiveringskenmerken afwijken van de " "archiveringskenmerken van het ZAAKTYPE."), ) heeft_verplichte_zot = models.ManyToManyField( "catalogi.ZaakObjectType", verbose_name=_("heeft verplichte"), blank=True, help_text=_( "De ZAAKOBJECTTYPEn die verplicht gerelateerd moeten zijn aan ZAAKen van dit ZAAKTYPE voordat een " "resultaat van dit RESULTAATTYPE kan worden gezet."), ) heeft_verplichte_ziot = models.ManyToManyField( "catalogi.ZaakInformatieObjectType", verbose_name=_("heeft verplichte zaakinformatie objecttype"), blank=True, related_name="resultaattypen", # TODO needs a better related name help_text= _("De INFORMATIEOBJECTTYPEn die verplicht aanwezig moeten zijn in het zaakdossier van ZAAKen van dit " "ZAAKTYPE voordat een resultaat van dit RESULTAATTYPE kan worden gezet." ), ) heeft_voor_brondatum_archiefprocedure_relevante = models.ForeignKey( "catalogi.Eigenschap", verbose_name=_("heeft voor brondatum archiefprocedure relevante"), blank=True, null=True, on_delete=models.CASCADE, help_text=_( "De EIGENSCHAP die bepalend is voor het moment waarop de Archiefactietermijn start voor een ZAAK " "met een resultaat van dit RESULTAATTYPE."), ) class Meta: unique_together = ("zaaktype", "omschrijving") verbose_name = _("resultaattype") verbose_name_plural = _("resultaattypen") def save(self, *args, **kwargs): """ Save some derived fields into local object as a means of caching. """ if not self.omschrijving_generiek and self.resultaattypeomschrijving: response = requests.get(self.resultaattypeomschrijving).json() self.omschrijving_generiek = response["omschrijving"] # derive the default archiefnominatie if not self.archiefnominatie and self.selectielijstklasse: selectielijstklasse = self.get_selectielijstklasse() self.archiefnominatie = selectielijstklasse["waardering"] if not self.archiefactietermijn and self.selectielijstklasse: selectielijstklasse = self.get_selectielijstklasse() self.archiefactietermijn = selectielijstklasse["bewaartermijn"] super().save(*args, **kwargs) def __str__(self): return f"{self.zaaktype} - {self.omschrijving}" def get_selectielijstklasse(self): if not hasattr(self, "_selectielijstklasse"): # selectielijstklasse should've been validated at this point by either # forms or serializers response = requests.get(self.selectielijstklasse) response.raise_for_status() self._selectielijstklasse = response.json() return self._selectielijstklasse
def test_none_value_survives_to_python(self): self.assertIsNone(RelativeDeltaField().to_python(None))
class Interval(models.Model): value = RelativeDeltaField(null=True, blank=True) date = models.DateField(default=datetime.date(2020, 10, 21))
class Resultaat(models.Model): uuid = models.UUIDField(_("uuid"), default=uuid.uuid4) # relations/tree # The tree is modelled via a simple self-FK because it cannot have arbitrary depth proces_type = models.ForeignKey("ProcesType", on_delete=models.CASCADE, verbose_name=_("procestype")) generiek_resultaat = models.ForeignKey( "self", null=True, blank=True, on_delete=models.CASCADE, limit_choices_to={"generiek_resultaat__isnull": True}, verbose_name=_("generiek resultaat"), help_text= _("Voor specifieke resultaten, geef aan bij welk generiek resultaat deze hoort" ), ) nummer = models.PositiveSmallIntegerField( _("nummer"), help_text=_( "Nummer van het resultaat. Dit wordt samengesteld met het procestype en " "generiek resultaat indien van toepassing."), ) naam = models.CharField(_("naam"), max_length=40, help_text=_("Benaming van het procestype")) omschrijving = models.CharField( _("omschrijving"), max_length=150, blank=True, help_text=_("Omschrijving van het specifieke resultaat"), ) herkomst = models.CharField( _("herkomst"), max_length=200, help_text= _("Voorbeeld: 'Risicoanalyse', 'Systeemanalyse' of verwijzing naar Wet- en regelgeving" ), ) waardering = models.CharField(_("waardering"), max_length=50, choices=Archiefnominatie.choices) procestermijn = models.CharField(_("procestermijn"), max_length=50, choices=Procestermijnen.choices, blank=True) bewaartermijn = RelativeDeltaField(_("bewaartermijn"), null=True, blank=True) toelichting = models.TextField(_("toelichting"), blank=True) # relevant domains algemeen_bestuur_en_inrichting_organisatie = models.BooleanField( _("algemeen bestuur en inrichting organisatie"), default=False) bedrijfsvoering_en_personeel = models.BooleanField( _("bedrijfsvoering en personeel"), default=False) publieke_informatie_en_registratie = models.BooleanField( _("publieke informatie en registratie"), default=False) burgerzaken = models.BooleanField(_("burgerzaken"), default=False) veiligheid = models.BooleanField(_("veiligheid"), default=False) verkeer_en_vervoer = models.BooleanField(_("verkeer en vervoer"), default=False) economie = models.BooleanField(_("economie"), default=False) onderwijs = models.BooleanField(_("onderwijs"), default=False) sport_cultuur_en_recreatie = models.BooleanField( _("sport, cultuur en recreatie"), default=False) sociaal_domein = models.BooleanField(_("sociaal domein"), default=False) volksgezonheid_en_milieu = models.BooleanField( _("volksgezonheid en milieu"), default=False) vhrosv = models.BooleanField(_("VHROSV"), default=False) heffen_belastingen = models.BooleanField(_("heffen belastingen etc."), default=False) alle_taakgebieden = models.BooleanField(_("alle taakgebieden"), default=False) procestermijn_opmerking = models.CharField( _("procestermijn opmerking"), max_length=20, blank=True, help_text=_("Voorbeeld: '25 jaar', '30 jaar, '5 of 10 jaar'"), ) objects = ResultaatQuerySet.as_manager() class Meta: verbose_name = _("resultaat") verbose_name_plural = _("resultaten") unique_together = ("proces_type", "generiek_resultaat", "nummer") def __str__(self): return f"{self.volledig_nummer} - {self.naam}" def clean(self): super().clean() if self.specifiek: if not self.omschrijving: raise ValidationError( { "omschrijving": _("Omschrijving is een verplicht veld voor specifieke resultaten" ) }, code="required", ) if self.proces_type_id != self.generiek_resultaat.proces_type_id: raise ValidationError( { "proces_type": _("Het procestype moet hetzelfde zijn als het procestype van het " "generiek resultaat.") }, code="invalid", ) elif self.generiek and self.omschrijving: raise ValidationError( { "omschrijving": _("Omschrijving mag niet opgegeven worden voor generieke resultaten" ) }, code="forbidden", ) @property def generiek(self) -> bool: return self.generiek_resultaat_id is None @property def specifiek(self) -> bool: return not self.generiek @property def volledig_nummer(self) -> str: """ Calculate the complete number of the result. """ generiek_resultaat_nr = (f".{self.generiek_resultaat.nummer}" if self.specifiek else "") return f"{self.proces_type.nummer}{generiek_resultaat_nr}.{self.nummer}"