Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#52 #55

Open
wants to merge 19 commits into
base: main
Choose a base branch
from
Open

#52 #55

Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,6 @@ line_length = 120
"AXES_ENABLED=False",
"DEBUG=Off",
"SECRET_KEY=secret",
"DATABASE_URL=sqlite:///db.sqlite",
]
[tool.mypy]
env = [
Expand Down
2 changes: 1 addition & 1 deletion src/companies/api/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 2 additions & 1 deletion src/companies/api/serializers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -45,6 +45,7 @@
"MasterProcedureReadSerializer",
"MasterProcedureWriteSerializer",
"MaterialSerializer",
"MaterialsStatisticSerializer",
"MaterialTypeSerializer",
"PointSerializer",
"PointCreateSerializer",
Expand Down
9 changes: 9 additions & 0 deletions src/companies/api/serializers/point.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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")
5 changes: 3 additions & 2 deletions src/companies/api/viewsets/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
29 changes: 25 additions & 4 deletions src/companies/api/viewsets/point.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet

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"])
Expand All @@ -33,3 +40,17 @@ 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(ModelViewSet):
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We only need listing here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you saying that we won't have a retrieve logic at the front? Do I need to remove the retrieve method?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can use mixins.ListModelMixin and GenericViewSet.

http_method_names = ["get"]
platsajacki marked this conversation as resolved.
Show resolved Hide resolved
serializer_class = MaterialsStatisticSerializer
permission_classes = [IsCompanyOwner]

def get_queryset(self) -> QuerySet[Material]:
DateValidatorService(self.request)()
return Material.objects.statistic(
self.kwargs["point_pk"],
self.request.query_params.get("date_from"),
self.request.query_params.get("date_to"),
)
3 changes: 2 additions & 1 deletion src/companies/models/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
108 changes: 108 additions & 0 deletions src/companies/models/material.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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()
q_date_from = Q(date__gte=date_from) if date_from else Q(date__gte=(now - timedelta(days=30)))
platsajacki marked this conversation as resolved.
Show resolved Hide resolved
q_date_to = Q(date__lte=date_to) if date_to else Q(date__lte=now)
stock_queryset = (
StockMaterial.objects.select_related("stock__date")
.filter(stock__point__id=point_id)
.annotate(date=F("stock__date"))
.filter(q_date_from & q_date_to)
platsajacki marked this conversation as resolved.
Show resolved Hide resolved
)
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(q_date_from & q_date_to)
)
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")
37 changes: 0 additions & 37 deletions src/companies/models/stock.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 2 additions & 0 deletions src/companies/services/__init__.py
Original file line number Diff line number Diff line change
@@ -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",
]
5 changes: 5 additions & 0 deletions src/companies/services/materials_statistic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from companies.services.materials_statistic.validator import DateValidatorService

__all__ = [
"DateValidatorService",
]
36 changes: 36 additions & 0 deletions src/companies/services/materials_statistic/validator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Callable

from rest_framework.exceptions import ValidationError
from rest_framework.request import Request

from app.services import BaseService


@dataclass
class DateValidatorService(BaseService):
request: Request
platsajacki marked this conversation as resolved.
Show resolved Hide resolved

def valide_date_query_params(self) -> None:
query_params = self.request.query_params
if not query_params:
return
try:
date_format = "%Y-%m-%d"
date_from_str = query_params.get("date_from")
date_to_str = 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
1 change: 1 addition & 0 deletions src/companies/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Empty file added tests/apps/__init__.py
Empty file.
Empty file added tests/apps/app/__init__.py
Empty file.
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import pytest

from rest_framework.exceptions import ValidationError
from rest_framework.request import Request

from companies.services import DateValidatorService


def test_valid_date_params(mock_request: Request, material_date_query_params: dict[str, str]):
mock_request.query_params = material_date_query_params # type: ignore

assert DateValidatorService(mock_request)() is True


def test_only_date_from_param(mock_request: Request, material_date_query_params: dict[str, str]):
del material_date_query_params["date_to"]
mock_request.query_params = material_date_query_params # type: ignore

assert DateValidatorService(mock_request)() is True


def test_only_date_to_param(mock_request: Request, material_date_query_params: dict[str, str]):
del material_date_query_params["date_from"]
mock_request.query_params = material_date_query_params # type: ignore

assert DateValidatorService(mock_request)() is True


def test_invalid_date_params(mock_request: Request, invalide_material_date_query_params: dict[str, str]):
mock_request.query_params = invalide_material_date_query_params # type: ignore
platsajacki marked this conversation as resolved.
Show resolved Hide resolved
with pytest.raises(ValidationError):
DateValidatorService(mock_request)()
Loading
Loading