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_id = Column(Integer, ForeignKey('clients.id'), nullable=False, comment='ID сделки')
client = relationship('Client', back_populates='products') 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): 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_id = Column(Integer, ForeignKey('products.id'), nullable=False, comment='ID товара', primary_key=True)
product = relationship('Product', back_populates='barcodes') 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( @product_router.post(
'/create', '/create',
response_model=ProductCreateResponse response_model=ProductCreateResponse,
operation_id='create_product'
) )
async def create_product( async def create_product(
request: ProductCreateRequest, request: ProductCreateRequest,
@@ -26,6 +27,28 @@ async def create_product(
return await ProductService(session).create(request) 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( @product_router.get(
'/get', '/get',
response_model=ProductGetResponse, 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 auth_date: int
first_name: str first_name: str
hash: str hash: str
@@ -9,5 +9,5 @@ class AuthLoginRequest(CustomModel):
photo_url: str photo_url: str
class AuthLoginResponse(CustomModel): class AuthLoginResponse(CustomModelCamel):
access_token: str access_token: str

View File

@@ -1,4 +1,5 @@
from pydantic import BaseModel from pydantic import BaseModel
from pydantic.alias_generators import to_camel
class CustomConfig: class CustomConfig:
@@ -6,21 +7,34 @@ class CustomConfig:
from_attributes = True 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: class Config:
from_attributes = True from_attributes = True
class OkMessageSchema(BaseModel): class OkMessageSchema(CustomModelCamel):
ok: bool ok: bool
message: str message: str
class PaginationSchema(CustomModel): class PaginationSchema(CustomModelCamel):
page: int page: int
items_per_page: int items_per_page: int
class PaginationInfoSchema(CustomModel): class PaginationInfoSchema(CustomModelCamel):
total_pages: int total_pages: int
total_items: int total_items: int

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,8 @@
from fastapi import HTTPException 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 schemas.base import PaginationSchema
from services.base import BaseService from services.base import BaseService
from schemas.product import * from schemas.product import *
@@ -9,6 +10,7 @@ from schemas.product import *
class ProductService(BaseService): class ProductService(BaseService):
async def create(self, request: ProductCreateRequest) -> ProductCreateResponse: async def create(self, request: ProductCreateRequest) -> ProductCreateResponse:
# Unique article validation
existing_product_query = await self.session.execute( existing_product_query = await self.session.execute(
select(Product) select(Product)
.where(Product.client_id == request.client_id, .where(Product.client_id == request.client_id,
@@ -16,22 +18,101 @@ class ProductService(BaseService):
) )
existing_product = existing_product_query.first() existing_product = existing_product_query.first()
if existing_product: if existing_product:
raise HTTPException(status_code=403, detail="Product already exists") return ProductCreateResponse(ok=False, message='Товар с таким артикулом уже существует у клиента')
product = Product(**request.dict())
# Creating product
product_dict = request.dict()
del product_dict['barcodes']
product = Product(**product_dict)
self.session.add(product) 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() 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: async def get_by_client_id(self, client_id: int, pagination: PaginationSchema) -> ProductGetResponse:
query = await self.session.execute( stmt = (
select(Product) select(Product)
.options(selectinload(Product.barcodes))
.where(Product.client_id == client_id) .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) .offset(pagination.page * pagination.items_per_page)
.limit(pagination.items_per_page) .limit(pagination.items_per_page)
) )
products: list[ProductSchema] = [] products: list[ProductSchema] = []
for product in query.scalars().all(): 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( 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())