This commit is contained in:
2024-03-31 07:36:35 +03:00
parent df6e2e7fb1
commit 5de5b9b3e4
12 changed files with 1469 additions and 56 deletions

View File

@@ -12,7 +12,7 @@ class Product(BaseModel):
client_id = Column(Integer, ForeignKey('clients.id'), nullable=False, comment='ID сделки')
client = relationship('Client', back_populates='products')
barcodes = relationship('ProductBarcode', back_populates='product')
barcodes = relationship('ProductBarcode', back_populates='product', cascade="all, delete-orphan")
class ProductBarcode(BaseModel):
@@ -20,4 +20,4 @@ class ProductBarcode(BaseModel):
product_id = Column(Integer, ForeignKey('products.id'), nullable=False, comment='ID товара', primary_key=True)
product = relationship('Product', back_populates='barcodes')
barcode = Column(String, nullable=False, index=True, comment='ШК товара')
barcode = Column(String, nullable=False, index=True, comment='ШК товара', primary_key=True)

View File

@@ -17,7 +17,8 @@ product_router = APIRouter(
@product_router.post(
'/create',
response_model=ProductCreateResponse
response_model=ProductCreateResponse,
operation_id='create_product'
)
async def create_product(
request: ProductCreateRequest,
@@ -26,6 +27,28 @@ async def create_product(
return await ProductService(session).create(request)
@product_router.post(
'/delete',
response_model=ProductDeleteResponse,
operation_id='delete_product'
)
async def delete_product(
request: ProductDeleteRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await ProductService(session).delete(request)
@product_router.post(
'/update',
response_model=ProductUpdateResponse,
operation_id='update_product'
)
async def delete_product(
request: ProductUpdateRequest,
session: Annotated[AsyncSession, Depends(get_session)]
):
return await ProductService(session).update(request)
@product_router.get(
'/get',
response_model=ProductGetResponse,

View File

@@ -1,7 +1,7 @@
from schemas.base import CustomModel
from schemas.base import CustomModelCamel, CustomModelSnake
class AuthLoginRequest(CustomModel):
class AuthLoginRequest(CustomModelSnake):
auth_date: int
first_name: str
hash: str
@@ -9,5 +9,5 @@ class AuthLoginRequest(CustomModel):
photo_url: str
class AuthLoginResponse(CustomModel):
class AuthLoginResponse(CustomModelCamel):
access_token: str

View File

@@ -1,4 +1,5 @@
from pydantic import BaseModel
from pydantic.alias_generators import to_camel
class CustomConfig:
@@ -6,21 +7,34 @@ class CustomConfig:
from_attributes = True
class CustomModel(BaseModel):
class CustomModelCamel(BaseModel):
class Config:
from_attributes = True
alias_generator = to_camel
populate_by_name = True
@classmethod
def from_sql_model(cls, model, fields: dict):
model_dict = {c.name: getattr(model, c.name) for c in model.__table__.columns}
model_dict.update(fields)
return cls(**model_dict)
class CustomModelSnake(BaseModel):
class Config:
from_attributes = True
class OkMessageSchema(BaseModel):
class OkMessageSchema(CustomModelCamel):
ok: bool
message: str
class PaginationSchema(CustomModel):
class PaginationSchema(CustomModelCamel):
page: int
items_per_page: int
class PaginationInfoSchema(CustomModel):
class PaginationInfoSchema(CustomModelCamel):
total_pages: int
total_items: int

View File

@@ -1,42 +1,42 @@
from typing import List
from schemas.base import CustomModel
from schemas.base import CustomModelCamel
class ClientDetailsSchema(CustomModel):
class ClientDetailsSchema(CustomModelCamel):
address: str | None = None
phone_number: str | None = None
inn: int | None = None
email: str | None = None
class ClientSchema(CustomModel):
class ClientSchema(CustomModelCamel):
id: int
name: str
details: ClientDetailsSchema | None = None
class ClientSearchRequest(CustomModel):
class ClientSearchRequest(CustomModelCamel):
name: str
class ClientCreateRequest(CustomModel):
class ClientCreateRequest(CustomModelCamel):
name: str
address: str
class ClientSearchResponse(CustomModel):
class ClientSearchResponse(CustomModelCamel):
clients: List[ClientSchema]
class ClientUpdateDetailsRequest(CustomModel):
class ClientUpdateDetailsRequest(CustomModelCamel):
client_id: int
details: ClientDetailsSchema
class ClientUpdateDetailsResponse(CustomModel):
class ClientUpdateDetailsResponse(CustomModelCamel):
ok: bool
class ClientGetAllResponse(CustomModel):
class ClientGetAllResponse(CustomModelCamel):
clients: List[ClientSchema]

View File

@@ -1,19 +1,19 @@
import datetime
from typing import List
from schemas.base import CustomModel
from schemas.base import CustomModelCamel
from schemas.client import ClientSchema
# region Entities
class FastDeal(CustomModel):
class FastDeal(CustomModelCamel):
name: str
client: ClientSchema
comment: str
acceptance_date: datetime.datetime
class DealSummary(CustomModel):
class DealSummary(CustomModelCamel):
id: int
name: str
client_name: str
@@ -22,7 +22,7 @@ class DealSummary(CustomModel):
total_price: int
class DealServiceSchema(CustomModel):
class DealServiceSchema(CustomModelCamel):
id: int
quantity: int
@@ -30,16 +30,16 @@ class DealServiceSchema(CustomModel):
# endregion Entities
# region Requests
class DealChangeStatusRequest(CustomModel):
class DealChangeStatusRequest(CustomModelCamel):
deal_id: int
new_status: int
class DealCreateRequest(CustomModel):
class DealCreateRequest(CustomModelCamel):
name: str
class DealQuickCreateRequest(CustomModel):
class DealQuickCreateRequest(CustomModelCamel):
name: str
client_name: str
client_address: str
@@ -47,11 +47,11 @@ class DealQuickCreateRequest(CustomModel):
acceptance_date: datetime.datetime
class DealSummaryRequest(CustomModel):
class DealSummaryRequest(CustomModelCamel):
pass
class DealAddServicesRequest(CustomModel):
class DealAddServicesRequest(CustomModelCamel):
deal_id: int
services: list[DealServiceSchema]
@@ -60,23 +60,23 @@ class DealAddServicesRequest(CustomModel):
# region Responses
class DealChangeStatusResponse(CustomModel):
class DealChangeStatusResponse(CustomModelCamel):
ok: bool
class DealCreateResponse(CustomModel):
class DealCreateResponse(CustomModelCamel):
ok: bool
class DealQuickCreateResponse(CustomModel):
class DealQuickCreateResponse(CustomModelCamel):
deal_id: int
class DealSummaryResponse(CustomModel):
class DealSummaryResponse(CustomModelCamel):
summaries: List[DealSummary]
class DealAddServicesResponse(CustomModel):
class DealAddServicesResponse(CustomModelCamel):
ok: bool
message: str
# endregion Responses

View File

@@ -1,33 +1,51 @@
from typing import List
from schemas.base import CustomModel, PaginationInfoSchema
from schemas.base import CustomModelCamel, PaginationInfoSchema, OkMessageSchema
# region Entities
class ProductSchema(CustomModel):
class ProductSchema(CustomModelCamel):
id: int
name: str
article: str
client_id: int
barcodes: list[str]
# endregion
# region Requests
class ProductCreateRequest(CustomModel):
class ProductCreateRequest(CustomModelCamel):
name: str
article: str
client_id: int
barcodes: List[str]
class ProductDeleteRequest(CustomModelCamel):
product_id: int
class ProductUpdateRequest(CustomModelCamel):
product: ProductSchema
# endregion
# region Responses
class ProductCreateResponse(CustomModel):
product_id: int
class ProductCreateResponse(OkMessageSchema):
product_id: int | None = None
class ProductGetResponse(CustomModel):
class ProductGetResponse(CustomModelCamel):
products: List[ProductSchema]
pagination_info: PaginationInfoSchema
class ProductDeleteResponse(OkMessageSchema):
pass
class ProductUpdateResponse(OkMessageSchema):
pass
# endregion

View File

@@ -1,15 +1,15 @@
from typing import List
from schemas.base import CustomModel, OkMessageSchema
from schemas.base import CustomModelCamel, OkMessageSchema
# region Entities
class ServiceCategorySchema(CustomModel):
class ServiceCategorySchema(CustomModelCamel):
id: int
name: str
class ServiceSchema(CustomModel):
class ServiceSchema(CustomModelCamel):
id: int
name: str
category: ServiceCategorySchema
@@ -20,11 +20,11 @@ class ServiceSchema(CustomModel):
# region Requests
class ServiceCreateRequest(CustomModel):
class ServiceCreateRequest(CustomModelCamel):
service: ServiceSchema
class ServiceCreateCategoryRequest(CustomModel):
class ServiceCreateCategoryRequest(CustomModelCamel):
category: ServiceCategorySchema
@@ -32,11 +32,11 @@ class ServiceCreateCategoryRequest(CustomModel):
# region Responses
class ServiceGetAllResponse(CustomModel):
class ServiceGetAllResponse(CustomModelCamel):
services: List[ServiceSchema]
class ServiceGetAllCategoriesResponse(CustomModel):
class ServiceGetAllCategoriesResponse(CustomModelCamel):
categories: List[ServiceCategorySchema]

View File

@@ -1,7 +1,7 @@
from typing import Union, Annotated
from fastapi import Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, HTTPBearer, HTTPAuthorizationCredentials
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import jwt, JWTError
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
@@ -10,8 +10,8 @@ from starlette import status
import backend.config
from backend.session import get_session
from models import User
from services.base import BaseService
from schemas.auth import *
from services.base import BaseService
oauth2_schema = HTTPBearer()
algorithm = 'HS256'

View File

@@ -1,7 +1,8 @@
from fastapi import HTTPException
from sqlalchemy import select
from sqlalchemy import select, func, Integer, update
from sqlalchemy.orm import selectinload
from models.product import Product
from models.product import Product, ProductBarcode
from schemas.base import PaginationSchema
from services.base import BaseService
from schemas.product import *
@@ -9,6 +10,7 @@ from schemas.product import *
class ProductService(BaseService):
async def create(self, request: ProductCreateRequest) -> ProductCreateResponse:
# Unique article validation
existing_product_query = await self.session.execute(
select(Product)
.where(Product.client_id == request.client_id,
@@ -16,22 +18,101 @@ class ProductService(BaseService):
)
existing_product = existing_product_query.first()
if existing_product:
raise HTTPException(status_code=403, detail="Product already exists")
product = Product(**request.dict())
return ProductCreateResponse(ok=False, message='Товар с таким артикулом уже существует у клиента')
# Creating product
product_dict = request.dict()
del product_dict['barcodes']
product = Product(**product_dict)
self.session.add(product)
# Creating barcodes
await self.session.flush()
for barcode in request.barcodes:
product_barcode = ProductBarcode(product_id=product.id,
barcode=barcode)
self.session.add(product_barcode)
await self.session.flush()
await self.session.commit()
return ProductCreateResponse(product_id=product.id)
return ProductCreateResponse(ok=True, message='Товар успешно создан', product_id=product.id)
async def delete(self, request: ProductDeleteRequest):
product = await self.session.get(Product, request.product_id)
if not product:
return ProductDeleteResponse(ok=False, message='Указанного товара не существует')
await self.session.delete(product)
await self.session.commit()
return ProductDeleteResponse(ok=True, message="Товар успешно удален!")
async def update(self, request: ProductUpdateRequest):
stmt = (
select(Product)
.where(Product.id == request.product.id)
.options(selectinload(Product.barcodes))
)
product_query = await self.session.execute(stmt)
product = product_query.scalar()
if not product:
return ProductUpdateResponse(ok=False, message='Указанного товара не существует')
product_dict = request.product.dict()
del product_dict['id']
del product_dict['barcodes']
await self.session.execute(
update(Product)
.where(Product.id == request.product.id)
.values(**product_dict)
)
# Updating barcodes
product_barcodes = set([barcode for barcode in product.barcodes])
request_barcodes = set(request.product.barcodes)
new_barcodes = request_barcodes.difference(product_barcodes)
deleted_barcodes = product_barcodes.difference(request_barcodes)
for product_barcode in product.barcodes:
if product_barcode not in deleted_barcodes:
continue
await self.session.delete(product_barcode)
for new_barcode in new_barcodes:
product_barcode = ProductBarcode(
product_id=product.id,
barcode=new_barcode
)
self.session.add(product_barcode)
await self.session.flush()
await self.session.commit()
return ProductUpdateResponse(ok=True, message='Товар успешно обновлен')
async def get_by_client_id(self, client_id: int, pagination: PaginationSchema) -> ProductGetResponse:
query = await self.session.execute(
stmt = (
select(Product)
.options(selectinload(Product.barcodes))
.where(Product.client_id == client_id)
.order_by(Product.id)
)
total_products_query = await self.session.execute(
select(
func.cast(func.ceil(func.count() / pagination.items_per_page), Integer),
func.count()
)
.select_from(stmt.subquery())
)
total_pages, total_items = total_products_query.first()
pagination_info = PaginationInfoSchema(total_pages=total_pages, total_items=total_items)
query = await self.session.execute(
stmt
.offset(pagination.page * pagination.items_per_page)
.limit(pagination.items_per_page)
)
products: list[ProductSchema] = []
for product in query.scalars().all():
product: Product
barcodes = []
for barcode_obj in product.barcodes:
barcode_obj: ProductBarcode
barcodes.append(barcode_obj.barcode)
products.append(
ProductSchema.model_validate(product)
ProductSchema.from_sql_model(product, {'barcodes': barcodes})
)
return ProductGetResponse(products=products)
return ProductGetResponse(products=products, pagination_info=pagination_info)

1241
test/test.json Normal file

File diff suppressed because it is too large Load Diff

36
test/test.py Normal file
View File

@@ -0,0 +1,36 @@
import asyncio
from sqlalchemy.ext.asyncio import AsyncSession
from backend.session import session_maker
from models import Product, ProductBarcode
async def main(session: AsyncSession):
client_ids = [2, 4]
for client_id in client_ids:
for i in range(1, 500 + 1):
product = Product(
name=f"Товар №{i}",
article=f"Ариткул товара №{i}",
client_id=client_id
)
session.add(product)
await session.flush()
for j in range(1, 5 + 1):
barcode = ProductBarcode(
barcode=f"Штрихкод №{j} для товара №{i}",
product_id=product.id
)
session.add(barcode)
await session.flush()
await session.commit()
async def preload():
async with session_maker() as session:
await main(session)
if __name__ == '__main__':
asyncio.run(preload())