diff --git a/core/static/bundled/core/components/ajax-select-base.ts b/core/static/bundled/core/components/ajax-select-base.ts index 525de097..06c3508e 100644 --- a/core/static/bundled/core/components/ajax-select-base.ts +++ b/core/static/bundled/core/components/ajax-select-base.ts @@ -68,7 +68,7 @@ export class AutoCompleteSelectBase extends inheritHtmlElement("select") { title: gettext("Remove"), }, // biome-ignore lint/style/useNamingConvention: this is required by the api - restore_on_backspace: {} + restore_on_backspace: {}, }, persist: false, maxItems: this.node.multiple ? this.max : 1, diff --git a/counter/api.py b/counter/api.py index f3f0f101..7c181aa0 100644 --- a/counter/api.py +++ b/counter/api.py @@ -12,21 +12,21 @@ # OR WITHIN THE LOCAL FILE "LICENSE" # # -from typing import Annotated - -from annotated_types import MinLen -from django.db.models import Q +from django.conf import settings +from django.db.models import F from ninja import Query from ninja_extra import ControllerBase, api_controller, paginate, route from ninja_extra.pagination import PageNumberPaginationExtra from ninja_extra.schemas import PaginatedResponseSchema -from core.api_permissions import CanAccessLookup, CanView, IsRoot +from core.api_permissions import CanAccessLookup, CanView, IsInGroup, IsRoot from counter.models import Counter, Product from counter.schemas import ( CounterFilterSchema, CounterSchema, + ProductFilterSchema, ProductSchema, + SimpleProductSchema, SimplifiedCounterSchema, ) @@ -64,15 +64,39 @@ class CounterController(ControllerBase): class ProductController(ControllerBase): @route.get( "/search", - response=PaginatedResponseSchema[ProductSchema], + response=PaginatedResponseSchema[SimpleProductSchema], permissions=[CanAccessLookup], ) @paginate(PageNumberPaginationExtra, page_size=50) - def search_products(self, search: Annotated[str, MinLen(1)]): - return ( - Product.objects.filter( - Q(name__icontains=search) | Q(code__icontains=search) - ) - .filter(archived=False) - .values() + def search_products(self, filters: Query[ProductFilterSchema]): + return filters.filter( + Product.objects.order_by( + F("product_type__priority").desc(nulls_last=True), + "product_type", + "name", + ).values() + ) + + @route.get( + "/search/detailed", + response=PaginatedResponseSchema[ProductSchema], + permissions=[ + IsRoot + | IsInGroup(settings.SITH_GROUP_COUNTER_ADMIN_ID) + | IsInGroup(settings.SITH_GROUP_ACCOUNTING_ADMIN_ID) + ], + url_name="search_products_detailed", + ) + @paginate(PageNumberPaginationExtra, page_size=50) + def search_products_detailed(self, filters: Query[ProductFilterSchema]): + """Get the detailed information about the products.""" + return filters.filter( + Product.objects.select_related("club") + .prefetch_related("buying_groups") + .select_related("product_type") + .order_by( + F("product_type__priority").desc(nulls_last=True), + "product_type", + "name", + ) ) diff --git a/counter/schemas.py b/counter/schemas.py index ec1a842d..7ecb346b 100644 --- a/counter/schemas.py +++ b/counter/schemas.py @@ -1,10 +1,12 @@ from typing import Annotated from annotated_types import MinLen +from django.urls import reverse from ninja import Field, FilterSchema, ModelSchema -from core.schemas import SimpleUserSchema -from counter.models import Counter, Product +from club.schemas import ClubSchema +from core.schemas import GroupSchema, SimpleUserSchema +from counter.models import Counter, Product, ProductType class CounterSchema(ModelSchema): @@ -26,7 +28,47 @@ class SimplifiedCounterSchema(ModelSchema): fields = ["id", "name"] -class ProductSchema(ModelSchema): +class ProductTypeSchema(ModelSchema): + class Meta: + model = ProductType + fields = ["id", "name"] + + +class SimpleProductSchema(ModelSchema): class Meta: model = Product fields = ["id", "name", "code"] + + +class ProductSchema(ModelSchema): + class Meta: + model = Product + fields = [ + "id", + "name", + "code", + "description", + "purchase_price", + "selling_price", + "icon", + "limit_age", + "archived", + ] + + buying_groups: list[GroupSchema] + club: ClubSchema + product_type: ProductTypeSchema | None + url: str + + @staticmethod + def resolve_url(obj: Product) -> str: + return reverse("counter:product_edit", kwargs={"product_id": obj.id}) + + +class ProductFilterSchema(FilterSchema): + search: Annotated[str, MinLen(1)] | None = Field( + None, q=["name__icontains", "code__icontains"] + ) + is_archived: bool | None = Field(None, q="archived") + buying_groups: set[int] | None = Field(None, q="buying_groups__in") + product_type: set[int] | None = Field(None, q="product_type__in") diff --git a/counter/static/bundled/counter/components/ajax-select-index.ts b/counter/static/bundled/counter/components/ajax-select-index.ts index 147e4733..a2d61a48 100644 --- a/counter/static/bundled/counter/components/ajax-select-index.ts +++ b/counter/static/bundled/counter/components/ajax-select-index.ts @@ -4,7 +4,7 @@ import type { TomOption } from "tom-select/dist/types/types"; import type { escape_html } from "tom-select/dist/types/utils"; import { type CounterSchema, - type ProductSchema, + type SimpleProductSchema, counterSearchCounter, productSearchProducts, } from "#openapi"; @@ -23,13 +23,13 @@ export class ProductAjaxSelect extends AjaxSelect { return []; } - protected renderOption(item: ProductSchema, sanitize: typeof escape_html) { + protected renderOption(item: SimpleProductSchema, sanitize: typeof escape_html) { return `