diff --git a/pyproject.toml b/pyproject.toml index accae13..b233d71 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -144,7 +144,6 @@ line_length = 120 "AXES_ENABLED=False", "DEBUG=Off", "SECRET_KEY=secret", - "DATABASE_URL=sqlite:///db.sqlite", ] [tool.mypy] env = [ diff --git a/src/companies/api/filters.py b/src/companies/api/filters.py index 1809de7..21d40a1 100644 --- a/src/companies/api/filters.py +++ b/src/companies/api/filters.py @@ -8,5 +8,5 @@ class CompanyFilterSet(filters.FilterSet): user = filters.NumberFilter(field_name="user", method="filter_user") - def filter_user(self, queryset: QuerySet, _: str, value: int) -> QuerySet[Company]: + def filter_user(self, queryset: QuerySet[Company], _: str, value: int) -> QuerySet[Company]: return queryset.filter(owner_id=value) diff --git a/src/companies/api/serializers/__init__.py b/src/companies/api/serializers/__init__.py index 71aaba8..b0e1495 100644 --- a/src/companies/api/serializers/__init__.py +++ b/src/companies/api/serializers/__init__.py @@ -18,7 +18,7 @@ MasterProcedureReadSerializer, MasterProcedureWriteSerializer, ) -from companies.api.serializers.point import PointCreateSerializer, PointSerializer +from companies.api.serializers.point import MaterialsStatisticSerializer, PointCreateSerializer, PointSerializer from companies.api.serializers.stock import ( MaterialSerializer, MaterialTypeSerializer, @@ -45,6 +45,7 @@ "MasterProcedureReadSerializer", "MasterProcedureWriteSerializer", "MaterialSerializer", + "MaterialsStatisticSerializer", "MaterialTypeSerializer", "PointSerializer", "PointCreateSerializer", diff --git a/src/companies/api/serializers/point.py b/src/companies/api/serializers/point.py index 18acf0d..3553f3d 100644 --- a/src/companies/api/serializers/point.py +++ b/src/companies/api/serializers/point.py @@ -1,6 +1,7 @@ from rest_framework import serializers from companies.api.serializers import CurrentCompanyDefault +from companies.api.serializers.stock import MaterialSerializer from companies.models import Point @@ -18,3 +19,11 @@ def to_representation(self, instance: Point) -> dict: class Meta(PointSerializer.Meta): pass + + +class MaterialsStatisticSerializer(MaterialSerializer): + stocks = serializers.ListField(child=serializers.JSONField()) + usage = serializers.ListField(child=serializers.JSONField()) + + class Meta(MaterialSerializer.Meta): + fields: tuple = MaterialSerializer.Meta.fields + ("stocks", "usage") diff --git a/src/companies/api/viewsets/__init__.py b/src/companies/api/viewsets/__init__.py index f1655b2..5f73fa4 100644 --- a/src/companies/api/viewsets/__init__.py +++ b/src/companies/api/viewsets/__init__.py @@ -1,16 +1,17 @@ from companies.api.viewsets.company import CompanyViewSet from companies.api.viewsets.department import CategoryViewSet, DepartmentViewSet, ProcedureViewSet from companies.api.viewsets.employee import EmployeeViewSet, MasterProcedureViewSet -from companies.api.viewsets.point import PointViewSet +from companies.api.viewsets.point import MaterialsStatisticViewSet, PointViewSet from companies.api.viewsets.stock import MaterialTypeViewSet, MaterialViewSet, StockMaterialViewSet, StockViewSet __all__ = [ "CategoryViewSet", "CompanyViewSet", - "PointViewSet", "DepartmentViewSet", "EmployeeViewSet", "MasterProcedureViewSet", + "MaterialsStatisticViewSet", + "PointViewSet", "ProcedureViewSet", "StockViewSet", "StockMaterialViewSet", diff --git a/src/companies/api/viewsets/point.py b/src/companies/api/viewsets/point.py index 1bfd992..4431ae2 100644 --- a/src/companies/api/viewsets/point.py +++ b/src/companies/api/viewsets/point.py @@ -4,12 +4,19 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticatedOrReadOnly from rest_framework.response import Response -from rest_framework.viewsets import ModelViewSet +from rest_framework.viewsets import ModelViewSet, ReadOnlyModelViewSet -from app.api.permissions import IsCompanyOwnerOrReadOnly -from companies.api.serializers import EmployeeSerializer, PointCreateSerializer, PointSerializer -from companies.models import Point -from companies.services import EmployeeCreator +from django.db.models import QuerySet + +from app.api.permissions import IsCompanyOwner, IsCompanyOwnerOrReadOnly +from companies.api.serializers import ( + EmployeeSerializer, + MaterialsStatisticSerializer, + PointCreateSerializer, + PointSerializer, +) +from companies.models import Material, Point +from companies.services import DateValidatorService, EmployeeCreator @extend_schema(tags=["points"]) @@ -33,3 +40,16 @@ def add_employees(self, *args: Any, **kwargs: Any) -> Response: serializer = EmployeeSerializer(data=self.request.data, context=self.get_serializer_context(*args, **kwargs)) employee = EmployeeCreator(serializer)() return Response(EmployeeSerializer(employee).data) + + +class MaterialsStatisticViewSet(ReadOnlyModelViewSet): + serializer_class = MaterialsStatisticSerializer + permission_classes = [IsCompanyOwner] + + def get_queryset(self) -> QuerySet[Material]: + DateValidatorService(self.request.query_params)() + return Material.objects.statistic( + self.kwargs["point_pk"], + self.request.query_params.get("date_from"), + self.request.query_params.get("date_to"), + ) diff --git a/src/companies/models/__init__.py b/src/companies/models/__init__.py index c6591ee..dcc8c55 100644 --- a/src/companies/models/__init__.py +++ b/src/companies/models/__init__.py @@ -1,8 +1,9 @@ from companies.models.company import Company from companies.models.department import Category, Department, Procedure from companies.models.employee import Employee, MasterProcedure +from companies.models.material import Material, MaterialType from companies.models.point import Point -from companies.models.stock import Material, MaterialType, Stock, StockMaterial +from companies.models.stock import Stock, StockMaterial __all__ = [ "Category", diff --git a/src/companies/models/material.py b/src/companies/models/material.py new file mode 100644 index 0000000..6c40ab4 --- /dev/null +++ b/src/companies/models/material.py @@ -0,0 +1,107 @@ +from datetime import timedelta +from typing import Self + +from django.contrib.postgres.expressions import ArraySubquery +from django.db import models +from django.db.models import F, OuterRef, Q, QuerySet, Sum +from django.db.models.functions import JSONObject, TruncDate +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from app.models import DefaultModel +from companies.models.stock import StockMaterial +from purchases.models import UsedMaterial + + +class MaterialType(DefaultModel): + name = models.CharField( + _("material type name"), + max_length=255, + ) + + class Meta: + ordering = ("name",) + verbose_name = _("material type") + verbose_name_plural = _("material types") + + +class MaterialQuerySet(QuerySet): + def statistic(self, point_id: int, date_from: str | None = None, date_to: str | None = None) -> Self: + now = timezone.now().date() + date_filter = Q(date__gte=date_from or (now - timedelta(days=30))) & Q(date__lte=date_to or now) + stock_queryset = ( + StockMaterial.objects.select_related("stock__date") + .filter(stock__point__id=point_id) + .annotate(date=F("stock__date")) + .filter(date_filter) + ) + stocks = ( + stock_queryset.filter(material_id=OuterRef("id")) + .order_by("date") + .values("material_id", "date") + .annotate(amount=Sum("quantity")) + .annotate( # noqa: BLK100 + stocks=JSONObject( + date=F("date"), + amount=F("amount") + ) + ) + .values_list("stocks", flat=True) + ) + usage_queryset = ( + UsedMaterial.objects + .filter(material__stock__point_id=point_id) + .select_related("material__material_id") + .annotate(date=TruncDate("modified")) + .filter(date_filter) + ) + usage = ( + usage_queryset.filter(material__material_id=OuterRef("id")) + .order_by("date") + .values("material__material_id", "date") + .annotate(amount=Sum("amount")) + .annotate( + stocks=JSONObject( + date=F("date"), + amount=F("amount") + ) + ) + .values_list("stocks", flat=True) + ) + material_ids = set( + list( + stock_queryset.values_list("material_id", flat=True) + ) + list( + usage_queryset.values_list("material_id", flat=True) + ) + ) + return self.filter(id__in=material_ids).annotate( + stocks=ArraySubquery(stocks), usage=ArraySubquery(usage) + ) + + +class Material(DefaultModel): + brand = models.CharField( + _("material brand"), + max_length=255, + ) + name = models.CharField( + _("material name"), + max_length=255, + ) + unit = models.CharField( + _("material unit"), + max_length=255, + ) + kind = models.ForeignKey( + "MaterialType", + on_delete=models.PROTECT, + related_name="materials", + ) + + objects = MaterialQuerySet.as_manager() + + class Meta: + ordering = ("name",) + verbose_name = _("material") + verbose_name_plural = _("materials") diff --git a/src/companies/models/stock.py b/src/companies/models/stock.py index 5a30788..2718cc2 100644 --- a/src/companies/models/stock.py +++ b/src/companies/models/stock.py @@ -4,43 +4,6 @@ from app.models import DefaultModel -class MaterialType(DefaultModel): - class Meta: - ordering = ("name",) - verbose_name = _("material type") - verbose_name_plural = _("material types") - - name = models.CharField( - _("material type name"), - max_length=255, - ) - - -class Material(DefaultModel): - class Meta: - ordering = ("name",) - verbose_name = _("material") - verbose_name_plural = _("materials") - - brand = models.CharField( - _("material brand"), - max_length=255, - ) - name = models.CharField( - _("material name"), - max_length=255, - ) - unit = models.CharField( - _("material unit"), - max_length=255, - ) - kind = models.ForeignKey( - "MaterialType", - on_delete=models.PROTECT, - related_name="materials", - ) - - class StockMaterial(DefaultModel): class Meta: ordering = ("stock", "material") diff --git a/src/companies/services/__init__.py b/src/companies/services/__init__.py index 1a863e0..559b4ac 100644 --- a/src/companies/services/__init__.py +++ b/src/companies/services/__init__.py @@ -1,9 +1,11 @@ from companies.services.department import CategoryCreator from companies.services.employee import EmployeeCreator +from companies.services.materials_statistic import DateValidatorService from companies.services.stock import MaterialTypeCreator __all__ = [ "CategoryCreator", "EmployeeCreator", "MaterialTypeCreator", + "DateValidatorService", ] diff --git a/src/companies/services/materials_statistic/__init__.py b/src/companies/services/materials_statistic/__init__.py new file mode 100644 index 0000000..45a241e --- /dev/null +++ b/src/companies/services/materials_statistic/__init__.py @@ -0,0 +1,5 @@ +from companies.services.materials_statistic.validator import DateValidatorService + +__all__ = [ + "DateValidatorService", +] diff --git a/src/companies/services/materials_statistic/validator.py b/src/companies/services/materials_statistic/validator.py new file mode 100644 index 0000000..976e19b --- /dev/null +++ b/src/companies/services/materials_statistic/validator.py @@ -0,0 +1,36 @@ +from dataclasses import dataclass +from datetime import datetime +from typing import Callable + +from rest_framework.exceptions import ValidationError + +from django.http.request import QueryDict + +from app.services import BaseService + + +@dataclass +class DateValidatorService(BaseService): + query_params: QueryDict | None + + def valide_date_query_params(self) -> None: + if not self.query_params: + return + try: + date_format = "%Y-%m-%d" + date_from_str = self.query_params.get("date_from") + date_to_str = self.query_params.get("date_to") + if date_from_str: + date_from = datetime.strptime(date_from_str, date_format).date() + if date_to_str: + date_to = datetime.strptime(date_to_str, date_format).date() + if date_from_str and date_to_str and date_from >= date_to: + raise ValidationError("date_from must be less than the date_to.") + except (TypeError, ValueError): + raise ValidationError("Invalid date format.") + + def get_validators(self) -> list[Callable]: + return super().get_validators() + [self.valide_date_query_params] + + def act(self) -> bool: + return True diff --git a/src/companies/urls.py b/src/companies/urls.py index 4bd2aca..049fac8 100644 --- a/src/companies/urls.py +++ b/src/companies/urls.py @@ -20,6 +20,7 @@ point_router.register("departments", viewsets.DepartmentViewSet, basename="department") point_router.register("stocks", viewsets.StockViewSet, basename="stock") point_router.register("employees", viewsets.EmployeeViewSet, basename="employee") +point_router.register("materials", viewsets.MaterialsStatisticViewSet, basename="materials-statistic") department_router = NestedSimpleRouter(point_router, "departments", lookup="department") department_router.register("procedures", viewsets.ProcedureViewSet, basename="procedure") diff --git a/tests/apps/__init__.py b/tests/apps/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/app/__init__.py b/tests/apps/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/apps/companies/services/test_materials_statistic_validator.py b/tests/apps/companies/services/test_materials_statistic_validator.py new file mode 100644 index 0000000..b4596a3 --- /dev/null +++ b/tests/apps/companies/services/test_materials_statistic_validator.py @@ -0,0 +1,38 @@ +import pytest + +from rest_framework.exceptions import ValidationError + +from django.http.request import QueryDict + +from companies.services import DateValidatorService + + +def test_valid_date_params(material_date_query_params: dict[str, str]): + query_params = QueryDict(mutable=True) + query_params.update(**material_date_query_params) + + assert DateValidatorService(query_params)() is True + + +def test_only_date_from_param(material_date_query_params: dict[str, str]): + del material_date_query_params["date_to"] + query_params = QueryDict(mutable=True) + query_params.update(**material_date_query_params) + + assert DateValidatorService(query_params)() is True + + +def test_only_date_to_param(material_date_query_params: dict[str, str]): + del material_date_query_params["date_from"] + query_params = QueryDict(mutable=True) + query_params.update(**material_date_query_params) + + assert DateValidatorService(query_params)() is True + + +def test_invalid_date_params(invalide_material_date_query_params: dict[str, str]): + query_params = QueryDict(mutable=True) + query_params.update(**invalide_material_date_query_params) + + with pytest.raises(ValidationError): + DateValidatorService(query_params)() diff --git a/tests/apps/companies/test_materials_statistic_viewset.py b/tests/apps/companies/test_materials_statistic_viewset.py new file mode 100644 index 0000000..982246a --- /dev/null +++ b/tests/apps/companies/test_materials_statistic_viewset.py @@ -0,0 +1,290 @@ +import pytest +from datetime import datetime + +from freezegun import freeze_time +from pytest_lazyfixture import lazy_fixture as lf +from rest_framework import status + +from django.db.models import QuerySet +from django.urls import reverse +from django.utils import timezone + +from app.testing import ApiClient, StatusApiClient +from app.types import RestPageAssertion +from companies.api.serializers import MaterialsStatisticSerializer +from companies.models import Material, Point, StockMaterial +from purchases.models import UsedMaterial + +pytestmark = [pytest.mark.django_db] + + +def get_date(date: str) -> datetime: + return datetime.strptime(date, "%Y-%m-%d") + + +def test_point_non_managing_staff_cannot_read_materials_statistic( + as_point_non_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, +): + as_point_non_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-list", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + }, + ), + expected_status=as_point_non_managing_staff.expected_status, + ) + + +def test_point_managing_staff_can_read_materials_statistic( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, +): + as_point_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-list", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + }, + ), + ) + + +def test_point_managing_staff_cannot_read_materials_statistic_with_invalid_query_params( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + invalide_material_date_query_params: dict[str, str], +): + as_point_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-list", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + }, + ), + expected_status=status.HTTP_400_BAD_REQUEST, + data=invalide_material_date_query_params, + ) + + +def test_materials_statistic_list( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + assert_rest_page: RestPageAssertion, + material_date_query_params: dict[str, str], +): + materials_statistic: QuerySet[Material] = Material.objects.statistic( + point_with_materials_statistic.id, **material_date_query_params + ) + response = as_point_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-list", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + }, + ), + data=material_date_query_params, + ) + assert_rest_page(response, materials_statistic, MaterialsStatisticSerializer) + + +def test_materials_statistic_detail( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + material_date_query_params: dict[str, str], +): + material = Material.objects.statistic(point_with_materials_statistic.id, **material_date_query_params).first() + + assert material + + response = as_point_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-detail", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + "pk": material.id, + }, + ), + data=material_date_query_params, + ) + assert response == MaterialsStatisticSerializer(material).data + + +def test_materials_statistic_usage_and_stocks_sort_by_date( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + material_date_query_params: dict[str, str], +): + material = Material.objects.statistic(point_with_materials_statistic.id, **material_date_query_params).first() + + assert material + + response = as_point_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-detail", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + "pk": material.id, + }, + ), + data=material_date_query_params, + ) + + for movement in [response["stocks"], response["usage"]]: + assert all(elem1["date"] < elem2["date"] for elem1, elem2 in zip(movement, movement[1:])) + + +def test_materials_statistic_usage_and_stocks_filter_by_date( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + material_date_query_params: dict[str, str], +): + params = material_date_query_params + material = Material.objects.statistic(point_with_materials_statistic.id, **params).first() + + assert material + + response = as_point_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-detail", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + "pk": material.id, + }, + ), + data=params, + ) + for movement in [response["stocks"], response["usage"]]: + assert all( + get_date(params["date_from"]) <= get_date(elem["date"]) <= get_date(params["date_to"]) for elem in movement + ) + + +@freeze_time("2022-01-01") +def test_materials_statistic_amount( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + material_date_query_params: dict[str, str], +): + material_date_query_params["date_to"] = "2022-01-01" + material = Material.objects.statistic(point_with_materials_statistic.id, **material_date_query_params).first() + + assert material + + url = reverse( + "api_v1:companies:materials-statistic-detail", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + "pk": material.id, + }, + ) + response = as_point_managing_staff.get(url, data=material_date_query_params) # type: ignore[no-untyped-call] + stock_material = StockMaterial.objects.filter(material_id=material.id).order_by("stock__date").first() + + assert stock_material + + stock_material.stock.date = timezone.now().date() + stock_material.stock.save() + new_response = as_point_managing_staff.get(url, data=material_date_query_params) # type: ignore[no-untyped-call] + + assert int(response["stocks"][0]["amount"]) - stock_material.quantity == int(new_response["stocks"][0]["amount"]) + + +@freeze_time("2022-01-01") +def test_materials_statistic_usage_amount( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + material_date_query_params: dict[str, str], +): + material_date_query_params["date_to"] = "2022-01-01" + material = Material.objects.statistic(point_with_materials_statistic.id, **material_date_query_params).first() + + assert material + + url = reverse( + "api_v1:companies:materials-statistic-detail", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + "pk": material.id, + }, + ) + response = as_point_managing_staff.get(url, data=material_date_query_params) # type: ignore[no-untyped-call] + used_material = UsedMaterial.objects.filter(material__material_id=material.id).order_by("created").first() + + assert used_material + + used_material.created = timezone.now() + used_material.save() + new_response = as_point_managing_staff.get(url, data=material_date_query_params) # type: ignore[no-untyped-call] + + assert int(response["usage"][0]["amount"]) - used_material.amount == int(new_response["usage"][0]["amount"]) + + +@freeze_time("2000-01-31") +def test_materials_statistic_without_parameters(point_with_materials_statistic_30_days: Point): + response = ApiClient(point_with_materials_statistic_30_days.company.owner).get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-list", + kwargs={ + "company_pk": point_with_materials_statistic_30_days.company.id, + "point_pk": point_with_materials_statistic_30_days.id, + }, + ) + ) + for material in response["results"]: + for movement in [material["stocks"], material["usage"]]: + assert all(get_date("2000-01-01") <= get_date(elem["date"]) <= get_date("2001-01-31") for elem in movement) + + +@pytest.mark.parametrize( + "another_point", + [ + lf("point_without_materials_statistic"), + lf("point_with_materials_statistic_the_same_period"), + lf("point_with_materials_statistic_30_days"), + ], +) +def test_materials_statistic_different_results_for_each_point( + as_point_managing_staff: StatusApiClient, + point_with_materials_statistic: Point, + another_point: Point, + material_date_query_params: dict[str, str], + quantity_materials_in_stocks: dict[Point, dict[str, int]], +): + point_response = as_point_managing_staff.get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-list", + kwargs={ + "company_pk": point_with_materials_statistic.company.id, + "point_pk": point_with_materials_statistic.id, + }, + ), + data=material_date_query_params, + ) + + another_point_response = ApiClient(another_point.company.owner).get( # type: ignore[no-untyped-call] + reverse( + "api_v1:companies:materials-statistic-list", + kwargs={ + "company_pk": another_point.company.id, + "point_pk": another_point.id, + }, + ), + data=material_date_query_params, + ) + + for point_materials, another_point_materials in zip(point_response["results"], another_point_response["results"]): + assert len(point_materials["stocks"]) == quantity_materials_in_stocks[point_with_materials_statistic]["stocks"] + assert len(point_materials["usage"]) == quantity_materials_in_stocks[point_with_materials_statistic]["usage"] + assert len(another_point_materials["stocks"]) == quantity_materials_in_stocks[another_point]["stocks"] + assert len(another_point_materials["usage"]) == quantity_materials_in_stocks[another_point]["usage"] diff --git a/tests/fixtures/apps/companies/__init__.py b/tests/fixtures/apps/companies/__init__.py index 9418389..5d02584 100644 --- a/tests/fixtures/apps/companies/__init__.py +++ b/tests/fixtures/apps/companies/__init__.py @@ -32,6 +32,15 @@ master_procedure_reverse_kwargs, ) from tests.fixtures.apps.companies.fields import lowercase_char_field +from tests.fixtures.apps.companies.materials_statistic import ( + invalide_material_date_query_params, + material_date_query_params, + point_with_materials_statistic, + point_with_materials_statistic_30_days, + point_with_materials_statistic_the_same_period, + point_without_materials_statistic, + quantity_materials_in_stocks, +) from tests.fixtures.apps.companies.point import company_point, company_point_data, company_point_pk from tests.fixtures.apps.companies.stock import ( material, @@ -67,17 +76,24 @@ "employee_with_duplicating_department", "employee_with_non_existing_department", "employee_with_non_existing_user", + "invalide_material_date_query_params", "lowercase_char_field", "master_procedure", "master_procedure_data", "master_procedure_reverse_kwargs", "material", + "material_date_query_params", "material_type", "material_type_data", + "point_with_materials_statistic", + "point_with_materials_statistic_30_days", + "point_with_materials_statistic_the_same_period", + "point_without_materials_statistic", "procedure", "procedure_data", "procedure_reverse_kwargs", "stock", "stock_data", "stock_material", + "quantity_materials_in_stocks", ] diff --git a/tests/fixtures/apps/companies/materials_statistic.py b/tests/fixtures/apps/companies/materials_statistic.py new file mode 100644 index 0000000..3d0e737 --- /dev/null +++ b/tests/fixtures/apps/companies/materials_statistic.py @@ -0,0 +1,107 @@ +import pytest +from datetime import datetime, timedelta + +from freezegun import freeze_time + +from django.utils import timezone + +from app.testing import FixtureFactory +from companies.models import Point, Procedure + + +@pytest.fixture +def material_date_query_params() -> dict[str, str]: + return {"date_from": "2000-01-01", "date_to": "2002-01-01"} + + +@pytest.fixture( + params=[ + {"date_from": "2000-01-01", "date_to": "2222"}, + {"date_from": "2222-01-01", "date_to": "2002-01-01"}, + {"date_from": "2001-01-01", "date_to": "2001-01-01"}, + ] +) +def invalide_material_date_query_params(request: pytest.FixtureRequest) -> dict[str, str]: + return request.param + + +def create_point_with_materials_statistic( + factory: FixtureFactory, + procedure: Procedure, + now: datetime, + timedelta_stock_material: int, + quantity_stock_material: int, + quantity_used_material: int, +) -> None: + stock_materials = [] + point = procedure.department.point + for date in [now + timedelta(days=timedelta_stock_material * i) for i in range(4)]: + stock_material = factory.stock_material( + stock=factory.stock(point=point, date=date.date()), + ) + stock_materials.append(stock_material) + iter_date = [date.date()] + [date.date() + timedelta(days=i) for i in range(1, quantity_stock_material)] + for created_date in iter_date: + stock_materials.append( + factory.stock_material( + stock=factory.stock(point=point, date=created_date), + material=stock_material.material, + ) + ) + iter_datetime = [now] + [now + timedelta(days=i) for i in range(quantity_used_material)] + for created_date in iter_datetime: + for stock_material in stock_materials: + with freeze_time(created_date): + factory.used_material( + procedure=factory.purchase_procedure(procedure=procedure), + material=stock_material, + ) + + +@pytest.fixture +@freeze_time("2000-01-01 10:23:40") +def point_with_materials_statistic(factory: FixtureFactory, procedure: Procedure) -> Point: + now = timezone.now() + point = procedure.department.point + create_point_with_materials_statistic(factory, procedure, now, 100, 6, 10) + return point + + +@pytest.fixture +@freeze_time("2000-01-01 10:23:40") +def point_with_materials_statistic_the_same_period(factory: FixtureFactory) -> Point: + now = timezone.now() + procedure = factory.procedure(department=factory.department()) + point = procedure.department.point + create_point_with_materials_statistic(factory, procedure, now, 100, 5, 15) + return point + + +@pytest.fixture +@freeze_time("2000-01-01 10:23:40") +def point_with_materials_statistic_30_days(factory: FixtureFactory) -> Point: + now = timezone.now() + procedure = factory.procedure(department=factory.department()) + point = procedure.department.point + create_point_with_materials_statistic(factory, procedure, now, 15, 4, 20) + return point + + +@pytest.fixture +def point_without_materials_statistic(factory: FixtureFactory) -> Point: + return factory.company_point() + + +@pytest.fixture +def quantity_materials_in_stocks( + point_with_materials_statistic: Point, + point_with_materials_statistic_the_same_period: Point, + point_with_materials_statistic_30_days: Point, + point_without_materials_statistic: Point, +) -> dict[Point, dict[str, int]]: + return { + point_with_materials_statistic: {"stocks": 6, "usage": 10}, + point_with_materials_statistic_the_same_period: {"stocks": 5, "usage": 15}, + point_with_materials_statistic_30_days: {"stocks": 4, "usage": 20}, + point_without_materials_statistic: {"stocks": 0, "usage": 0}, + }