feat: product endpoints changes for products table
This commit is contained in:
@ -3,7 +3,7 @@ from typing import Optional
|
|||||||
from sqlalchemy import Select, select
|
from sqlalchemy import Select, select
|
||||||
from sqlalchemy.orm import joinedload, selectinload
|
from sqlalchemy.orm import joinedload, selectinload
|
||||||
|
|
||||||
from modules.fulfillment_base.models import DealProductService
|
from modules.fulfillment_base.models import DealProductService, Product
|
||||||
from modules.fulfillment_base.models.deal_product import DealProduct
|
from modules.fulfillment_base.models.deal_product import DealProduct
|
||||||
from modules.fulfillment_base.schemas.deal_product import (
|
from modules.fulfillment_base.schemas.deal_product import (
|
||||||
UpdateDealProductSchema,
|
UpdateDealProductSchema,
|
||||||
@ -25,7 +25,7 @@ class DealProductRepository(
|
|||||||
deal_id = args[0]
|
deal_id = args[0]
|
||||||
return (
|
return (
|
||||||
stmt.options(
|
stmt.options(
|
||||||
joinedload(DealProduct.product),
|
joinedload(DealProduct.product).selectinload(Product.barcodes),
|
||||||
selectinload(DealProduct.product_services).joinedload(
|
selectinload(DealProduct.product_services).joinedload(
|
||||||
DealProductService.service
|
DealProductService.service
|
||||||
),
|
),
|
||||||
|
|||||||
@ -1,15 +1,16 @@
|
|||||||
from modules.fulfillment_base.models import Product
|
from sqlalchemy import or_, delete
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
|
from modules.fulfillment_base.models import Product, ProductBarcode
|
||||||
from modules.fulfillment_base.schemas.product import (
|
from modules.fulfillment_base.schemas.product import (
|
||||||
CreateProductSchema,
|
CreateProductSchema,
|
||||||
UpdateProductSchema,
|
UpdateProductSchema,
|
||||||
)
|
)
|
||||||
from repositories.mixins import *
|
from repositories.mixins import *
|
||||||
from schemas.base import PaginationSchema
|
|
||||||
|
|
||||||
|
|
||||||
class ProductRepository(
|
class ProductRepository(
|
||||||
BaseRepository,
|
BaseRepository,
|
||||||
RepGetAllMixin[Product],
|
|
||||||
RepDeleteMixin[Product],
|
RepDeleteMixin[Product],
|
||||||
RepCreateMixin[Product, CreateProductSchema],
|
RepCreateMixin[Product, CreateProductSchema],
|
||||||
RepUpdateMixin[Product, UpdateProductSchema],
|
RepUpdateMixin[Product, UpdateProductSchema],
|
||||||
@ -18,17 +19,73 @@ class ProductRepository(
|
|||||||
entity_class = Product
|
entity_class = Product
|
||||||
entity_not_found_msg = "Товар не найден"
|
entity_not_found_msg = "Товар не найден"
|
||||||
|
|
||||||
def _process_get_all_stmt_with_args(self, stmt: Select, *args) -> Select:
|
async def get_all(
|
||||||
search_input = args[0]
|
self,
|
||||||
pagination: PaginationSchema = args[1]
|
page: Optional[int],
|
||||||
|
items_per_page: Optional[int],
|
||||||
|
client_id: Optional[int],
|
||||||
|
search_input: Optional[str],
|
||||||
|
) -> tuple[list[Product], int]:
|
||||||
|
stmt = (
|
||||||
|
select(Product)
|
||||||
|
.options(selectinload(Product.barcodes))
|
||||||
|
.where(Product.is_deleted.is_(False))
|
||||||
|
)
|
||||||
|
|
||||||
|
if client_id:
|
||||||
|
stmt = stmt.where(Product.client_id == client_id)
|
||||||
if search_input:
|
if search_input:
|
||||||
stmt = stmt.where(Product.name.ilike(f"%{search_input}%"))
|
stmt = stmt.where(
|
||||||
if pagination.items_per_page and pagination.page:
|
or_(
|
||||||
stmt = self._apply_pagination(
|
Product.name.ilike(f"%{search_input}%"),
|
||||||
stmt, pagination.page, pagination.items_per_page
|
Product.barcodes.any(
|
||||||
|
ProductBarcode.barcode.ilike(f"%{search_input}%")
|
||||||
|
),
|
||||||
|
Product.article.ilike(f"%{search_input}%"),
|
||||||
|
Product.factory_article.ilike(f"%{search_input}%"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return stmt
|
total_items = len((await self.session.execute(stmt)).all())
|
||||||
|
|
||||||
|
if page and items_per_page:
|
||||||
|
stmt = self._apply_pagination(stmt, page, items_per_page)
|
||||||
|
|
||||||
|
result = await self.session.execute(stmt)
|
||||||
|
return list(result.scalars().all()), total_items
|
||||||
|
|
||||||
|
def _process_get_by_id_stmt(self, stmt: Select) -> Select:
|
||||||
|
return stmt.options(selectinload(Product.barcodes))
|
||||||
|
|
||||||
|
async def _after_create(self, product: Product, data: CreateProductSchema) -> None:
|
||||||
|
new_barcodes = [
|
||||||
|
ProductBarcode(product_id=product.id, barcode=barcode)
|
||||||
|
for barcode in data.barcodes
|
||||||
|
]
|
||||||
|
self.session.add_all(new_barcodes)
|
||||||
|
|
||||||
|
async def _update_barcodes(self, product: Product, new_barcodes: list[str]):
|
||||||
|
new_barcodes_set: set[str] = set(new_barcodes)
|
||||||
|
old_barcodes_set: set[str] = set(obj.barcode for obj in product.barcodes)
|
||||||
|
barcodes_to_add = new_barcodes_set - old_barcodes_set
|
||||||
|
barcodes_to_delete = old_barcodes_set - new_barcodes_set
|
||||||
|
|
||||||
|
del_stmt = delete(ProductBarcode).where(
|
||||||
|
ProductBarcode.product_id == product.id,
|
||||||
|
ProductBarcode.barcode.in_(barcodes_to_delete),
|
||||||
|
)
|
||||||
|
await self.session.execute(del_stmt)
|
||||||
|
|
||||||
|
new_barcodes = [
|
||||||
|
ProductBarcode(product_id=product.id, barcode=barcode)
|
||||||
|
for barcode in barcodes_to_add
|
||||||
|
]
|
||||||
|
self.session.add_all(new_barcodes)
|
||||||
|
await self.session.commit()
|
||||||
|
await self.session.refresh(product)
|
||||||
|
|
||||||
async def update(self, product: Product, data: UpdateProductSchema) -> Product:
|
async def update(self, product: Product, data: UpdateProductSchema) -> Product:
|
||||||
|
if data.barcodes is not None:
|
||||||
|
await self._update_barcodes(product, data.barcodes)
|
||||||
|
del data.barcodes
|
||||||
return await self._apply_update_data_to_model(product, data, True)
|
return await self._apply_update_data_to_model(product, data, True)
|
||||||
|
|||||||
@ -15,9 +15,10 @@ router = APIRouter(tags=["product"])
|
|||||||
async def get_products(
|
async def get_products(
|
||||||
session: SessionDependency,
|
session: SessionDependency,
|
||||||
pagination: PaginationDependency,
|
pagination: PaginationDependency,
|
||||||
|
client_id: Optional[int] = Query(alias="clientId", default=None),
|
||||||
search_input: Optional[str] = Query(alias="searchInput", default=None),
|
search_input: Optional[str] = Query(alias="searchInput", default=None),
|
||||||
):
|
):
|
||||||
return await ProductService(session).get_all(search_input, pagination)
|
return await ProductService(session).get_all(pagination, client_id, search_input)
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
|
|||||||
@ -1,6 +1,10 @@
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from schemas.base import BaseSchema, BaseResponse
|
from pydantic import field_validator
|
||||||
|
|
||||||
|
from modules.fulfillment_base.models import ProductBarcode
|
||||||
|
from modules.fulfillment_base.schemas.barcode_template import BarcodeTemplateSchema
|
||||||
|
from schemas.base import BaseSchema, BaseResponse, PaginationInfoSchema
|
||||||
|
|
||||||
|
|
||||||
# region Entity
|
# region Entity
|
||||||
@ -16,26 +20,38 @@ class CreateProductSchema(BaseSchema):
|
|||||||
name: str
|
name: str
|
||||||
article: str
|
article: str
|
||||||
factory_article: str
|
factory_article: str
|
||||||
|
client_id: int
|
||||||
|
barcode_template_id: int
|
||||||
brand: Optional[str]
|
brand: Optional[str]
|
||||||
color: Optional[str]
|
color: Optional[str]
|
||||||
composition: Optional[str]
|
composition: Optional[str]
|
||||||
size: Optional[str]
|
size: Optional[str]
|
||||||
additional_info: Optional[str]
|
additional_info: Optional[str]
|
||||||
|
barcodes: list[str]
|
||||||
|
|
||||||
|
|
||||||
class ProductSchema(CreateProductSchema):
|
class ProductSchema(CreateProductSchema):
|
||||||
id: int
|
id: int
|
||||||
|
barcode_template: BarcodeTemplateSchema
|
||||||
|
|
||||||
|
@field_validator("barcodes", mode="before")
|
||||||
|
def barcodes_to_list(cls, v: Optional[list[ProductBarcode]]):
|
||||||
|
if isinstance(v, list):
|
||||||
|
return [barcode.barcode for barcode in v]
|
||||||
|
return v
|
||||||
|
|
||||||
|
|
||||||
class UpdateProductSchema(BaseSchema):
|
class UpdateProductSchema(BaseSchema):
|
||||||
name: Optional[str] = None
|
name: Optional[str] = None
|
||||||
article: Optional[str] = None
|
article: Optional[str] = None
|
||||||
factory_article: Optional[str] = None
|
factory_article: Optional[str] = None
|
||||||
|
barcode_template_id: Optional[int] = None
|
||||||
brand: Optional[str] = None
|
brand: Optional[str] = None
|
||||||
color: Optional[str] = None
|
color: Optional[str] = None
|
||||||
composition: Optional[str] = None
|
composition: Optional[str] = None
|
||||||
size: Optional[str] = None
|
size: Optional[str] = None
|
||||||
additional_info: Optional[str] = None
|
additional_info: Optional[str] = None
|
||||||
|
barcodes: Optional[list[str]] = None
|
||||||
|
|
||||||
images: list[ProductImageSchema] | None = []
|
images: list[ProductImageSchema] | None = []
|
||||||
|
|
||||||
@ -60,6 +76,7 @@ class UpdateProductRequest(BaseSchema):
|
|||||||
|
|
||||||
class GetProductsResponse(BaseSchema):
|
class GetProductsResponse(BaseSchema):
|
||||||
items: list[ProductSchema]
|
items: list[ProductSchema]
|
||||||
|
pagination_info: PaginationInfoSchema
|
||||||
|
|
||||||
|
|
||||||
class CreateProductResponse(BaseResponse):
|
class CreateProductResponse(BaseResponse):
|
||||||
|
|||||||
@ -1,15 +1,17 @@
|
|||||||
|
import math
|
||||||
|
|
||||||
from modules.fulfillment_base.models import Product
|
from modules.fulfillment_base.models import Product
|
||||||
from modules.fulfillment_base.repositories import ProductRepository
|
from modules.fulfillment_base.repositories import ProductRepository
|
||||||
from modules.fulfillment_base.schemas.product import (
|
from modules.fulfillment_base.schemas.product import (
|
||||||
CreateProductRequest,
|
CreateProductRequest,
|
||||||
ProductSchema,
|
ProductSchema,
|
||||||
UpdateProductRequest,
|
UpdateProductRequest, GetProductsResponse,
|
||||||
)
|
)
|
||||||
|
from schemas.base import PaginationSchema, PaginationInfoSchema
|
||||||
from services.mixins import *
|
from services.mixins import *
|
||||||
|
|
||||||
|
|
||||||
class ProductService(
|
class ProductService(
|
||||||
ServiceGetAllMixin[Product, ProductSchema],
|
|
||||||
ServiceCreateMixin[Product, CreateProductRequest, ProductSchema],
|
ServiceCreateMixin[Product, CreateProductRequest, ProductSchema],
|
||||||
ServiceUpdateMixin[Product, UpdateProductRequest],
|
ServiceUpdateMixin[Product, UpdateProductRequest],
|
||||||
ServiceDeleteMixin[Product],
|
ServiceDeleteMixin[Product],
|
||||||
@ -22,5 +24,27 @@ class ProductService(
|
|||||||
def __init__(self, session: AsyncSession):
|
def __init__(self, session: AsyncSession):
|
||||||
self.repository = ProductRepository(session)
|
self.repository = ProductRepository(session)
|
||||||
|
|
||||||
|
async def get_all(
|
||||||
|
self,
|
||||||
|
pagination: PaginationSchema,
|
||||||
|
*filters,
|
||||||
|
) -> GetProductsResponse:
|
||||||
|
products, total_items = await self.repository.get_all(
|
||||||
|
pagination.page,
|
||||||
|
pagination.items_per_page,
|
||||||
|
*filters,
|
||||||
|
)
|
||||||
|
|
||||||
|
total_pages = 1
|
||||||
|
if pagination.items_per_page:
|
||||||
|
total_pages = math.ceil(total_items / pagination.items_per_page)
|
||||||
|
|
||||||
|
return GetProductsResponse(
|
||||||
|
items=[ProductSchema.model_validate(product) for product in products],
|
||||||
|
pagination_info=PaginationInfoSchema(
|
||||||
|
total_pages=total_pages, total_items=total_items
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
async def is_soft_delete(self, product: ProductSchema) -> bool:
|
async def is_soft_delete(self, product: ProductSchema) -> bool:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@ -40,9 +40,19 @@ class RepDeleteMixin(Generic[EntityType], RepBaseMixin[EntityType]):
|
|||||||
class RepCreateMixin(Generic[EntityType, CreateSchemaType], RepBaseMixin[EntityType]):
|
class RepCreateMixin(Generic[EntityType, CreateSchemaType], RepBaseMixin[EntityType]):
|
||||||
entity_class: Type[EntityType]
|
entity_class: Type[EntityType]
|
||||||
|
|
||||||
|
async def _prepare_create(self, data: CreateSchemaType) -> dict:
|
||||||
|
return data.model_dump()
|
||||||
|
|
||||||
|
async def _after_create(self, obj: EntityType, data: CreateSchemaType) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
async def create(self, data: CreateSchemaType) -> int:
|
async def create(self, data: CreateSchemaType) -> int:
|
||||||
obj = self.entity_class(**data.model_dump())
|
prepared_data = await self._prepare_create(data)
|
||||||
|
obj = self.entity_class(**prepared_data)
|
||||||
self.session.add(obj)
|
self.session.add(obj)
|
||||||
|
await self.session.flash()
|
||||||
|
await self.session.refresh(obj)
|
||||||
|
await self._after_create(obj, data)
|
||||||
await self.session.commit()
|
await self.session.commit()
|
||||||
await self.session.refresh(obj)
|
await self.session.refresh(obj)
|
||||||
return obj.id
|
return obj.id
|
||||||
|
|||||||
Reference in New Issue
Block a user