diff --git a/enums/base_marketplace.py b/enums/base_marketplace.py new file mode 100644 index 0000000..a441c59 --- /dev/null +++ b/enums/base_marketplace.py @@ -0,0 +1,7 @@ +from enum import StrEnum + + +class BaseMarketplace(StrEnum): + WILDBERRIES = 'wb' + OZON = 'ozon' + YANDEX_MARKET = 'ym' diff --git a/main.py b/main.py index 6ce2788..c863c79 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,9 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware import platform + +from starlette.staticfiles import StaticFiles + import routers origins = [ @@ -36,6 +39,9 @@ routers_list = [ routers.position_router, routers.user_router, routers.role_router, + routers.marketplace_router, ] for router in routers_list: app.include_router(router) + +app.mount("/static", StaticFiles(directory="static"), name="static") diff --git a/models/__init__.py b/models/__init__.py index 99e304e..e9e05d3 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -8,5 +8,6 @@ from .product import * from .secondary import * from .barcode import * from .shipping_warehouse import * +from .marketplace import * configure_mappers() diff --git a/models/auth.py b/models/auth.py index 048048a..84c1b60 100644 --- a/models/auth.py +++ b/models/auth.py @@ -1,6 +1,7 @@ from sqlalchemy import BigInteger, Table, ForeignKey, Column from sqlalchemy.orm import Mapped, mapped_column, relationship +from enums.user import UserRole from models.base import BaseModel role_permissions = Table( @@ -55,7 +56,7 @@ class User(BaseModel): is_blocked: Mapped[bool] = mapped_column(nullable=False, server_default='0') is_deleted: Mapped[bool] = mapped_column(nullable=False, server_default='0') - role_key: Mapped[int] = mapped_column(ForeignKey('roles.key')) + role_key: Mapped[int] = mapped_column(ForeignKey('roles.key'), server_default=UserRole.user) role: Mapped["Role"] = relationship( 'Role', lazy='joined' diff --git a/models/deal.py b/models/deal.py index 95b71ee..65bbd08 100644 --- a/models/deal.py +++ b/models/deal.py @@ -4,6 +4,7 @@ from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean from sqlalchemy.orm import relationship, backref, Mapped, mapped_column from models.base import BaseModel +from .marketplace import BaseMarketplace from .shipping_warehouse import ShippingWarehouse @@ -36,9 +37,22 @@ class Deal(BaseModel): shipping_warehouse_id: Mapped[int] = mapped_column(ForeignKey('shipping_warehouses.id'), nullable=True) shipping_warehouse: Mapped["ShippingWarehouse"] = relationship() - services = relationship('DealService', back_populates='deal', cascade="all, delete-orphan") + base_marketplace_key: Mapped[str] = mapped_column(ForeignKey("base_marketplaces.key"), nullable=True) + base_marketplace: Mapped["BaseMarketplace"] = relationship(lazy="joined") - products = relationship('DealProduct', back_populates='deal', cascade="all, delete-orphan") + services = relationship( + 'DealService', + back_populates='deal', + cascade="all, delete-orphan", + order_by="desc(DealService.service_id)" + ) + + products = relationship( + 'DealProduct', + back_populates='deal', + cascade="all, delete-orphan", + order_by="desc(DealProduct.product_id)" + ) # TODO remake with sequence lexorank = Column(String, nullable=False, comment='Lexorank', index=True) diff --git a/models/marketplace.py b/models/marketplace.py new file mode 100644 index 0000000..e5d6329 --- /dev/null +++ b/models/marketplace.py @@ -0,0 +1,11 @@ +from sqlalchemy.orm import Mapped, mapped_column + +from models import BaseModel + + +class BaseMarketplace(BaseModel): + __tablename__ = 'base_marketplaces' + key: Mapped[str] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column() + + icon_url: Mapped[str] = mapped_column() diff --git a/models/secondary.py b/models/secondary.py index 0323df2..10ac177 100644 --- a/models/secondary.py +++ b/models/secondary.py @@ -1,7 +1,31 @@ -from sqlalchemy import Table, Column, Integer, ForeignKey, Boolean, and_, ForeignKeyConstraint -from sqlalchemy.orm import relationship, foreign, remote +from sqlalchemy import Table, Column, Integer, ForeignKey, ForeignKeyConstraint, UniqueConstraint +from sqlalchemy.orm import relationship -from models.base import metadata, BaseModel +from models.base import BaseModel + +deal_product_service_employees = Table( + 'deal_product_service_employees', + BaseModel.metadata, + Column('deal_id', primary_key=True), + Column('service_id', primary_key=True), + Column('product_id', primary_key=True), + Column('user_id', ForeignKey('users.id'), primary_key=True), + ForeignKeyConstraint( + ['deal_id', 'product_id', 'service_id'], + ['deal_product_services.deal_id', 'deal_product_services.product_id', 'deal_product_services.service_id'] + ) +) +deal_service_employees = Table( + 'deal_service_employees', + BaseModel.metadata, + Column('deal_id', primary_key=True), + Column('service_id', primary_key=True), + Column('user_id', ForeignKey('users.id'), primary_key=True), + ForeignKeyConstraint( + ['deal_id', 'service_id'], + ['deal_services.deal_id', 'deal_services.service_id'] + ) +) class DealService(BaseModel): @@ -16,14 +40,23 @@ class DealService(BaseModel): service = relationship('Service') quantity = Column(Integer, nullable=False, comment='Кол-во услуги') - price = Column(Integer, nullable=False,server_default='0', comment='Цена услуги') + price = Column(Integer, nullable=False, server_default='0', comment='Цена услуги') + + employees = relationship('User', secondary=deal_service_employees) + + __table_args__ = ( + UniqueConstraint('deal_id', 'service_id', name='uix_deal_service'), + ) class DealProductService(BaseModel): __tablename__ = 'deal_product_services' deal_id = Column(Integer, primary_key=True, nullable=False, comment='ID Сделки') + product_id = Column(Integer, primary_key=True, nullable=False, comment='ID Продукта') + service_id = Column(Integer, ForeignKey('services.id'), primary_key=True, nullable=False, comment='ID Услуги') + price = Column(Integer, nullable=False, comment='Цена услуги') deal_product = relationship('DealProduct', @@ -31,9 +64,15 @@ class DealProductService(BaseModel): primaryjoin="and_(DealProductService.deal_id == DealProduct.deal_id, " "DealProductService.product_id == DealProduct.product_id)", foreign_keys=[deal_id, product_id]) + service = relationship('Service', foreign_keys=[service_id], - lazy='joined') + lazy='joined' + + ) + employees = relationship('User', + secondary=deal_product_service_employees, + ) __table_args__ = ( ForeignKeyConstraint( @@ -52,9 +91,11 @@ class DealProduct(BaseModel): deal = relationship('Deal', back_populates='products', foreign_keys=[deal_id]) - product = relationship('Product', - lazy='joined', - foreign_keys=[product_id]) + product = relationship( + 'Product', + lazy='joined', + foreign_keys=[product_id], + ) services = relationship('DealProductService', back_populates='deal_product', @@ -62,7 +103,8 @@ class DealProduct(BaseModel): primaryjoin="and_(DealProductService.deal_id == DealProduct.deal_id, " "DealProductService.product_id == DealProduct.product_id)", foreign_keys=[DealProductService.deal_id, DealProductService.product_id], - lazy='joined' + lazy='joined', + order_by="desc(DealProductService.service_id)" ) diff --git a/routers/__init__.py b/routers/__init__.py index df3f9cc..2c8fa1d 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -8,3 +8,4 @@ from .shipping_warehouse import shipping_warehouse_router from .position import position_router from .user import user_router from .role import role_router +from .marketplace import marketplace_router diff --git a/routers/marketplace.py b/routers/marketplace.py new file mode 100644 index 0000000..80f8480 --- /dev/null +++ b/routers/marketplace.py @@ -0,0 +1,21 @@ +from fastapi import APIRouter + +from backend.dependecies import SessionDependency +from schemas.marketplace import * +from services.marketplace import MarketplaceService + +marketplace_router = APIRouter( + prefix="/marketplace", + tags=["marketplace"] +) + + +@marketplace_router.get( + '/base/get-all', + operation_id='get_all_base_marketplaces', + response_model=GetAllBaseMarketplacesResponse +) +async def get_all( + session: SessionDependency +): + return await MarketplaceService(session).get_all_base_marketplaces() diff --git a/routers/position.py b/routers/position.py index f99b0e7..d8dba40 100644 --- a/routers/position.py +++ b/routers/position.py @@ -31,3 +31,15 @@ async def create( request: CreatePositionRequest ): return await PositionService(session).create(request) + + +@position_router.post( + '/delete', + operation_id='delete_position', + response_model=DeletePositionResponse +) +async def delete( + session: SessionDependency, + request: DeletePositionRequest +): + return await PositionService(session).delete(request) diff --git a/schemas/deal.py b/schemas/deal.py index 3f8e240..9f78782 100644 --- a/schemas/deal.py +++ b/schemas/deal.py @@ -5,6 +5,7 @@ from pydantic import constr, field_validator from schemas.base import BaseSchema, OkMessageSchema from schemas.client import ClientSchema +from schemas.marketplace import BaseMarketplaceSchema from schemas.product import ProductSchema from schemas.service import ServiceSchema from schemas.shipping_warehouse import ShippingWarehouseSchema @@ -28,17 +29,20 @@ class DealSummary(BaseSchema): status: int total_price: int rank: int + base_marketplace: Optional[BaseMarketplaceSchema] = None class DealServiceSchema(BaseSchema): service: ServiceSchema quantity: int price: int + employees: List[UserSchema] class DealProductServiceSchema(BaseSchema): service: ServiceSchema price: int + employees: List[UserSchema] class DealProductSchema(BaseSchema): @@ -98,6 +102,7 @@ class DealQuickCreateRequest(BaseSchema): comment: str acceptance_date: datetime.datetime shipping_warehouse: constr(strip_whitespace=True) + base_marketplace: BaseMarketplaceSchema class DealSummaryRequest(BaseSchema): diff --git a/schemas/marketplace.py b/schemas/marketplace.py new file mode 100644 index 0000000..64b3ad7 --- /dev/null +++ b/schemas/marketplace.py @@ -0,0 +1,22 @@ +from typing import List + +from schemas.base import BaseSchema + + +# region Entities +class BaseMarketplaceSchema(BaseSchema): + key: str + name: str + icon_url: str + + +# endregion + +# region Requests +# endregion + +# region Responses +class GetAllBaseMarketplacesResponse(BaseSchema): + base_marketplaces: List[BaseMarketplaceSchema] + +# endregion diff --git a/schemas/position.py b/schemas/position.py index 3f5f986..7f8e62f 100644 --- a/schemas/position.py +++ b/schemas/position.py @@ -12,9 +12,17 @@ class CreatePositionRequest(BaseSchema): data: PositionSchema +class DeletePositionRequest(BaseSchema): + position_key: str + + class GetAllPositionsResponse(BaseSchema): positions: List[PositionSchema] class CreatePositionResponse(OkMessageSchema): pass + + +class DeletePositionResponse(OkMessageSchema): + pass diff --git a/schemas/user.py b/schemas/user.py index 345c388..0f3b75b 100644 --- a/schemas/user.py +++ b/schemas/user.py @@ -19,10 +19,10 @@ class BaseUser(BaseSchema): is_admin: bool is_blocked: bool is_deleted: bool + role_key: str class UserSchema(BaseUser): - role_key: str role: RoleSchema position: Optional[PositionSchema] = None diff --git a/services/deal.py b/services/deal.py index 1a9aac6..637146b 100644 --- a/services/deal.py +++ b/services/deal.py @@ -4,7 +4,7 @@ import models.secondary from typing import Union import models.deal from fastapi import HTTPException -from sqlalchemy import select, func, update +from sqlalchemy import select, func, update, delete, insert from sqlalchemy.orm import joinedload, selectinload from models import User, Service, Client @@ -103,7 +103,8 @@ class DealService(BaseService): client_id=client.id, current_status=DealStatus.CREATED, lexorank=rank, - shipping_warehouse_id=shipping_warehouse.id + shipping_warehouse_id=shipping_warehouse.id, + base_marketplace_key=request.base_marketplace.key ) self.session.add(deal) await self.session.flush() @@ -183,6 +184,9 @@ class DealService(BaseService): deal: Deal last_status: DealStatusHistory = max(deal.status_history, key=lambda status: status.changed_at) deadline = last_status.next_status_deadline + base_marketplace = None + if deal.base_marketplace: + base_marketplace = BaseMarketplaceSchema.model_validate(deal.base_marketplace) summaries.append( DealSummary( id=deal.id, @@ -192,7 +196,8 @@ class DealService(BaseService): deadline=deadline, status=last_status.to_status, total_price=total_price, - rank=rank + rank=rank, + base_marketplace=base_marketplace ) ) return DealSummaryResponse(summaries=summaries) @@ -214,8 +219,10 @@ class DealService(BaseService): joinedload(Deal.client) .joinedload(Client.details), selectinload(Deal.services) - .joinedload(models.secondary.DealService.service) - .joinedload(Service.category), + .options( + joinedload(models.secondary.DealService.service).joinedload(Service.category), + selectinload(models.secondary.DealService.employees) + ), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.product) .joinedload(models.Product.client), @@ -224,7 +231,10 @@ class DealService(BaseService): .joinedload(models.Product.barcodes), selectinload(Deal.products) .joinedload(models.secondary.DealProduct.services) - .joinedload(models.secondary.DealProductService.service), + .options( + joinedload(models.secondary.DealProductService.service), + selectinload(models.secondary.DealProductService.employees) + ), selectinload(Deal.status_history) .joinedload(DealStatusHistory.user), selectinload(Deal.status_history) @@ -439,13 +449,43 @@ class DealService(BaseService): raise HTTPException(status_code=404, detail="Сделка не найдена") service_dict = request.service.dict() del service_dict['service'] + del service_dict['employees'] + service_dict['service_id'] = request.service.service.id + await self.session.execute( update(models.secondary.DealService) .where(models.secondary.DealService.deal_id == request.deal_id, models.secondary.DealService.service_id == request.service.service.id) .values(**service_dict) ) + + # Updating deleting previous employees + delete_stmt = ( + delete( + models.deal_service_employees + ) + .where( + models.deal_service_employees.c.deal_id == request.deal_id, + models.deal_service_employees.c.service_id == request.service.service.id, + ) + ) + await self.session.execute(delete_stmt) + await self.session.flush() + insert_data = [] + for employee in request.service.employees: + insert_data.append({ + 'deal_id': request.deal_id, + 'service_id': request.service.service.id, + 'user_id': employee.id + }) + if insert_data: + await self.session.execute( + insert(models.deal_service_employees), + insert_data + ) + await self.session.flush() + await self.session.commit() return DealUpdateServiceQuantityResponse(ok=True, message='Услуга успешно обновлена') except Exception as e: @@ -586,6 +626,35 @@ class DealService(BaseService): # Updating product deal_product.quantity = request.product.quantity + # Updating deleting old employees + delete_stmt = ( + delete( + models.deal_product_service_employees + ) + .where( + models.deal_product_service_employees.c.deal_id == request.deal_id, + models.deal_product_service_employees.c.service_id.in_(request_services.union(database_services)), + models.deal_product_service_employees.c.product_id == request.product.product.id + ) + ) + await self.session.execute(delete_stmt) + await self.session.flush() + + insert_data = [] + for service in request.product.services: + service: DealProductServiceSchema + for employee in service.employees: + insert_data.append({ + 'deal_id': request.deal_id, + 'service_id': service.service.id, + 'product_id': request.product.product.id, + 'user_id': employee.id + }) + if insert_data: + await self.session.execute(insert(models.deal_product_service_employees), + insert_data) + await self.session.flush() + await self.session.commit() return DealUpdateProductResponse(ok=True, message='Товар успешно обновлен') diff --git a/services/marketplace.py b/services/marketplace.py new file mode 100644 index 0000000..ebdca40 --- /dev/null +++ b/services/marketplace.py @@ -0,0 +1,14 @@ +from sqlalchemy import select + +from models import BaseMarketplace +from schemas.marketplace import GetAllBaseMarketplacesResponse +from services.base import BaseService + + +class MarketplaceService(BaseService): + async def get_all_base_marketplaces(self) -> GetAllBaseMarketplacesResponse: + stmt = (select(BaseMarketplace).order_by(BaseMarketplace.key)) + base_marketplaces = (await self.session.scalars(stmt)).all() + return GetAllBaseMarketplacesResponse( + base_marketplaces=base_marketplaces + ) diff --git a/services/position.py b/services/position.py index fb91eca..de7eed8 100644 --- a/services/position.py +++ b/services/position.py @@ -1,8 +1,8 @@ from typing import Union -from sqlalchemy import select, insert +from sqlalchemy import select, insert, delete -from models import Position +from models import Position, user_position from schemas.position import * from services.base import BaseService @@ -19,7 +19,6 @@ class PositionService(BaseService): stmt = select(Position).where(Position.key == key) return await self.session.scalar(stmt) - async def create(self, request: CreatePositionRequest) -> CreatePositionResponse: try: if await self.get_by_key(request.data.key): @@ -30,3 +29,30 @@ class PositionService(BaseService): return CreatePositionResponse(ok=True, message='Должность успешно создана') except Exception as e: return CreatePositionResponse(ok=False, message=str(e)) + + async def delete(self, request: DeletePositionRequest) -> DeletePositionResponse: + try: + # Prevent deleting if existing on users + stmt = ( + select( + user_position + ) + .where( + user_position.c.position_key == request.position_key + ) + ) + if await self.session.scalar(stmt): + return DeletePositionResponse(ok=False, message='Должность привязана к существующим сотрудникам') + delete_stmt = ( + delete( + Position + ) + .where( + Position.key == request.position_key + ) + ) + await self.session.execute(delete_stmt) + await self.session.commit() + return DeletePositionResponse(ok=True, message="Должность успешно удалена!") + except Exception as e: + return DeletePositionResponse(ok=False, message=str(e)) diff --git a/services/user.py b/services/user.py index 0cb4471..1d84de9 100644 --- a/services/user.py +++ b/services/user.py @@ -10,7 +10,7 @@ class UserService(BaseService): stmt = ( select(User) .order_by(User.id.desc()) - # .where(User.is_deleted == False) + .where(User.is_deleted == False) ) users = (await self.session.scalars(stmt)).all() users_schemas = [UserSchema.model_validate(user) for user in users] @@ -25,6 +25,7 @@ class UserService(BaseService): return UpdateUserResponse(ok=False, message='Указанный пользователь не найден') base_fields = request.data.model_dump_parent() stmt = update(User).values(**base_fields).where(User.id == request.data.id) + print(stmt) await self.session.execute(stmt) await self.session.flush() diff --git a/static/icons/ozon.png b/static/icons/ozon.png new file mode 100644 index 0000000..52293bf Binary files /dev/null and b/static/icons/ozon.png differ diff --git a/static/icons/ozon.svg b/static/icons/ozon.svg new file mode 100644 index 0000000..0e2622b --- /dev/null +++ b/static/icons/ozon.svg @@ -0,0 +1,114 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/static/icons/wb.svg b/static/icons/wb.svg new file mode 100644 index 0000000..3fc2817 --- /dev/null +++ b/static/icons/wb.svg @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + diff --git a/static/icons/ym.png b/static/icons/ym.png new file mode 100644 index 0000000..a19c517 Binary files /dev/null and b/static/icons/ym.png differ diff --git a/static/icons/ym.svg b/static/icons/ym.svg new file mode 100644 index 0000000..b90e1af --- /dev/null +++ b/static/icons/ym.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + diff --git a/test.py b/test.py index 0602cc4..b262575 100644 --- a/test.py +++ b/test.py @@ -1,14 +1,3 @@ -from typing import Self - - -class A: - @classmethod - def test(cls) -> Self: - return cls() - - -class B(A): - pass - - -a = B.test() +a = {1, 2, 3} +b = {4, 5, 6} +print(a.union(b)) diff --git a/utils/init_marketplaces.py b/utils/init_marketplaces.py new file mode 100644 index 0000000..0b6769c --- /dev/null +++ b/utils/init_marketplaces.py @@ -0,0 +1,37 @@ +import asyncio + +from sqlalchemy import insert +from sqlalchemy.ext.asyncio import AsyncSession + +import models +from backend.session import session_maker +from enums.base_marketplace import BaseMarketplace + + +async def main(): + session: AsyncSession = session_maker() + marketplaces = [ + { + 'key': BaseMarketplace.WILDBERRIES, + 'name': 'Wildberries', + 'icon_url': '/api/static/icons/wb.svg' + }, + { + 'key': BaseMarketplace.OZON, + 'name': 'OZON', + 'icon_url': '/api/static/icons/ozon.svg' + }, + { + 'key': BaseMarketplace.YANDEX_MARKET, + 'name': 'Яндекс Маркет', + 'icon_url': '/api/static/icons/ym.svg' + } + ] + await session.execute(insert(models.BaseMarketplace), marketplaces) + await session.commit() + await session.close() + + +if __name__ == '__main__': + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) diff --git a/utils/init_roles.py b/utils/init_roles.py index ba38d37..5058213 100644 --- a/utils/init_roles.py +++ b/utils/init_roles.py @@ -6,14 +6,15 @@ from backend.session import session_maker from enums.user import UserRole from models import Role +role_name_dictionary = { + UserRole.admin: "Админ", + UserRole.user: "Базовый пользователь", + UserRole.manager: "Менеджер", + UserRole.employee: "Сотрудник", +} + async def main(): - role_name_dictionary = { - UserRole.admin: "Админ", - UserRole.user: "Базовый пользователь", - UserRole.manager: "Менеджер", - UserRole.employee: "Сотрудник", - } session: AsyncSession = session_maker() for key, name in role_name_dictionary.items(): role = Role(