class DestructionReport(models.Model): title = models.CharField( max_length=200, verbose_name=_("title"), help_text=_("Title of the destruction report"), ) process_owner = models.ForeignKey( to="accounts.User", on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("process owner"), help_text= _("Process owner of the destruction list for which the report was created" ), ) content_pdf = PrivateMediaFileField( verbose_name=_("content pdf"), upload_to="reports/%Y/%m/", help_text=_("Content of the destruction report in PDF format"), blank=True, null=True, ) content_csv = PrivateMediaFileField( verbose_name=_("content csv"), upload_to="reports/%Y/%m/", help_text=_("Content of the destruction report in CSV format"), blank=True, null=True, ) destruction_list = models.ForeignKey( to="destruction.DestructionList", on_delete=models.SET_NULL, blank=True, null=True, verbose_name=_("destruction list"), help_text=_("Destruction list for which the report was created."), ) class Meta: verbose_name = _("Destruction report") verbose_name_plural = _("Destruction reports") def __str__(self): return self.title def clean(self): if (self.process_owner and self.process_owner.role.type != RoleTypeChoices.process_owner): error_message = _( "Only a process owner can be associated with a destruction report" ) raise ValidationError(error_message) def get_filename(self, extension="pdf"): attr = f"content_{extension}" return os.path.basename(getattr(self, attr).name)
class BestandsDeel(models.Model): uuid = models.UUIDField( unique=True, default=_uuid.uuid4, help_text="Unieke resource identifier (UUID4)" ) informatieobject = models.ForeignKey( "EnkelvoudigInformatieObjectCanonical", on_delete=models.CASCADE, related_name="bestandsdelen", ) volgnummer = models.PositiveIntegerField( help_text=_("Een volgnummer dat de volgorde van de bestandsdelen aangeeft.") ) omvang = models.BigIntegerField( validators=[MinValueValidator(0)], help_text=_("De grootte van dit specifieke bestandsdeel."), ) inhoud = PrivateMediaFileField( upload_to="part-uploads/%Y/%m/", blank=True, help_text=_("De (binaire) bestandsinhoud van dit specifieke bestandsdeel."), ) class Meta: verbose_name = "bestands deel" verbose_name_plural = "bestands delen" unique_together = ("informatieobject", "volgnummer") def unique_representation(self): return f"({self.informatieobject.latest_version.unique_representation()}) - {self.volgnummer}" @property def voltooid(self) -> bool: return bool(self.inhoud.name)
class Deduction(models.Model): name = models.CharField(_('name'), max_length=255) notes = models.TextField(_('notes'), blank=True, null=True) receipt = PrivateMediaFileField(_('receipt'), blank=True, upload_to='receipts/%Y/%m') date = models.DateField(_('date'), default=date.today) amount = models.DecimalField(_('amount'), max_digits=10, decimal_places=2) class Meta: verbose_name = _('deduction') verbose_name_plural = _('deductions')
class EnkelvoudigInformatieObject(AuditTrailMixin, APIMixin, InformatieObject, CMISClientMixin): """ Stores the content of a specific version of an EnkelvoudigInformatieObjectCanonical The model is split into two parts to support versioning, now a single `EnkelvoudigInformatieObjectCanonical` can exist with multiple different `EnkelvoudigInformatieObject`s, which can be retrieved by filtering """ canonical = models.ForeignKey(EnkelvoudigInformatieObjectCanonical, on_delete=models.CASCADE) uuid = models.UUIDField(default=_uuid.uuid4, help_text="Unieke resource identifier (UUID4)") # NOTE: Don't validate but rely on externally maintened list of Media Types # and that consumers know what they're doing. This prevents updating the # API specification on every Media Type that is added. formaat = models.CharField( max_length=255, blank=True, help_text='Het "Media Type" (voorheen "MIME type") voor de wijze waarop' "de inhoud van het INFORMATIEOBJECT is vastgelegd in een " "computerbestand. Voorbeeld: `application/msword`. Zie: " "https://www.iana.org/assignments/media-types/media-types.xhtml", ) taal = models.CharField( max_length=3, help_text="Een ISO 639-2/B taalcode waarin de inhoud van het " "INFORMATIEOBJECT is vastgelegd. Voorbeeld: `nld`. Zie: " "https://www.iso.org/standard/4767.html", ) bestandsnaam = models.CharField( _("bestandsnaam"), max_length=255, blank=True, help_text=_("De naam van het fysieke bestand waarin de inhoud van het " "informatieobject is vastgelegd, inclusief extensie."), ) inhoud = PrivateMediaFileField(upload_to="uploads/%Y/%m/", storage=private_media_storage_cmis) # inhoud = models.FileField(upload_to='uploads/%Y/%m/') link = models.URLField( max_length=200, blank=True, help_text="De URL waarmee de inhoud van het INFORMATIEOBJECT op te " "vragen is.", ) # these fields should not be modified directly, but go through the `integriteit` descriptor integriteit_algoritme = models.CharField( _("integriteit algoritme"), max_length=20, choices=ChecksumAlgoritmes.choices, blank=True, help_text=_( "Aanduiding van algoritme, gebruikt om de checksum te maken."), ) integriteit_waarde = models.CharField( _("integriteit waarde"), max_length=128, blank=True, help_text=_("De waarde van de checksum."), ) integriteit_datum = models.DateField( _("integriteit datum"), null=True, blank=True, help_text=_("Datum waarop de checksum is gemaakt."), ) integriteit = GegevensGroepType({ "algoritme": integriteit_algoritme, "waarde": integriteit_waarde, "datum": integriteit_datum, }) versie = models.PositiveIntegerField( default=1, help_text=_( "Het (automatische) versienummer van het INFORMATIEOBJECT. Deze begint bij 1 als het " "INFORMATIEOBJECT aangemaakt wordt."), ) begin_registratie = models.DateTimeField( auto_now=True, help_text=_( "Een datumtijd in ISO8601 formaat waarop deze versie van het INFORMATIEOBJECT is aangemaakt of " "gewijzigd."), db_index=True, ) # When dealing with remote EIO, there is no pk or canonical instance to derive # the lock status from. The getters and setters then use this private attribute. _locked = False objects = AdapterManager() class Meta: # No bronorganisatie/identificatie unique-together constraint, otherwise new versions of a document cannot be # saved to the database. unique_together = [("uuid", "versie")] verbose_name = _("Document") verbose_name_plural = _("Documenten") indexes = [models.Index(fields=["canonical", "-versie"])] ordering = ["canonical", "-versie"] def __init__(self, *args, **kwargs): kwargs.pop("_request", None) # see hacky workaround in EIOSerializer.create super().__init__(*args, **kwargs) @property def locked(self) -> bool: if self.pk or self.canonical is not None: return bool(self.canonical.lock) return self._locked @locked.setter def locked(self, value: bool) -> None: # this should only be called for remote objects, as other objects derive the # lock status from the canonical object assert self.canonical is None, "Setter should only be called for remote objects" self._locked = value def save(self, *args, **kwargs) -> None: if not settings.CMIS_ENABLED: return super().save(*args, **kwargs) else: model_data = model_to_dict(self) # If the document doesn't exist, create it, otherwise update it try: # sanity - check - assert the doc exists in CMIS backend self.cmis_client.get_document(drc_uuid=self.uuid) # update the instance state to the storage backend EnkelvoudigInformatieObject.objects.filter( uuid=self.uuid).update(**model_data) # Needed or the current django object will contain the version number and the download url # from before the update and this data is sent back in the response modified_document = EnkelvoudigInformatieObject.objects.get( uuid=self.uuid) self.versie = modified_document.versie self.inhoud = modified_document.inhoud except exceptions.DocumentDoesNotExistError: EnkelvoudigInformatieObject.objects.create(**model_data) def delete(self, *args, **kwargs): if not settings.CMIS_ENABLED: return super().delete(*args, **kwargs) else: if self.has_gebruiksrechten(): eio_instance_url = self.get_url() gebruiksrechten = Gebruiksrechten.objects.filter( informatieobject=eio_instance_url) for gebruiksrechten_doc in gebruiksrechten: gebruiksrechten_doc.delete() self.cmis_client.delete_document(self.uuid) def destroy(self): if settings.CMIS_ENABLED: self.delete() else: self.canonical.delete() def has_references(self): if settings.CMIS_ENABLED: if (BesluitInformatieObject.objects.filter( _informatieobject=self.canonical).exists() or ZaakInformatieObject.objects.filter( _informatieobject=self.canonical).exists()): return True else: return False else: if (self.canonical.besluitinformatieobject_set.exists() or self.canonical.zaakinformatieobject_set.exists()): return True else: return False def get_url(self): eio_path = reverse( "enkelvoudiginformatieobject-detail", kwargs={ "version": "1", "uuid": self.uuid }, ) return make_absolute_uri(eio_path) def has_gebruiksrechten(self): if settings.CMIS_ENABLED: eio_url = self.get_url() return Gebruiksrechten.objects.filter( informatieobject=eio_url).exists() else: return self.canonical.gebruiksrechten_set.exists()
class File(models.Model): file = PrivateMediaFileField()
class Invoice(models.Model): client = models.ForeignKey('crm.Client', on_delete=models.PROTECT) date = models.DateField( _('date'), help_text=_('Include work up to (including) this day.')) generated = models.DateTimeField(editable=False, null=True) invoice_number = models.CharField( _('invoice number'), max_length=50, blank=True, unique=True, default=None, null=True, validators=[validators.RegexValidator(RE_INVOICE_NUMBER)]) reference_number = models.CharField(_('reference number'), max_length=50, blank=True, null=True) due_date = models.DateTimeField(_('due date'), null=True, blank=True) pdf = PrivateMediaFileField(_('pdf'), blank=True, upload_to='invoices/%Y/%m') received = models.DateTimeField(_('received'), null=True, blank=True) created = models.DateTimeField(auto_now_add=True) updated = models.DateTimeField(auto_now=True, null=True) def __str__(self): return '{client} - {date}'.format(client=self.client, date=self.date) def get_absolute_url(self): return reverse('invoices:detail', kwargs={'invoice_number': self.invoice_number}) def get_previous(self, **kwargs): kwargs['client'] = self.client return self.get_previous_by_date(**kwargs) def generate_invoice_number(self, save=True): """ We choose here to prefix the year that the invoice object was created, and number incrementally (+1) across the years. """ prefix = self.created.year agg = self.__class__.objects.aggregate(Max('invoice_number')) max_number = agg['invoice_number__max'] or '201500000' match = RE_INVOICE_NUMBER.match(max_number) if not match: raise ValueError('Invalid invoice number for invoice %d', self.pk) max_number = int(max_number[4:]) next_number = '{:05d}'.format(max_number + 1) self.invoice_number = '{prefix}{number}'.format(prefix=prefix, number=next_number) if save: self.save() def generate(self): if self.generated is not None: return # collect the work entries try: previous = self.get_previous() lower = datetime.combine(previous.date, time( 0, 0)) + timedelta(days=1) except self.__class__.DoesNotExist: previous = None lower = datetime(1970, 1, 1, 0, 0) lower = timezone.make_aware(lower) upper = timezone.make_aware( datetime.combine(self.date, time(23, 59, 59))) work_entries = WorkEntry.objects.filter( project__client=self.client, date__range=[lower, upper]).select_related('project') with transaction.atomic(): for entry in work_entries: InvoiceItem.objects.create( invoice=self, project=entry.project, rate=entry.project.base_rate if not entry.project.flat_fee else entry.project.flat_fee, amount=entry.duration.total_seconds() / 3600, tax_rate=entry.project.vat, source_object=entry, remarks=entry.notes, date=entry.date) # either created from hourly rate (work_entries) or manual invoice items if work_entries or self.invoiceitem_set.exists(): self.generated = timezone.now() self.generate_invoice_number(save=False) self.reference_number = generate_invoice_reference( self.client, self.invoice_number) self.save() def regenerate(self): if self.received is not None: logger.info('Not regenerating paid invoice %d, fix this manually', self.pk) return if self.generated is not None: self.invoiceitem_set.all().delete() self.generated = None logger.info( 'Regenerating invoice %d, potentially rewriting sent-out invoices.', self.pk) self.generate() def get_totals(self): totals = self.invoiceitem_set.annotate(base=F('rate') * F('amount'), tax=F('rate') * F('amount') * F('tax_rate')).aggregate( Sum('base'), Sum('tax')) return totals def total_hours(self): items = self.invoiceitem_set.select_related('project').order_by( 'project', 'tax_rate') return items.aggregate(Sum('amount'))['amount__sum'] @property def total_no_vat(self): return self.get_totals()['base__sum'] @property def total_vat(self): return self.get_totals()['tax__sum'] @property def total_with_vat(self): totals = self.get_totals() return totals['base__sum'] + totals['tax__sum'] @cached_property def vat_reverse_charge(self): return self.client.country != settings.SITE_COUNTRY
class EnkelvoudigInformatieObject(APIMixin, InformatieObject): """ Stores the content of a specific version of an EnkelvoudigInformatieObjectCanonical The model is split into two parts to support versioning, now a single `EnkelvoudigInformatieObjectCanonical` can exist with multiple different `EnkelvoudigInformatieObject`s, which can be retrieved by filtering """ canonical = models.ForeignKey( EnkelvoudigInformatieObjectCanonical, on_delete=models.CASCADE ) uuid = models.UUIDField( default=_uuid.uuid4, help_text="Unieke resource identifier (UUID4)" ) # NOTE: Don't validate but rely on externally maintened list of Media Types # and that consumers know what they're doing. This prevents updating the # API specification on every Media Type that is added. formaat = models.CharField( max_length=255, blank=True, help_text='Het "Media Type" (voorheen "MIME type") voor de wijze waarop' "de inhoud van het INFORMATIEOBJECT is vastgelegd in een " "computerbestand. Voorbeeld: `application/msword`. Zie: " "https://www.iana.org/assignments/media-types/media-types.xhtml", ) taal = models.CharField( max_length=3, help_text="Een ISO 639-2/B taalcode waarin de inhoud van het " "INFORMATIEOBJECT is vastgelegd. Voorbeeld: `nld`. Zie: " "https://www.iso.org/standard/4767.html", ) bestandsnaam = models.CharField( _("bestandsnaam"), max_length=255, blank=True, help_text=_( "De naam van het fysieke bestand waarin de inhoud van het " "informatieobject is vastgelegd, inclusief extensie." ), ) inhoud = PrivateMediaFileField(upload_to="uploads/%Y/%m/") # inhoud = models.FileField(upload_to='uploads/%Y/%m/') link = models.URLField( max_length=200, blank=True, help_text="De URL waarmee de inhoud van het INFORMATIEOBJECT op te " "vragen is.", ) # these fields should not be modified directly, but go through the `integriteit` descriptor integriteit_algoritme = models.CharField( _("integriteit algoritme"), max_length=20, choices=ChecksumAlgoritmes.choices, blank=True, help_text=_("Aanduiding van algoritme, gebruikt om de checksum te maken."), ) integriteit_waarde = models.CharField( _("integriteit waarde"), max_length=128, blank=True, help_text=_("De waarde van de checksum."), ) integriteit_datum = models.DateField( _("integriteit datum"), null=True, blank=True, help_text=_("Datum waarop de checksum is gemaakt."), ) integriteit = GegevensGroepType( { "algoritme": integriteit_algoritme, "waarde": integriteit_waarde, "datum": integriteit_datum, } ) versie = models.PositiveIntegerField( default=1, help_text=_( "Het (automatische) versienummer van het INFORMATIEOBJECT. Deze begint bij 1 als het " "INFORMATIEOBJECT aangemaakt wordt." ), ) begin_registratie = models.DateTimeField( auto_now=True, help_text=_( "Een datumtijd in ISO8601 formaat waarop deze versie van het INFORMATIEOBJECT is aangemaakt of " "gewijzigd." ), ) class Meta: unique_together = ("uuid", "versie")
class StufBGClient(SingletonModel): ontvanger_organisatie = models.CharField(_("organisatie"), max_length=200, blank=True) ontvanger_applicatie = models.CharField(_("applicatie"), max_length=200) ontvanger_administratie = models.CharField(_("administratie"), max_length=200, blank=True) ontvanger_gebruiker = models.CharField(_("gebruiker"), max_length=200, blank=True) zender_organisatie = models.CharField(_("organisatie"), max_length=200, blank=True) zender_applicatie = models.CharField(_("applicatie"), max_length=200) zender_administratie = models.CharField(_("administratie"), max_length=200, blank=True) zender_gebruiker = models.CharField(_("gebruiker"), max_length=200, blank=True) url = models.URLField( _("url"), blank=True, help_text="URL of the StUF-BG service to connect to.", ) user = models.CharField( _("user"), max_length=200, blank=True, help_text="Username to use in the XML security context.", ) password = models.CharField( _("password"), max_length=200, blank=True, help_text="Password to use in the XML security context.", ) certificate = PrivateMediaFileField( upload_to="certificate/", blank=True, null=True, help_text= "The SSL certificate file used for client identification. If left empty, mutual TLS is disabled.", ) certificate_key = PrivateMediaFileField( upload_to="certificate/", help_text= "The SSL certificate key file used for client identification. If left empty, mutual TLS is disabled.", blank=True, null=True, ) class Meta: verbose_name = _("StUF-BG Client") def _get_headers(self): credentials = f"{self.user}:{self.password}".encode("utf-8") encoded_credentials = base64.b64encode(credentials).decode("utf-8") return { "Authorization": "Basic " + encoded_credentials, "Content-Type": "application/soap+xml", } def _get_request_base_context(self): return { "created": timezone.now(), "expired": timezone.now() + timedelta(minutes=5), "username": self.user, "password": self.password, "zender_organisatie": self.zender_organisatie, "zender_applicatie": self.zender_applicatie, "zender_administratie": self.zender_administratie, "zender_gebruiker": self.zender_gebruiker, "ontvanger_organisatie": self.ontvanger_organisatie, "ontvanger_applicatie": self.ontvanger_applicatie, "ontvanger_administratie": self.ontvanger_administratie, "ontvanger_gebruiker": self.ontvanger_gebruiker, "referentienummer": str(uuid.uuid4()), "tijdstip_bericht": dateformat.format(timezone.now(), "YmdHis"), } def _make_request(self, data): cert = ((self.certificate.path, self.certificate_key.path) if self.certificate and self.certificate_key else (None, None)) with open( f"{settings.DJANGO_PROJECT_DIR}/xsd/bg0310/vraagAntwoord/bg0310_namespace.xsd", "r", ) as f: xmlschema_doc = etree.parse(f) xmlschema = etree.XMLSchema(xmlschema_doc) doc = etree.parse(BytesIO(bytes(data, encoding="UTF-8"))) el = (doc.getroot().xpath( "soap:Body", namespaces={"soap": "http://schemas.xmlsoap.org/soap/envelope/"}, )[0].getchildren()[0]) if not xmlschema.validate(el): raise ValidationError(xmlschema.error_log.last_error.message) response = requests.post( self.url, data=data, headers=self._get_headers(), cert=cert, ) return response def _make_historie_request(self, request_file, additional_context): request_context = self._get_request_base_context() request_context.update(additional_context) data = loader.render_to_string(request_file, request_context) return self._make_request(data) def get_persoon_request_data(self, bsn=None, filters=None): context = self._get_request_base_context() if bsn: context.update({"bsn": bsn}) if filters: context.update(filters) template = "request/RequestIngeschrevenPersoon.xml" return loader.render_to_string(template, context) def get_nested_request_data(self, template, bsn): context = self._get_request_base_context() context.update({"bsn": bsn}) return loader.render_to_string(template, context) def get_ingeschreven_persoon(self, bsn=None, filters=None): data = self.get_persoon_request_data(bsn=bsn, filters=filters) return self._make_request(data) def get_kind(self, bsn): data = self.get_nested_request_data("request/RequestKind.xml", bsn) return self._make_request(data) def get_ouder(self, bsn): data = self.get_nested_request_data("request/RequestOuder.xml", bsn) return self._make_request(data) def get_partner(self, bsn): data = self.get_nested_request_data("request/RequestPartner.xml", bsn) return self._make_request(data) def get_verblijf_plaats_historie(self, bsn, filters): additional_context = {"bsn": bsn} additional_context.update(filters) return self._make_historie_request( "request/RequestVerblijfPlaatsHistorie.xml", additional_context, ) def get_partner_historie(self, bsn, filters): additional_context = {"bsn": bsn} additional_context.update(filters) return self._make_historie_request( "request/RequestPartnerHistorie.xml", additional_context, ) def get_verblijfs_titel_historie(self, bsn, filters): additional_context = {"bsn": bsn} additional_context.update(filters) return self._make_historie_request( "request/RequestVerblijfsTitelHistorie.xml", additional_context, ) def get_nationaliteit_historie(self, bsn, filters): additional_context = {"bsn": bsn} additional_context.update(filters) return self._make_historie_request( "request/RequestNationaliteitHistorie.xml", additional_context, )