forked from open-formulieren/open-forms
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[maykinmedia/open-producten#24] Add product price logic
- Loading branch information
Showing
25 changed files
with
700 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
PRICE_OPTION_KEY = "productPrice" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
from django.contrib import admin | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from solo.admin import SingletonModelAdmin | ||
|
||
from .models import OpenProductenConfig | ||
|
||
|
||
@admin.register(OpenProductenConfig) | ||
class OpenProductenConfigAdmin(SingletonModelAdmin): | ||
|
||
fieldsets = [ | ||
( | ||
_("Services"), | ||
{ | ||
"fields": [ | ||
"producten_service", | ||
], | ||
}, | ||
) | ||
] |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
from rest_framework import serializers | ||
|
||
from openforms.products.models import Product as ProductType | ||
|
||
from ..models import Price, PriceOption | ||
|
||
|
||
class PriceOptionSerializer(serializers.ModelSerializer): | ||
class Meta: | ||
model = PriceOption | ||
exclude = ("price",) | ||
|
||
|
||
class PriceSerializer(serializers.ModelSerializer): | ||
options = PriceOptionSerializer(many=True) | ||
|
||
class Meta: | ||
model = Price | ||
fields = "__all__" | ||
|
||
|
||
class ProductTypeSerializer(serializers.ModelSerializer): | ||
open_producten_price = PriceSerializer() | ||
|
||
class Meta: | ||
model = ProductType | ||
exclude = ("id", "information", "price") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
from drf_spectacular.utils import extend_schema, extend_schema_view | ||
from rest_framework import authentication, permissions, viewsets | ||
|
||
from openforms.products.models import Product as ProductType | ||
|
||
from .serializers import ProductTypeSerializer | ||
|
||
|
||
@extend_schema_view( | ||
list=extend_schema( | ||
summary=_("List available products with current price options"), | ||
), | ||
retrieve=extend_schema( | ||
summary=_("Retrieve details of a single product"), | ||
), | ||
) | ||
class PriceViewSet(viewsets.ReadOnlyModelViewSet): | ||
""" | ||
List and retrieve the products registered in the admin interface. | ||
Note that these endpoints are only available to authenticated admin users. The | ||
products functionality is minimal to be able to register prices. In the future, | ||
probably a dedicated products catalogue will become relevant. | ||
""" | ||
|
||
# queryset = ProductType.objects.all() | ||
authentication_classes = ( | ||
authentication.SessionAuthentication, | ||
authentication.TokenAuthentication, | ||
) | ||
permission_classes = (permissions.IsAdminUser,) | ||
serializer_class = ProductTypeSerializer | ||
lookup_field = "open_product_uuid" | ||
lookup_url_kwarg = "uuid" | ||
|
||
def get_queryset(self): | ||
return ProductType.objects.filter( | ||
open_producten_price__isnull=False, is_deleted=False | ||
).distinct() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
from dataclasses import dataclass | ||
from datetime import date | ||
from typing import Optional | ||
|
||
from zgw_consumers.api_models.base import Model | ||
|
||
|
||
@dataclass | ||
class PriceOption(Model): | ||
id: str | ||
amount: str | ||
description: str | ||
|
||
|
||
@dataclass | ||
class Price(Model): | ||
id: str | ||
valid_from: date | ||
options: list[PriceOption] | ||
|
||
|
||
@dataclass | ||
class ProductType(Model): | ||
id: str | ||
name: str | ||
current_price: Optional[Price] | ||
upl_name: str | ||
upl_uri: str |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
from django.apps import AppConfig | ||
from django.utils.translation import gettext_lazy as _ | ||
|
||
|
||
class OpenProductenConfig(AppConfig): | ||
name = "openforms.contrib.open_producten" | ||
label = "open_producten" | ||
verbose_name = _("Open Producten configuration") |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import logging | ||
|
||
import requests | ||
from ape_pie import APIClient | ||
from zgw_consumers.api_models.base import factory | ||
from zgw_consumers.client import build_client | ||
|
||
from openforms.contrib.open_producten.models import OpenProductenConfig | ||
|
||
from .api_models import ProductType | ||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
class OpenProductenClient(APIClient): | ||
def get_current_prices(self) -> list[ProductType]: | ||
try: | ||
response = self.get("producttypes/current-prices") | ||
response.raise_for_status() | ||
except requests.RequestException as exc: | ||
logger.exception( | ||
"exception while making KVK basisprofiel request", exc_info=exc | ||
) | ||
raise exc | ||
|
||
data = response.json() | ||
product_types = factory(ProductType, data) | ||
|
||
return product_types | ||
|
||
|
||
class NoServiceConfigured(RuntimeError): | ||
pass | ||
|
||
|
||
def get_open_producten_client() -> OpenProductenClient: | ||
config = OpenProductenConfig.get_solo() | ||
if not (service := config.producten_service): | ||
raise NoServiceConfigured("No open producten service configured!") | ||
return build_client(service, client_factory=OpenProductenClient) |
Empty file.
Empty file.
34 changes: 34 additions & 0 deletions
34
src/openforms/contrib/open_producten/management/commands/import_prices.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
from django.core.management.base import BaseCommand | ||
|
||
from openforms.contrib.open_producten.client import get_open_producten_client | ||
from openforms.contrib.open_producten.models import OpenProductenConfig | ||
from openforms.contrib.open_producten.price_import import PriceImporter | ||
|
||
|
||
class Command(BaseCommand): | ||
help = "Import product types" | ||
|
||
def handle(self, *args, **options): | ||
if OpenProductenConfig.objects.count() == 0: | ||
self.stdout.write( | ||
"Please define the OpenProductenConfig before running this command." | ||
) | ||
return | ||
|
||
client = get_open_producten_client() | ||
price_importer = PriceImporter(client) | ||
|
||
( | ||
created, | ||
updated, | ||
deleted_count, | ||
soft_deleted_count, | ||
) = price_importer.import_product_types() | ||
|
||
self.stdout.write(f"deleted {deleted_count} product type(s):\n") | ||
self.stdout.write(f"soft deleted {soft_deleted_count} product type(s):\n") | ||
self.stdout.write(f"updated {len(updated)} exising product type(s)") | ||
self.stdout.write(f"created {len(created)} new product type(s):\n") | ||
|
||
for instance in created: | ||
self.stdout.write(f"{type(instance).__name__}: {instance.uuid}") |
110 changes: 110 additions & 0 deletions
110
src/openforms/contrib/open_producten/migrations/0001_initial.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# Generated by Django 4.2.16 on 2024-10-10 12:59 | ||
|
||
import datetime | ||
from decimal import Decimal | ||
import django.core.validators | ||
from django.db import migrations, models | ||
import django.db.models.deletion | ||
|
||
|
||
class Migration(migrations.Migration): | ||
|
||
initial = True | ||
|
||
dependencies = [ | ||
("zgw_consumers", "0020_service_timeout"), | ||
] | ||
|
||
operations = [ | ||
migrations.CreateModel( | ||
name="Price", | ||
fields=[ | ||
("uuid", models.UUIDField(primary_key=True, serialize=False)), | ||
( | ||
"valid_from", | ||
models.DateField( | ||
help_text="The date at which this price is valid", | ||
unique=True, | ||
validators=[ | ||
django.core.validators.MinValueValidator( | ||
datetime.date.today | ||
) | ||
], | ||
verbose_name="Start date", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name": "Price", | ||
"verbose_name_plural": "Prices", | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name="PriceOption", | ||
fields=[ | ||
("uuid", models.UUIDField(primary_key=True, serialize=False)), | ||
( | ||
"amount", | ||
models.DecimalField( | ||
decimal_places=2, | ||
help_text="The amount of the price option", | ||
max_digits=8, | ||
validators=[ | ||
django.core.validators.MinValueValidator(Decimal("0.01")) | ||
], | ||
verbose_name="Price", | ||
), | ||
), | ||
( | ||
"description", | ||
models.CharField( | ||
help_text="Short description of the option", | ||
max_length=100, | ||
verbose_name="Description", | ||
), | ||
), | ||
( | ||
"price", | ||
models.ForeignKey( | ||
help_text="The price this option belongs to", | ||
on_delete=django.db.models.deletion.CASCADE, | ||
related_name="options", | ||
to="open_producten.price", | ||
verbose_name="Price", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name": "Price option", | ||
"verbose_name_plural": "Price options", | ||
}, | ||
), | ||
migrations.CreateModel( | ||
name="OpenProductenConfig", | ||
fields=[ | ||
( | ||
"id", | ||
models.AutoField( | ||
auto_created=True, | ||
primary_key=True, | ||
serialize=False, | ||
verbose_name="ID", | ||
), | ||
), | ||
( | ||
"producten_service", | ||
models.OneToOneField( | ||
limit_choices_to={"api_type": "orc"}, | ||
null=True, | ||
on_delete=django.db.models.deletion.PROTECT, | ||
related_name="+", | ||
to="zgw_consumers.service", | ||
verbose_name="Producten API", | ||
), | ||
), | ||
], | ||
options={ | ||
"verbose_name": "Open Producten configuration", | ||
}, | ||
), | ||
] |
Empty file.
Oops, something went wrong.