From 1af78ce08a8bedb9820fe01cc6c7b4ce29a9e2a4 Mon Sep 17 00:00:00 2001 From: AlexSserb Date: Wed, 19 Feb 2025 14:46:31 +0400 Subject: [PATCH] feat: cards, attributes and modules --- card_attributes/__init__.py | 1 + card_attributes/exceptions.py | 2 + card_attributes/handlers/__init__.py | 2 + card_attributes/handlers/base_handler.py | 6 + .../card_attributes_command_handler.py | 88 ++ .../handlers/card_attributes_query_handler.py | 37 + generators/deal_pdf_generator/deal_data.py | 10 +- generators/deal_pdf_generator/generator.py | 38 +- .../price_list_pdf_generator/generator.py | 2 +- generators/services_excel_generator/core.py | 2 +- .../shipping_qr_code_generator/generator.py | 14 +- main.py | 4 +- models/__init__.py | 9 +- models/attribute.py | 100 ++ models/auth.py | 6 +- models/barcode.py | 19 +- models/billing.py | 30 +- models/board.py | 8 +- models/card.py | 118 ++ models/{deal_group.py => card_group.py} | 18 +- models/client.py | 75 +- models/deal.py | 106 -- models/module.py | 33 + models/product.py | 105 +- models/project.py | 31 +- models/secondary.py | 151 +- models/sequences.py | 4 - models/service.py | 53 +- models/shipping.py | 10 +- models/status.py | 46 +- parsers/deal_parser.py | 6 +- routers/__init__.py | 4 +- routers/billing.py | 16 +- routers/{deal.py => card.py} | 324 ++-- routers/group.py | 64 +- routers/shipping.py | 6 +- schemas/attribute.py | 43 + schemas/barcode.py | 6 +- schemas/billing.py | 24 +- schemas/board.py | 2 +- schemas/card.py | 421 ++++++ schemas/deal.py | 420 ------ schemas/group.py | 32 +- schemas/module.py | 11 + schemas/project.py | 9 +- schemas/service.py | 2 +- schemas/shipping.py | 8 +- schemas/statistics.py | 6 +- schemas/status.py | 2 +- services/barcode.py | 38 +- services/billing.py | 203 ++- services/board.py | 16 +- services/card.py | 1317 +++++++++++++++++ services/card_group.py | 168 +++ services/deal.py | 1317 ----------------- services/deal_group.py | 168 --- services/project.py | 38 +- services/service.py | 6 +- services/shipping.py | 22 +- services/statistics/profit_statistics.py | 146 +- services/status.py | 34 +- 61 files changed, 3212 insertions(+), 2795 deletions(-) create mode 100644 card_attributes/__init__.py create mode 100644 card_attributes/exceptions.py create mode 100644 card_attributes/handlers/__init__.py create mode 100644 card_attributes/handlers/base_handler.py create mode 100644 card_attributes/handlers/card_attributes_command_handler.py create mode 100644 card_attributes/handlers/card_attributes_query_handler.py create mode 100644 models/attribute.py create mode 100644 models/card.py rename models/{deal_group.py => card_group.py} (66%) delete mode 100644 models/deal.py create mode 100644 models/module.py delete mode 100644 models/sequences.py rename routers/{deal.py => card.py} (52%) create mode 100644 schemas/attribute.py create mode 100644 schemas/card.py delete mode 100644 schemas/deal.py create mode 100644 schemas/module.py create mode 100644 services/card.py create mode 100644 services/card_group.py delete mode 100644 services/deal.py delete mode 100644 services/deal_group.py diff --git a/card_attributes/__init__.py b/card_attributes/__init__.py new file mode 100644 index 0000000..d7ed779 --- /dev/null +++ b/card_attributes/__init__.py @@ -0,0 +1 @@ +from .handlers import * diff --git a/card_attributes/exceptions.py b/card_attributes/exceptions.py new file mode 100644 index 0000000..b434c8d --- /dev/null +++ b/card_attributes/exceptions.py @@ -0,0 +1,2 @@ +class CardAttributeException(Exception): + pass diff --git a/card_attributes/handlers/__init__.py b/card_attributes/handlers/__init__.py new file mode 100644 index 0000000..c3854f7 --- /dev/null +++ b/card_attributes/handlers/__init__.py @@ -0,0 +1,2 @@ +from .card_attributes_query_handler import CardAttributesQueryHandler +from .card_attributes_command_handler import CardAttributesCommandHandler diff --git a/card_attributes/handlers/base_handler.py b/card_attributes/handlers/base_handler.py new file mode 100644 index 0000000..9b3be48 --- /dev/null +++ b/card_attributes/handlers/base_handler.py @@ -0,0 +1,6 @@ +from sqlalchemy.ext.asyncio import AsyncSession + + +class BaseHandler: + def __init__(self, session: AsyncSession): + self.session = session diff --git a/card_attributes/handlers/card_attributes_command_handler.py b/card_attributes/handlers/card_attributes_command_handler.py new file mode 100644 index 0000000..5a2274f --- /dev/null +++ b/card_attributes/handlers/card_attributes_command_handler.py @@ -0,0 +1,88 @@ +import pickle +from typing import Optional + +from sqlalchemy import select, and_ + +from card_attributes.exceptions import CardAttributeException +from card_attributes.handlers.base_handler import BaseHandler +from models import CardAttribute, Attribute, Card +from .card_attributes_query_handler import CardAttributesQueryHandler + + +class CardAttributesCommandHandler(BaseHandler): + async def _create_card_attribute(self, card_id: int, attribute_id: int, value): + card_attribute = CardAttribute( + card_id=card_id, + attribute_id=attribute_id, + ) + card_attribute.set_value(value) + self.session.add(card_attribute) + await self.session.flush() + + async def _set_attribute_after_creation(self, card_id: int, project_attr: Attribute, attributes: Optional[dict]): + if attributes and project_attr.name in attributes: + passed_value = attributes[project_attr.name] + return await self._create_card_attribute(card_id, project_attr.id, passed_value) + + if project_attr.default_value: + default_value = pickle.loads(project_attr.default_value) + return await self._create_card_attribute(card_id, project_attr.id, default_value) + + if project_attr.is_nullable: + return await self._create_card_attribute(card_id, project_attr.id, None) + + raise CardAttributeException("Required value was not provided") + + async def set_attributes_after_creation(self, card: Card, attributes: Optional[dict] = None): + query_handler = CardAttributesQueryHandler(self.session) + project_attrs = await query_handler.get_attributes_for_project(card.board_id) + + try: + for project_attr in project_attrs: + await self._set_attribute_after_creation(card.id, project_attr, attributes) + except CardAttributeException: + raise + + async def _set_card_attribute(self, card_id: int, attribute_name: str, value): + query_handler = CardAttributesQueryHandler(self.session) + attribute = await query_handler.get_attr_by_name(attribute_name) + if not attribute: + raise CardAttributeException(f"Attribute [{attribute_name}] not found") + + stmt = ( + select(CardAttribute) + .where( + and_( + CardAttribute.card_id == card_id, + CardAttribute.attribute_id == attribute.id, + ) + ) + ) + card_attribute: Optional[CardAttribute] = (await self.session.scalars(stmt)).one_or_none() + + if not card_attribute: + await self._create_card_attribute(card_id, attribute.id, value) + else: + card_attribute.set_value(value) + + async def set_attr_for_each_in_group(self, group_id: int, attribute_name: str, value): + query_handler = CardAttributesQueryHandler(self.session) + card_ids: list[int] = await query_handler.get_card_ids_by_group_id(group_id) + + for card_id in card_ids: + await self._set_card_attribute(card_id, attribute_name, value) + + async def set_attributes(self, card: Card, attributes: Optional[dict] = None): + query_handler = CardAttributesQueryHandler(self.session) + project_attrs: list[Attribute] = await query_handler.get_attributes_for_project(card.board_id) + + try: + for attr_name, attr_value in attributes.items(): + attr = next(attr for attr in project_attrs if attr.name == attr_name) + if attr: + if attr.is_applicable_to_group and card.group: + await self.set_attr_for_each_in_group(card.group.id, attr_name, attr_value) + else: + await self._set_card_attribute(card.id, attr_name, attr_value) + except CardAttributeException: + raise diff --git a/card_attributes/handlers/card_attributes_query_handler.py b/card_attributes/handlers/card_attributes_query_handler.py new file mode 100644 index 0000000..67e29bd --- /dev/null +++ b/card_attributes/handlers/card_attributes_query_handler.py @@ -0,0 +1,37 @@ +from typing import Optional + +from sqlalchemy import select +from sqlalchemy.orm import joinedload, selectinload + +from card_attributes.handlers.base_handler import BaseHandler +from models import Attribute, project_attribute, card_relations + + +class CardAttributesQueryHandler(BaseHandler): + async def get_attributes_for_project(self, project_id: int) -> list[Attribute]: + stmt = ( + select(Attribute) + .join(project_attribute, project_attribute.c.attribute_id == Attribute.id) + .where(project_attribute.c.project_id == project_id) + ) + attributes = (await self.session.scalars(stmt)).all() + return list(attributes) + + async def get_attr_by_name(self, attr_name: str) -> Optional[Attribute]: + stmt = ( + select(Attribute) + .options( + selectinload(Attribute.projects), + ) + .where(Attribute.name == attr_name) + ) + attribute = (await self.session.scalars(stmt)).first() + return attribute + + async def get_card_ids_by_group_id(self, group_id: int) -> list[int]: + stmt = ( + select(card_relations.c.card_id) + .where(card_relations.c.group_id == group_id) + ) + ids = await self.session.scalars(stmt) + return list(ids) diff --git a/generators/deal_pdf_generator/deal_data.py b/generators/deal_pdf_generator/deal_data.py index 8d2f4d9..50bf66c 100644 --- a/generators/deal_pdf_generator/deal_data.py +++ b/generators/deal_pdf_generator/deal_data.py @@ -1,24 +1,24 @@ from typing import TypedDict, List, Dict, Tuple, Optional -from models import DealProduct, Deal, DealStatusHistory +from models import CardProduct, Card, CardStatusHistory class DealTechSpecProductData(TypedDict): - deal: Deal - last_status: DealStatusHistory + deal: Card + last_status: CardStatusHistory total_one_product: int quantity: int additional_info: Optional[str] # Поле для группировки товаров с одним артикулом и вывода таблицы [Штрихкод, Размер, Кол-во, Короба] - deal_products: List[DealProduct] + deal_products: List[CardProduct] # Поле для группировки товаров из нескольких сделок и вывода таблицы [Склад отгрузки, Кол-во] warehouses_and_quantities: List[Tuple[str, int]] class DealTechSpecData(TypedDict): - deals: List[Deal] + deals: List[Card] products: Dict[str, DealTechSpecProductData] product_images: Tuple deal_ids_header: str diff --git a/generators/deal_pdf_generator/generator.py b/generators/deal_pdf_generator/generator.py index bab3e54..ec41b5f 100644 --- a/generators/deal_pdf_generator/generator.py +++ b/generators/deal_pdf_generator/generator.py @@ -8,13 +8,13 @@ from weasyprint import HTML, CSS from constants import DEAL_STATUS_STR, ENV, APP_PATH from generators.deal_pdf_generator.deal_data import DealTechSpecProductData, DealTechSpecData -from models import Deal, DealProduct, DealService as DealServiceModel, Product, DealGroup -from services.deal_group import DealGroupService +from models import Card, CardProduct, CardService as DealServiceModel, Product, CardGroup +from services.card_group import CardGroupService from utils.images_fetcher import fetch_images # Генерация ключа для группировки deal_product по артикулу и услугам -def _gen_key_for_product(deal_product: DealProduct) -> str: +def _gen_key_for_product(deal_product: CardProduct) -> str: return f"{deal_product.product.article} - " + ",".join( str(service.service_id) for service in deal_product.services ) @@ -48,10 +48,10 @@ class DealTechSpecPdfGenerator: "deal_ids_header": "", "deal_status_str": DEAL_STATUS_STR, } - self.deal: Deal + self.deal: Card @staticmethod - async def _group_deal_products_by_products(deal_products: List[DealProduct]) -> Dict[str, DealTechSpecProductData]: + async def _group_deal_products_by_products(deal_products: List[CardProduct]) -> Dict[str, DealTechSpecProductData]: products: Dict[str, DealTechSpecProductData] = {} additional_info: Optional[str] @@ -61,7 +61,7 @@ class DealTechSpecPdfGenerator: if key not in products: products[key] = { - "deal": deal_product.deal, + "deal": deal_product.card, "deal_products": [deal_product], "quantity": deal_product.quantity, "additional_info": deal_product.product.additional_info, @@ -75,18 +75,18 @@ class DealTechSpecPdfGenerator: return products - async def _get_deal_by_id(self, deal_id: int) -> Optional[Deal]: - deal: Deal | None = await self._session.scalar( - select(Deal) - .where(Deal.id == deal_id) + async def _get_deal_by_id(self, deal_id: int) -> Optional[Card]: + deal: Card | None = await self._session.scalar( + select(Card) + .where(Card.id == deal_id) .options( - selectinload(Deal.products).selectinload(DealProduct.services), - selectinload(Deal.products).selectinload(DealProduct.product).selectinload(Product.barcodes), - selectinload(Deal.services).selectinload(DealServiceModel.service), - selectinload(Deal.status_history), - selectinload(Deal.group).selectinload(DealGroup.deals), - joinedload(Deal.client), - joinedload(Deal.shipping_warehouse), + selectinload(Card.products).selectinload(CardProduct.services), + selectinload(Card.products).selectinload(CardProduct.product).selectinload(Product.barcodes), + selectinload(Card.services).selectinload(DealServiceModel.service), + selectinload(Card.status_history), + selectinload(Card.group).selectinload(CardGroup.cards), + joinedload(Card.client), + joinedload(Card.shipping_warehouse), ) ) return deal @@ -94,7 +94,7 @@ class DealTechSpecPdfGenerator: def _set_deals_ids_header(self): self.deal_doc["deal_ids_header"] = f"ID: {self.deal.id}" if self.deal.group: - self.deal_doc["deal_ids_header"] = "ID: " + ", ".join(str(d.id) for d in self.deal.group.deals) + self.deal_doc["deal_ids_header"] = "ID: " + ", ".join(str(d.id) for d in self.deal.group.cards) async def _create_deal_tech_spec_document_html(self, deal_id: int): deal = await self._get_deal_by_id(deal_id) @@ -105,7 +105,7 @@ class DealTechSpecPdfGenerator: self._set_deals_ids_header() if deal.group: - deals = await DealGroupService(self._session).get_deals_by_group_id(deal.group.id) + deals = await CardGroupService(self._session).get_cards_by_group_id(deal.group.id) for d in deals: self.deal_doc["deals"].append(d) grouped_products = await self._group_deal_products_by_products(d.products) diff --git a/generators/price_list_pdf_generator/generator.py b/generators/price_list_pdf_generator/generator.py index 70c2f9c..268952d 100644 --- a/generators/price_list_pdf_generator/generator.py +++ b/generators/price_list_pdf_generator/generator.py @@ -42,7 +42,7 @@ class PriceListPdfGenerator: # Определяем функцию сортировки категорий по рангу def category_sort_key(category): if service_type == ServiceType.DEAL_SERVICE: - return category.deal_service_rank + return category.card_service_rank else: return category.product_service_rank diff --git a/generators/services_excel_generator/core.py b/generators/services_excel_generator/core.py index 5f79ea9..f2269eb 100644 --- a/generators/services_excel_generator/core.py +++ b/generators/services_excel_generator/core.py @@ -63,7 +63,7 @@ class ServiceExcelExporter: categories = {service.category for services in categories_dict.values() for service in services} def category_sort_key(category): - return category.deal_service_rank if service_type == ServiceType.DEAL_SERVICE else category.product_service_rank + return category.card_service_rank if service_type == ServiceType.DEAL_SERVICE else category.product_service_rank sorted_categories = sorted(categories, key=category_sort_key) sorted_categories_dict = {category.id: categories_dict[category.id] for category in sorted_categories} diff --git a/generators/shipping_qr_code_generator/generator.py b/generators/shipping_qr_code_generator/generator.py index def727e..13fe12d 100644 --- a/generators/shipping_qr_code_generator/generator.py +++ b/generators/shipping_qr_code_generator/generator.py @@ -11,18 +11,18 @@ from sqlalchemy.orm import joinedload, selectinload from constants import DOMAIN_NAME from generators.base_pdf_card_generator.base_pdf_card_generator import BasePdfCardGenerator -from models import Deal, ShippingWarehouse, Pallet +from models import Card, ShippingWarehouse, Pallet from models.shipping import Box class ShippingQRCodeGenerator(BasePdfCardGenerator): - async def _get_deal_by_id(self, deal_id: int) -> Optional[Deal]: + async def _get_deal_by_id(self, deal_id: int) -> Optional[Card]: stmt = ( - select(Deal) - .where(Deal.id == deal_id) + select(Card) + .where(Card.id == deal_id) .options( - joinedload(Deal.shipping_warehouse), - selectinload(Deal.pallets), + joinedload(Card.shipping_warehouse), + selectinload(Card.pallets), ) ) deal = (await self._session.execute(stmt)).one_or_none() @@ -93,7 +93,7 @@ class ShippingQRCodeGenerator(BasePdfCardGenerator): func.count(Box.id).label("box_count"), ) .join(Box, isouter=True) - .where(Pallet.deal_id == deal_id) + .where(Pallet.card_id == deal_id) .group_by(Pallet.id) ) pallets = (await self._session.execute(stmt_boxes_on_pallets)).all() diff --git a/main.py b/main.py index 162551f..cacbd82 100644 --- a/main.py +++ b/main.py @@ -31,8 +31,8 @@ app.add_middleware( routers_list = [ routers.auth_router, - routers.deal_router, - routers.deal_group_router, + routers.card_router, + routers.card_group_router, routers.client_router, routers.service_router, routers.product_router, diff --git a/models/__init__.py b/models/__init__.py index 4003892..abb95c7 100644 --- a/models/__init__.py +++ b/models/__init__.py @@ -1,10 +1,13 @@ from sqlalchemy.orm import configure_mappers -from .auth import * from .project import * +from .module import * from .board import * from .status import * -from .deal import * +from .attribute import * +from .card import * +from .auth import * +from .card import * from .client import * from .service import * from .product import * @@ -15,7 +18,7 @@ from .marketplace import * from .payroll import * from .billing import * from .marketplace_products import * -from .deal_group import * +from .card_group import * from .transaction import * from .residues import * from .shipping import * diff --git a/models/attribute.py b/models/attribute.py new file mode 100644 index 0000000..0adcaf4 --- /dev/null +++ b/models/attribute.py @@ -0,0 +1,100 @@ +import pickle +from typing import TYPE_CHECKING + +from sqlalchemy import ForeignKey, Table, Column, UniqueConstraint, Index +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from models.base import BaseModel + +if TYPE_CHECKING: + from models import Project, BaseModel, Card + +project_attribute = Table( + 'project_attribute', + BaseModel.metadata, + Column('project_id', ForeignKey('projects.id')), + Column('attribute_id', ForeignKey('attributes.id')), +) + + +class AttributeType(BaseModel): + __tablename__ = 'attribute_types' + + id: Mapped[int] = mapped_column(primary_key=True) + type: Mapped[str] = mapped_column(nullable=False, unique=True) + name: Mapped[str] = mapped_column(nullable=False, unique=True) + is_deleted: Mapped[bool] = mapped_column(default=False) + + attributes: Mapped['Attribute'] = relationship( + 'Attribute', + back_populates='type', + lazy='noload', + ) + + +class Attribute(BaseModel): + __tablename__ = 'attributes' + + id: Mapped[int] = mapped_column(primary_key=True) + label: Mapped[str] = mapped_column(nullable=False) + name: Mapped[str] = mapped_column(nullable=False, unique=True) + is_applicable_to_group: Mapped[bool] = mapped_column( + default=False, + comment='Применять ли изменения атрибута карточки ко всем карточкам в группе', + ) + is_nullable: Mapped[bool] = mapped_column(default=False, nullable=False) + default_value: Mapped[bytes] = mapped_column(nullable=True) + + projects: Mapped[list['Project']] = relationship( + 'Project', + uselist=True, + secondary='project_attribute', + back_populates='attributes', + lazy='noload', + ) + + type_id: Mapped[int] = mapped_column(ForeignKey('attribute_types.id'), nullable=False) + type: Mapped[AttributeType] = relationship( + 'AttributeType', + back_populates='attributes', + lazy='joined', + ) + + card_attributes: Mapped[list['CardAttribute']] = relationship( + 'CardAttribute', + uselist=True, + lazy='noload', + back_populates='attribute', + ) + + +class CardAttribute(BaseModel): + __tablename__ = 'card_attributes' + + id: Mapped[int] = mapped_column(primary_key=True) + value: Mapped[bytes] = mapped_column(nullable=True) + + card_id: Mapped[int] = mapped_column(ForeignKey('cards.id'), nullable=False) + card: Mapped['Card'] = relationship( + 'Card', + back_populates='attributes', + lazy='noload', + ) + + attribute_id: Mapped[int] = mapped_column(ForeignKey('attributes.id'), nullable=False) + attribute: Mapped[Attribute] = relationship( + 'Attribute', + back_populates='card_attributes', + lazy='joined', + ) + + __table_args__ = ( + UniqueConstraint('card_id', 'attribute_id', name='uix_card_service'), + Index('idx_card_id_attribute_id', 'card_id', 'attribute_id', unique=True) + ) + + def set_value(self, value): + self.value = pickle.dumps(value) + + def get_value(self): + return pickle.loads(self.value) diff --git a/models/auth.py b/models/auth.py index 7f2b735..1b21f72 100644 --- a/models/auth.py +++ b/models/auth.py @@ -9,7 +9,7 @@ from models.work_shifts import WorkShift if TYPE_CHECKING: from models.payroll import PayRate, PaymentRecord - from models import Deal, DealEmployees + from models import Card, CardEmployees role_permissions = Table( 'role_permissions', @@ -115,7 +115,7 @@ class User(BaseModel): foreign_keys="WorkShift.user_id" ) - managed_deals: Mapped[list["Deal"]] = relationship( + managed_cards: Mapped[list["Card"]] = relationship( back_populates="manager", uselist=True, ) @@ -127,7 +127,7 @@ class User(BaseModel): cascade="all, delete-orphan" ) - deals: Mapped[list['DealEmployees']] = relationship( + cards: Mapped[list['CardEmployees']] = relationship( back_populates='user', lazy='selectin' ) diff --git a/models/barcode.py b/models/barcode.py index c1e0207..7951b6f 100644 --- a/models/barcode.py +++ b/models/barcode.py @@ -35,14 +35,17 @@ class BarcodeTemplate(BaseModel): __tablename__ = 'barcode_templates' id = Column(Integer, autoincrement=True, primary_key=True, index=True) name = Column(String, nullable=False, index=True, comment='Название шаблона') - attributes = relationship('BarcodeTemplateAttribute', - secondary=barcode_template_attribute_link, - lazy='selectin' - ) - additional_attributes = relationship('BarcodeTemplateAdditionalField', - lazy='selectin', - back_populates='barcode_template', - cascade="all, delete") + attributes = relationship( + 'BarcodeTemplateAttribute', + secondary=barcode_template_attribute_link, + lazy='selectin', + ) + additional_attributes = relationship( + 'BarcodeTemplateAdditionalField', + lazy='selectin', + back_populates='barcode_template', + cascade="all, delete", + ) additional_field = Column(String, nullable=True, comment='Дополнительное поле') is_default = Column(Boolean, nullable=False, default=False, comment='По умолчанию') diff --git a/models/billing.py b/models/billing.py index dfb2736..5a2159e 100644 --- a/models/billing.py +++ b/models/billing.py @@ -7,17 +7,19 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from models import BaseModel if TYPE_CHECKING: - from models import Deal, DealGroup + from models import Card, CardGroup -class DealBillRequest(BaseModel): - __tablename__ = 'deal_bill_requests' +class CardBillRequest(BaseModel): + __tablename__ = 'card_bill_requests' - deal_id: Mapped[int] = mapped_column(ForeignKey('deals.id'), - nullable=False, - primary_key=True, - unique=True) - deal: Mapped['Deal'] = relationship(back_populates='bill_request') + card_id: Mapped[int] = mapped_column( + ForeignKey('cards.id'), + nullable=False, + primary_key=True, + unique=True, + ) + card: Mapped['Card'] = relationship(back_populates='bill_request') created_at: Mapped[datetime.datetime] = mapped_column(nullable=False) paid: Mapped[bool] = mapped_column(nullable=False, default=False) @@ -29,11 +31,13 @@ class DealBillRequest(BaseModel): class GroupBillRequest(BaseModel): __tablename__ = 'group_bill_requests' - group_id: Mapped[int] = mapped_column(ForeignKey('deal_groups.id'), - nullable=False, - primary_key=True, - unique=True) - group: Mapped['DealGroup'] = relationship(back_populates='bill_request') + group_id: Mapped[int] = mapped_column( + ForeignKey('card_groups.id'), + nullable=False, + primary_key=True, + unique=True, + ) + group: Mapped['CardGroup'] = relationship(back_populates='bill_request') created_at: Mapped[datetime.datetime] = mapped_column(nullable=False) paid: Mapped[bool] = mapped_column(nullable=False, default=False) diff --git a/models/board.py b/models/board.py index f6e3ec9..bbf54db 100644 --- a/models/board.py +++ b/models/board.py @@ -4,10 +4,10 @@ from typing import TYPE_CHECKING from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship -from models import BaseModel +from models.base import BaseModel if TYPE_CHECKING: - from models import Project, DealStatus, Deal + from models import Project, CardStatus, Card class Board(BaseModel): @@ -26,6 +26,6 @@ class Board(BaseModel): lazy="selectin", ) - deal_statuses: Mapped[list["DealStatus"]] = relationship("DealStatus", back_populates="board", lazy="selectin", cascade="all,delete") + statuses: Mapped[list["CardStatus"]] = relationship("CardStatus", back_populates="board", lazy="selectin", cascade="all,delete") - deals: Mapped[list["Deal"]] = relationship("Deal", uselist=True, back_populates="board", lazy="selectin") + cards: Mapped[list["Card"]] = relationship("Card", uselist=True, back_populates="board", lazy="selectin") diff --git a/models/card.py b/models/card.py new file mode 100644 index 0000000..ce8999b --- /dev/null +++ b/models/card.py @@ -0,0 +1,118 @@ +from datetime import datetime +from typing import Optional, TYPE_CHECKING + +from sqlalchemy import ForeignKey +from sqlalchemy.orm import relationship, backref, Mapped, mapped_column + +from models.base import BaseModel +from .marketplace import BaseMarketplace +from .shipping import Pallet, Box +from .shipping_warehouse import ShippingWarehouse + +if TYPE_CHECKING: + from . import CardBillRequest, User, BaseModel, Board, CardStatus, CardGroup, CardAttribute, CardService as CardServiceModel, \ + CardProduct, Client + + +class Card(BaseModel): + __tablename__ = 'cards' + + # region Base card attributes + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False, comment='Название карточки') + comment: Mapped[str] = mapped_column(nullable=False, server_default='', comment='Комментарий') + created_at: Mapped[datetime] = mapped_column(nullable=False, comment='Дата создания') + + is_deleted: Mapped[bool] = mapped_column(nullable=False, server_default='0', default=False, comment='Удалена') + is_completed: Mapped[bool] = mapped_column(nullable=False, server_default='0', default=False, comment='Завершена') + + lexorank: Mapped[str] = mapped_column(nullable=False, comment='Lexorank', index=True) + + board_id: Mapped[int] = mapped_column(ForeignKey('boards.id'), nullable=True, server_default='1') + board: Mapped['Board'] = relationship( + 'Board', + back_populates='cards', + ) + + current_status_id: Mapped[int] = mapped_column( + ForeignKey('card_statuses.id'), + nullable=False, + comment='Текущий статус', + ) + status: Mapped['CardStatus'] = relationship(lazy='selectin') + + status_history = relationship('CardStatusHistory', back_populates='card', cascade="all, delete-orphan") + + attributes: Mapped[list['CardAttribute']] = relationship( + 'CardAttribute', + uselist=True, + back_populates='card', + lazy='selectin', + ) + + group: Mapped[Optional["CardGroup"]] = relationship( + 'CardGroup', + secondary='card_relations', + lazy='joined', + back_populates='cards' + ) + # endregion + + # region Attributes handled by modules + + # module servicesAndProducts + is_locked: Mapped[bool] = mapped_column(default=False, server_default='0') + + shipping_warehouse_id: Mapped[int] = mapped_column(ForeignKey('shipping_warehouses.id'), nullable=True) + shipping_warehouse: Mapped["ShippingWarehouse"] = relationship() + + base_marketplace_key: Mapped[str] = mapped_column(ForeignKey("base_marketplaces.key"), nullable=True) + base_marketplace: Mapped["BaseMarketplace"] = relationship(lazy="joined") + + services: Mapped[list['CardServiceModel']] = relationship( + 'CardService', + back_populates='card', + cascade="all, delete-orphan", + order_by="desc(CardService.service_id)" + ) + + products: Mapped[list['CardProduct']] = relationship( + 'CardProduct', + back_populates='card', + cascade="all, delete-orphan", + order_by="desc(CardProduct.product_id)" + ) + + bill_request: Mapped[Optional['CardBillRequest']] = relationship(back_populates='card', lazy='joined') + + # module client + client_id: Mapped[int] = mapped_column( + ForeignKey('clients.id', ondelete='CASCADE'), + nullable=False, + comment='ID клиента', + ) + client: Mapped['Client'] = relationship('Client', backref=backref('cards', cascade="all, delete-orphan")) + + # module managers + manager_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=True) + manager: Mapped[Optional["User"]] = relationship(back_populates='managed_cards', lazy='joined') + + # module shipment + pallets: Mapped[list[Pallet]] = relationship(back_populates='card', lazy='selectin') + boxes: Mapped[list[Box]] = relationship(back_populates='card', lazy='selectin') + + # module employees + employees: Mapped[list['CardEmployees']] = relationship(back_populates='card', lazy='selectin') + + # endregion + +class CardEmployees(BaseModel): + __tablename__ = 'card_employees' + user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), primary_key=True) + user: Mapped['User'] = relationship('User', back_populates='cards', lazy='selectin') + + card_id: Mapped[int] = mapped_column(ForeignKey('cards.id'), primary_key=True) + card: Mapped[Card] = relationship('Card', back_populates='employees', lazy='selectin') + + created_at: Mapped[datetime] = mapped_column() + diff --git a/models/deal_group.py b/models/card_group.py similarity index 66% rename from models/deal_group.py rename to models/card_group.py index e3a6f7d..92bdb5b 100644 --- a/models/deal_group.py +++ b/models/card_group.py @@ -7,11 +7,11 @@ from models import BaseModel from models import GroupBillRequest if TYPE_CHECKING: - from models import Deal + from models import Card -class DealGroup(BaseModel): - __tablename__ = 'deal_groups' +class CardGroup(BaseModel): + __tablename__ = 'card_groups' id: Mapped[int] = mapped_column( primary_key=True ) @@ -21,9 +21,9 @@ class DealGroup(BaseModel): lexorank: Mapped[str] = mapped_column( nullable=False ) - deals: Mapped[list["Deal"]] = relationship( + cards: Mapped[list["Card"]] = relationship( back_populates='group', - secondary='deal_relations' + secondary='card_relations' ) bill_request: Mapped[Optional['GroupBillRequest']] = relationship( back_populates='group', @@ -31,9 +31,9 @@ class DealGroup(BaseModel): ) -deal_relations = Table( - 'deal_relations', +card_relations = Table( + 'card_relations', BaseModel.metadata, - Column('deal_id', ForeignKey('deals.id'), primary_key=True, unique=True), - Column('group_id', ForeignKey('deal_groups.id'), primary_key=True) + Column('card_id', ForeignKey('cards.id'), primary_key=True, unique=True), + Column('group_id', ForeignKey('card_groups.id'), primary_key=True) ) diff --git a/models/client.py b/models/client.py index 0f9ef19..66aff4b 100644 --- a/models/client.py +++ b/models/client.py @@ -1,70 +1,59 @@ +from datetime import datetime from typing import Optional, TYPE_CHECKING -from sqlalchemy import Column, Integer, String, DateTime, ForeignKey +from sqlalchemy import ForeignKey from sqlalchemy.orm import relationship, Mapped, mapped_column from models import BaseModel if TYPE_CHECKING: - from models import ResidualPallet, ResidualBox + from models import ResidualPallet, ResidualBox, Product, BarcodeTemplate, User class Client(BaseModel): __tablename__ = 'clients' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) - name = Column(String, nullable=False, unique=True, comment='Название клиента') + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False, unique=True, comment='Название клиента') - # TODO replace with additional model - company_name = Column(String, - nullable=False, - server_default='', - comment='Название компании') + company_name: Mapped[str] = mapped_column( + nullable=False, + server_default='', + comment='Название компании', + ) - created_at = Column(DateTime, nullable=False, comment='Дата создания') + created_at: Mapped[datetime] = mapped_column(nullable=False, comment='Дата создания') - products = relationship('Product', back_populates='client') - details = relationship('ClientDetails', uselist=False, back_populates='client', cascade='all, delete', - lazy='joined') + products: Mapped[list['Product']] = relationship('Product', back_populates='client') + details: Mapped['ClientDetails'] = relationship( + uselist=False, + back_populates='client', + cascade='all, delete', + lazy='joined', + ) - barcode_template_id = Column(Integer, ForeignKey('barcode_templates.id'), nullable=True) - barcode_template = relationship('BarcodeTemplate', lazy='selectin') + barcode_template_id: Mapped[int] = mapped_column(ForeignKey('barcode_templates.id'), nullable=True) + barcode_template: Mapped['BarcodeTemplate'] = relationship('BarcodeTemplate', lazy='selectin') comment: Mapped[Optional[str]] = mapped_column(nullable=True, server_default=None, comment='Комментарий') - pallets: Mapped[list["ResidualPallet"]] = relationship(back_populates='client', lazy='selectin') - boxes: Mapped[list["ResidualBox"]] = relationship(back_populates='client', lazy='selectin') + pallets: Mapped[list['ResidualPallet']] = relationship(back_populates='client', lazy='selectin') + boxes: Mapped[list['ResidualBox']] = relationship(back_populates='client', lazy='selectin') class ClientDetails(BaseModel): __tablename__ = 'client_details' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) + id: Mapped[int] = mapped_column(primary_key=True) - client_id = Column(Integer, ForeignKey('clients.id'), unique=True, nullable=False, comment='ID клиента') - client = relationship('Client', back_populates='details', cascade='all, delete', uselist=False) + client_id: Mapped[int] = mapped_column(ForeignKey('clients.id'), unique=True, nullable=False, comment='ID клиента') + client: Mapped[Client] = relationship('Client', back_populates='details', cascade='all, delete', uselist=False) - telegram = Column(String) - phone_number = Column(String) - inn = Column(String) - email = Column(String) + telegram: Mapped[Optional[str]] = mapped_column() + phone_number: Mapped[Optional[str]] = mapped_column() + inn: Mapped[Optional[str]] = mapped_column() + email: Mapped[Optional[str]] = mapped_column() - last_modified_at = Column(DateTime, nullable=False) + last_modified_at: Mapped[datetime] = mapped_column(nullable=False) - modified_by_user_id = Column(Integer, ForeignKey('users.id'), nullable=False) - modified_by_user = relationship('User') - -# class ClientContact(BaseModel): -# __tablename__ = 'client_contact' -# id: Mapped[int] = mapped_column(primary_key=True) -# -# client_id: Mapped[int] = mapped_column(ForeignKey('clients.id')) -# client: Mapped["Client"] = relationship('Client', back_populates='users') -# -# first_name: Mapped[str] = mapped_column() -# last_name: Mapped[str] = mapped_column() -# -# telegram: Mapped[str] = mapped_column() -# phone_number: Mapped[str] = mapped_column() -# email: Mapped[str] = mapped_column() -# inn: Mapped[str] = mapped_column() -# + modified_by_user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) + modified_by_user: Mapped['User'] = relationship('User') diff --git a/models/deal.py b/models/deal.py deleted file mode 100644 index 2a2dcaa..0000000 --- a/models/deal.py +++ /dev/null @@ -1,106 +0,0 @@ -from datetime import datetime -from enum import IntEnum, unique -from typing import Optional, TYPE_CHECKING - -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 .board import Board -from .status import DealStatus -from .shipping import Pallet, Box -from .shipping_warehouse import ShippingWarehouse - -if TYPE_CHECKING: - from . import ( - DealBillRequest, - DealGroup, - User, - ) - - -class Deal(BaseModel): - __tablename__ = 'deals' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) - name = Column(String, nullable=False, comment='Название сделки') - created_at = Column(DateTime, nullable=False, comment='Дата создания') - - client_id = Column(Integer, ForeignKey('clients.id', ondelete='CASCADE'), nullable=False, comment='ID клиента') - client = relationship('Client', backref=backref('deals', cascade="all, delete-orphan")) - - status_history = relationship('DealStatusHistory', back_populates='deal', cascade="all, delete-orphan") - - is_deleted = Column(Boolean, nullable=False, server_default='0', default=False, comment='Удалена') - is_completed = Column(Boolean, nullable=False, server_default='0', default=False, comment='Завершена') - is_locked: Mapped[bool] = mapped_column(default=False, server_default='0') - is_accounted: Mapped[bool] = mapped_column(default=True, server_default='1') - - current_status_id: Mapped[int] = mapped_column( - ForeignKey("deal_statuses.id"), - nullable=False, - comment='Текущий статус', - ) - status: Mapped["DealStatus"] = relationship(lazy="selectin") - - - shipping_warehouse_id: Mapped[int] = mapped_column(ForeignKey('shipping_warehouses.id'), nullable=True) - shipping_warehouse: Mapped["ShippingWarehouse"] = relationship() - - base_marketplace_key: Mapped[str] = mapped_column(ForeignKey("base_marketplaces.key"), nullable=True) - base_marketplace: Mapped["BaseMarketplace"] = relationship(lazy="joined") - - delivery_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) - receiving_slot_date: Mapped[Optional[datetime]] = mapped_column(nullable=True) - - 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)" - ) - - board_id: Mapped[int] = mapped_column(ForeignKey('boards.id'), nullable=True, server_default='1') - board: Mapped[Board] = relationship( - "Board", - back_populates="deals", - ) - - # TODO remake with sequence - lexorank = Column(String, nullable=False, comment='Lexorank', index=True) - - comment = Column(String, nullable=False, server_default='', comment='Коментарий к заданию') - bill_request: Mapped[Optional['DealBillRequest']] = relationship(back_populates='deal', lazy='joined') - - group: Mapped[Optional["DealGroup"]] = relationship( - 'DealGroup', - secondary='deal_relations', - lazy='joined', - back_populates='deals' - ) - - manager_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=True) - manager: Mapped[Optional["User"]] = relationship(back_populates='managed_deals', lazy='joined') - - pallets: Mapped[list[Pallet]] = relationship(back_populates='deal', lazy='selectin') - boxes: Mapped[list[Box]] = relationship(back_populates='deal', lazy='selectin') - - employees: Mapped[list['DealEmployees']] = relationship(back_populates='deal', lazy='selectin') - - -class DealEmployees(BaseModel): - __tablename__ = 'deal_employees' - user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), primary_key=True) - user: Mapped['User'] = relationship('User', back_populates='deals', lazy='selectin') - - deal_id: Mapped[int] = mapped_column(ForeignKey('deals.id'), primary_key=True) - deal: Mapped[Deal] = relationship('Deal', back_populates='employees', lazy='selectin') - - created_at: Mapped[datetime] = mapped_column() diff --git a/models/module.py b/models/module.py new file mode 100644 index 0000000..9323099 --- /dev/null +++ b/models/module.py @@ -0,0 +1,33 @@ +from typing import TYPE_CHECKING + +from sqlalchemy import Table, Column, ForeignKey +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from models.base import BaseModel + +if TYPE_CHECKING: + from models import Project + + +project_module = Table( + 'project_module', + BaseModel.metadata, + Column('project_id', ForeignKey('projects.id')), + Column('module_id', ForeignKey('modules.id')), +) + + +class Module(BaseModel): + __tablename__ = 'modules' + + id: Mapped[int] = mapped_column(primary_key=True) + key: Mapped[str] = mapped_column(unique=True, nullable=False) + is_deleted: Mapped[bool] = mapped_column(default=False) + + projects: Mapped[list['Project']] = relationship( + 'Project', + uselist=True, + secondary='project_module', + back_populates='modules', + lazy='noload', + ) diff --git a/models/product.py b/models/product.py index 27395a7..9a30609 100644 --- a/models/product.py +++ b/models/product.py @@ -1,74 +1,93 @@ -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional -from sqlalchemy import Column, Integer, String, ForeignKey, Sequence -from sqlalchemy.orm import relationship, Mapped -from sqlalchemy.testing.schema import mapped_column +from sqlalchemy import ForeignKey +from sqlalchemy.orm import relationship, Mapped, mapped_column -from models import BaseModel +from models.base import BaseModel if TYPE_CHECKING: - from models import Marketplace + from models import Client, BarcodeTemplate, WildberriesProduct, OzonProduct class Product(BaseModel): __tablename__ = 'products' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) - name = Column(String, nullable=False, index=True) - article = Column(String, nullable=False, default='', server_default='', index=True) - factory_article = Column(String, nullable=False, default='', server_default='', index=True) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False, index=True) + article: Mapped[str] = mapped_column(nullable=False, default='', server_default='', index=True) + factory_article: Mapped[str] = mapped_column(nullable=False, default='', server_default='', index=True) - client_id = Column(Integer, ForeignKey('clients.id'), nullable=False, comment='ID сделки') - client = relationship('Client', back_populates='products') - barcodes = relationship('ProductBarcode', back_populates='product', cascade="all, delete-orphan") + client_id: Mapped[int] = mapped_column(ForeignKey('clients.id'), nullable=False) + client: Mapped['Client'] = relationship('Client', back_populates='products') + barcodes: Mapped[list['ProductBarcode']] = relationship( + 'ProductBarcode', + back_populates='product', + cascade='all, delete-orphan', + ) - barcode_template_id = Column(Integer, ForeignKey('barcode_templates.id'), nullable=True) - barcode_template = relationship('BarcodeTemplate', lazy='joined') + barcode_template_id: Mapped[Optional[int]] = mapped_column(ForeignKey('barcode_templates.id'), nullable=True) + barcode_template: Mapped['BarcodeTemplate'] = relationship('BarcodeTemplate', lazy='joined') - barcode_image = relationship('ProductBarcodeImage', back_populates='product', lazy='joined', uselist=False) + barcode_image: Mapped['ProductBarcodeImage'] = relationship( + 'ProductBarcodeImage', + back_populates='product', + lazy='joined', + uselist=False, + ) # Attributes # TODO move to another table - brand = Column(String, nullable=True, comment='Бренд') - color = Column(String, nullable=True, comment='Цвет') - composition = Column(String, nullable=True, comment='Состав') - size = Column(String, nullable=True, comment='Размер') - additional_info = Column(String, nullable=True, comment='Дополнительное поле') - images = relationship('ProductImage', - back_populates='product', - lazy='selectin', - cascade="all, delete-orphan") + brand: Mapped[Optional[str]] = mapped_column(nullable=True, comment='Бренд') + color: Mapped[Optional[str]] = mapped_column(nullable=True, comment='Цвет') + composition: Mapped[Optional[str]] = mapped_column(nullable=True, comment='Состав') + size: Mapped[Optional[str]] = mapped_column(nullable=True, comment='Размер') + additional_info: Mapped[Optional[str]] = mapped_column(nullable=True, comment='Дополнительное поле') + images: Mapped[list['ProductImage']] = relationship( + 'ProductImage', + back_populates='product', + lazy='selectin', + cascade='all, delete-orphan', + ) - wildberries_products = relationship('WildberriesProduct', - back_populates='product', - cascade="all, delete-orphan", - uselist=True) + wildberries_products: Mapped[list['WildberriesProduct']] = relationship( + 'WildberriesProduct', + back_populates='product', + cascade='all, delete-orphan', + uselist=True, + ) - ozon_products = relationship('OzonProduct', - back_populates='product', - cascade="all, delete-orphan", - uselist=True) + ozon_products: Mapped[list['OzonProduct']] = relationship( + 'OzonProduct', + back_populates='product', + cascade='all, delete-orphan', + uselist=True, + ) class ProductImage(BaseModel): __tablename__ = 'product_images' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) - product_id = Column(Integer, ForeignKey('products.id'), nullable=False) - product: Mapped["Product"] = relationship(back_populates='images') + id: Mapped[int] = mapped_column(primary_key=True) + product_id: Mapped[int] = mapped_column(ForeignKey('products.id'), nullable=False) + product: Mapped['Product'] = relationship(back_populates='images') - image_url = Column(String, nullable=False) + image_url: Mapped[str] = mapped_column(nullable=False) class ProductBarcode(BaseModel): __tablename__ = 'product_barcodes' - product_id = Column(Integer, ForeignKey('products.id'), nullable=False, comment='ID товара', primary_key=True) - product: Mapped["Product"] = relationship(back_populates='barcodes') + product_id: Mapped[int] = mapped_column( + ForeignKey('products.id'), + nullable=False, + comment='ID товара', + primary_key=True, + ) + product: Mapped['Product'] = relationship(back_populates='barcodes') - barcode = Column(String, nullable=False, index=True, comment='ШК товара', primary_key=True) + barcode: Mapped[str] = mapped_column(nullable=False, index=True, comment='ШК товара', primary_key=True) class ProductBarcodeImage(BaseModel): __tablename__ = 'product_barcode_images' - product_id = Column(Integer, ForeignKey('products.id'), primary_key=True, comment='ID товара') - product: Mapped["Product"] = relationship(back_populates='barcode_image') + product_id: Mapped[int] = mapped_column(ForeignKey('products.id'), primary_key=True, comment='ID товара') + product: Mapped['Product'] = relationship(back_populates='barcode_image') - filename = Column(String, nullable=False) + filename: Mapped[str] = mapped_column(nullable=False) diff --git a/models/project.py b/models/project.py index f56d713..af1cd00 100644 --- a/models/project.py +++ b/models/project.py @@ -3,22 +3,41 @@ from typing import TYPE_CHECKING from sqlalchemy.orm import mapped_column, Mapped, relationship -from models import BaseModel +from models.base import BaseModel if TYPE_CHECKING: from board import Board + from attribute import Attribute + from module import Module class Project(BaseModel): - __tablename__ = "projects" + __tablename__ = 'projects' id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(nullable=False) created_at: Mapped[datetime] = mapped_column(nullable=False) is_deleted: Mapped[bool] = mapped_column(default=False) - boards: Mapped[list["Board"]] = relationship( - "Board", - back_populates="project", - lazy="noload", + boards: Mapped[list['Board']] = relationship( + 'Board', + back_populates='project', + lazy='noload', ) + + attributes: Mapped[list['Attribute']] = relationship( + 'Attribute', + uselist=True, + secondary='project_attribute', + back_populates='projects', + lazy='selectin', + ) + + modules: Mapped[list['Module']] = relationship( + 'Module', + uselist=True, + secondary='project_module', + back_populates='projects', + lazy='selectin', + ) + diff --git a/models/secondary.py b/models/secondary.py index a4f04cd..5bad382 100644 --- a/models/secondary.py +++ b/models/secondary.py @@ -1,115 +1,138 @@ -from sqlalchemy import Table, Column, Integer, ForeignKey, ForeignKeyConstraint, UniqueConstraint, String +from typing import TYPE_CHECKING + +from sqlalchemy import Table, Column, ForeignKey, ForeignKeyConstraint, UniqueConstraint from sqlalchemy.orm import relationship, mapped_column, Mapped +from models import Product from models.base import BaseModel -deal_product_service_employees = Table( - 'deal_product_service_employees', +if TYPE_CHECKING: + from models import Card, Service, User + +card_product_service_employees = Table( + 'card_product_service_employees', BaseModel.metadata, - Column('deal_id', primary_key=True), + Column('card_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'] + ['card_id', 'product_id', 'service_id'], + ['card_product_services.card_id', 'card_product_services.product_id', 'card_product_services.service_id'] ) ) -deal_service_employees = Table( - 'deal_service_employees', +card_service_employees = Table( + 'card_service_employees', BaseModel.metadata, - Column('deal_id', primary_key=True), + Column('card_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'] + ['card_id', 'service_id'], + ['card_services.card_id', 'card_services.service_id'] ) ) -class DealService(BaseModel): - __tablename__ = 'deal_services' - deal_id = Column(Integer, ForeignKey('deals.id'), - nullable=False, - comment='ID Сделки', - primary_key=True) - deal = relationship('Deal', back_populates='services') +class CardService(BaseModel): + __tablename__ = 'card_services' + card_id: Mapped[int] = mapped_column( + ForeignKey('cards.id'), + comment='ID Сделки', + primary_key=True, + ) + card: Mapped['Card'] = relationship('Card', back_populates='services') - service_id = Column(Integer, ForeignKey('services.id'), nullable=False, comment='ID Услуги', primary_key=True) - service = relationship('Service') + service_id: Mapped[int] = mapped_column(ForeignKey('services.id'), nullable=False, comment='ID Услуги', primary_key=True) + service: Mapped['Service'] = relationship('Service') - quantity = Column(Integer, nullable=False, comment='Кол-во услуги') - price = Column(Integer, nullable=False, server_default='0', comment='Цена услуги') + quantity: Mapped[int] = mapped_column(nullable=False, comment='Кол-во услуги') + price: Mapped[int] = mapped_column(nullable=False, server_default='0', comment='Цена услуги') is_fixed_price: Mapped[bool] = mapped_column(default=False, server_default='0', comment='Фиксированная цена') - employees = relationship('User', secondary=deal_service_employees) + employees: Mapped[list['User']] = relationship('User', secondary=card_service_employees) __table_args__ = ( - UniqueConstraint('deal_id', 'service_id', name='uix_deal_service'), + UniqueConstraint('card_id', 'service_id', name='uix_card_service'), ) -class DealProductService(BaseModel): - __tablename__ = 'deal_product_services' - deal_id = Column(Integer, primary_key=True, nullable=False, comment='ID Сделки') +class CardProductService(BaseModel): + __tablename__ = 'card_product_services' + card_id: Mapped[int] = mapped_column(primary_key=True, nullable=False, comment='ID Сделки') - product_id = Column(Integer, primary_key=True, nullable=False, comment='ID Продукта') + product_id: Mapped[int] = mapped_column(primary_key=True, nullable=False, comment='ID Продукта') - service_id = Column(Integer, ForeignKey('services.id'), primary_key=True, nullable=False, comment='ID Услуги') + service_id: Mapped[int] = mapped_column( + ForeignKey('services.id'), + primary_key=True, + nullable=False, + comment='ID Услуги', + ) - price = Column(Integer, nullable=False, comment='Цена услуги') + price: Mapped[int] = mapped_column(nullable=False, comment='Цена услуги') is_fixed_price: Mapped[bool] = mapped_column(default=False, server_default='0', comment='Фиксированная цена') - deal_product = relationship('DealProduct', - back_populates='services', - primaryjoin="and_(DealProductService.deal_id == DealProduct.deal_id, " - "DealProductService.product_id == DealProduct.product_id)", - foreign_keys=[deal_id, product_id]) + card_product: Mapped['CardProduct'] = relationship( + 'CardProduct', + back_populates='services', + primaryjoin="and_(CardProductService.card_id == CardProduct.card_id, " + "CardProductService.product_id == CardProduct.product_id)", + foreign_keys=[card_id, product_id], + ) - service = relationship('Service', - foreign_keys=[service_id], - lazy='joined' - - ) - employees = relationship('User', - secondary=deal_product_service_employees, - ) + service: Mapped['Service'] = relationship( + 'Service', + foreign_keys=[service_id], + lazy='joined', + ) + employees: Mapped[list['User']] = relationship( + 'User', + secondary=card_product_service_employees, + ) __table_args__ = ( ForeignKeyConstraint( - ['deal_id', 'product_id'], - ['deal_products.deal_id', 'deal_products.product_id'] + ['card_id', 'product_id'], + ['card_products.card_id', 'card_products.product_id'] ), ) -class DealProduct(BaseModel): - __tablename__ = 'deal_products' - deal_id = Column(Integer, ForeignKey('deals.id'), primary_key=True, nullable=False, comment='ID Сделки') - product_id = Column(Integer, ForeignKey('products.id'), primary_key=True, nullable=False, comment='ID Продукта') - quantity = Column(Integer, nullable=False, comment='Кол-во продукта') - comment = Column(String, nullable=False, server_default='', comment='Комментарий к товару') +class CardProduct(BaseModel): + __tablename__ = 'card_products' + card_id: Mapped[int] = mapped_column(ForeignKey('cards.id'), primary_key=True, nullable=False, comment='ID карточки') + product_id: Mapped[int] = mapped_column( + ForeignKey('products.id'), + primary_key=True, + nullable=False, + comment='ID Продукта', + ) + quantity: Mapped[int] = mapped_column(nullable=False, comment='Кол-во продукта') + comment: Mapped[str] = mapped_column(nullable=False, server_default='', comment='Комментарий к товару') - deal = relationship('Deal', - back_populates='products', - foreign_keys=[deal_id]) - product = relationship( + card: Mapped['Card'] = relationship( + 'Card', + back_populates='products', + foreign_keys=[card_id], + ) + product: Mapped['Product'] = relationship( 'Product', lazy='joined', foreign_keys=[product_id], ) - services = relationship('DealProductService', - back_populates='deal_product', - cascade="all, delete-orphan", - primaryjoin="and_(DealProductService.deal_id == DealProduct.deal_id, " - "DealProductService.product_id == DealProduct.product_id)", - foreign_keys=[DealProductService.deal_id, DealProductService.product_id], - lazy='selectin', - order_by="desc(DealProductService.service_id)" - ) + services: Mapped[list['CardProductService']] = relationship( + 'CardProductService', + back_populates='card_product', + cascade="all, delete-orphan", + primaryjoin="and_(CardProductService.card_id == CardProduct.card_id, " + "CardProductService.product_id == CardProduct.product_id)", + foreign_keys=[CardProductService.card_id, CardProductService.product_id], + lazy='selectin', + order_by="desc(CardProductService.service_id)" + ) barcode_template_attribute_link = Table( diff --git a/models/sequences.py b/models/sequences.py deleted file mode 100644 index 9642fdf..0000000 --- a/models/sequences.py +++ /dev/null @@ -1,4 +0,0 @@ -from sqlalchemy import Sequence - -from models import BaseModel - diff --git a/models/service.py b/models/service.py index 6beb0d1..2e3721d 100644 --- a/models/service.py +++ b/models/service.py @@ -1,4 +1,4 @@ -from sqlalchemy import Column, Integer, String, ForeignKey, Double, asc, Table +from sqlalchemy import Column, Integer, String, ForeignKey, Double, Table from sqlalchemy.orm import relationship, mapped_column, Mapped import enums.service @@ -14,11 +14,15 @@ services_kit_services = Table( class Service(BaseModel): __tablename__ = 'services' - id: Mapped[int] = mapped_column(Integer, autoincrement=True, primary_key=True, index=True) - name = Column(String, nullable=False, comment='Название услуги') + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False, comment='Название услуги') - category_id = Column(Integer, ForeignKey('service_categories.id'), nullable=False, comment='ID категории услуги') - category = relationship('ServiceCategory', lazy='joined') + category_id: Mapped[int] = mapped_column( + ForeignKey('service_categories.id'), + nullable=False, + comment='ID категории услуги', + ) + category: Mapped['ServiceCategory'] = relationship('ServiceCategory', lazy='joined') is_deleted: Mapped[bool] = mapped_column( nullable=False, server_default='0', @@ -37,15 +41,18 @@ class Service(BaseModel): comment='Себестоимость услуги' ) - service_type = Column(Integer, - server_default=f'{enums.service.ServiceType.DEAL_SERVICE}', - nullable=False, - comment='Тип услуги') - price_ranges = relationship('ServicePriceRange', - back_populates='service', - lazy='selectin', - order_by="asc(ServicePriceRange.from_quantity)", - cascade="all, delete-orphan") + service_type: Mapped[int] = mapped_column( + server_default=f'{enums.service.ServiceType.DEAL_SERVICE}', + nullable=False, + comment='Тип услуги', + ) + price_ranges: Mapped[list['ServicePriceRange']] = relationship( + 'ServicePriceRange', + back_populates='service', + lazy='selectin', + order_by="asc(ServicePriceRange.from_quantity)", + cascade="all, delete-orphan", + ) rank: Mapped[str] = mapped_column( nullable=False, server_default='', @@ -55,25 +62,25 @@ class Service(BaseModel): class ServicePriceRange(BaseModel): __tablename__ = 'service_price_ranges' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) - service_id = Column(Integer, ForeignKey('services.id'), nullable=False, comment='ID услуги') - service = relationship('Service', back_populates='price_ranges') - from_quantity = Column(Integer, nullable=False, comment='От количества') - to_quantity = Column(Integer, nullable=False, comment='До количества') - price = Column(Double, nullable=False, comment='Цена') + id: Mapped[int] = mapped_column(primary_key=True) + service_id: Mapped[int] = mapped_column(ForeignKey('services.id'), nullable=False, comment='ID услуги') + service: Mapped['Service'] = relationship('Service', back_populates='price_ranges') + from_quantity: Mapped[int] = mapped_column(nullable=False, comment='От количества') + to_quantity: Mapped[int] = mapped_column(nullable=False, comment='До количества') + price: Mapped[float] = mapped_column(Double, nullable=False, comment='Цена') class ServiceCategory(BaseModel): __tablename__ = 'service_categories' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) - name = Column(String, nullable=False) + id: Mapped[int] = mapped_column(primary_key=True) + name: Mapped[str] = mapped_column(nullable=False) is_deleted: Mapped[bool] = mapped_column( nullable=False, server_default='0', comment='Удалена ли категория' ) - deal_service_rank: Mapped[str] = mapped_column( + card_service_rank: Mapped[str] = mapped_column( nullable=False, server_default='', comment='Ранг услуги для сделки' diff --git a/models/shipping.py b/models/shipping.py index 1f367a9..241cd39 100644 --- a/models/shipping.py +++ b/models/shipping.py @@ -5,15 +5,15 @@ from sqlalchemy.orm import Mapped, mapped_column, relationship from models import BaseModel if TYPE_CHECKING: - from models import Deal, Product, Client + from models import Card, Product, Client class Pallet(BaseModel): __tablename__ = 'pallets' id: Mapped[int] = mapped_column(primary_key=True) - deal_id: Mapped[int] = mapped_column(ForeignKey('deals.id')) - deal: Mapped['Deal'] = relationship(back_populates='pallets') + card_id: Mapped[int] = mapped_column(ForeignKey('cards.id')) + card: Mapped['Card'] = relationship(back_populates='pallets') boxes: Mapped[list['Box']] = relationship( back_populates='pallet', @@ -54,5 +54,5 @@ class Box(BaseModel): pallet_id: Mapped[Optional[int]] = mapped_column(ForeignKey('pallets.id')) pallet: Mapped[Pallet] = relationship(back_populates='boxes') - deal_id: Mapped[Optional[int]] = mapped_column(ForeignKey('deals.id')) - deal: Mapped['Deal'] = relationship(back_populates='boxes') + card_id: Mapped[Optional[int]] = mapped_column(ForeignKey('cards.id')) + card: Mapped['Card'] = relationship(back_populates='boxes') diff --git a/models/status.py b/models/status.py index e34cbdd..717d921 100644 --- a/models/status.py +++ b/models/status.py @@ -1,16 +1,17 @@ +from datetime import datetime from typing import TYPE_CHECKING -from sqlalchemy import ForeignKey, Column, Integer, DateTime, String +from sqlalchemy import ForeignKey from sqlalchemy.orm import Mapped, mapped_column, relationship from models import BaseModel if TYPE_CHECKING: - from models import Board + from models import Board, Card, User -class DealStatus(BaseModel): - __tablename__ = "deal_statuses" +class CardStatus(BaseModel): + __tablename__ = 'card_statuses' id: Mapped[int] = mapped_column(primary_key=True) name: Mapped[str] = mapped_column(nullable=False) @@ -19,43 +20,44 @@ class DealStatus(BaseModel): is_deleted: Mapped[bool] = mapped_column(default=False, nullable=False) board_id: Mapped[int] = mapped_column(ForeignKey('boards.id'), nullable=False) - board: Mapped["Board"] = relationship("Board", back_populates="deal_statuses") + board: Mapped['Board'] = relationship('Board', back_populates='statuses') -class DealStatusHistory(BaseModel): - __tablename__ = 'deals_status_history' - id = Column(Integer, autoincrement=True, primary_key=True, index=True) +class CardStatusHistory(BaseModel): + __tablename__ = 'cards_status_history' + id: Mapped[int] = mapped_column(primary_key=True) - deal_id = Column(Integer, ForeignKey('deals.id'), nullable=False, comment='ID сделки') - deal = relationship('Deal', back_populates='status_history') + card_id: Mapped[int] = mapped_column(ForeignKey('cards.id'), nullable=False, comment='ID карточки') + card: Mapped['Card'] = relationship('Card', back_populates='status_history') - user_id = Column(Integer, ForeignKey('users.id'), nullable=False) - user = relationship('User') + user_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=False) + user: Mapped['User'] = relationship('User') - changed_at = Column(DateTime, nullable=False, comment='Дата и время когда произошла смена статуса') + changed_at: Mapped[datetime] = mapped_column(nullable=False, comment='Дата и время когда произошла смена статуса') from_status_id: Mapped[int] = mapped_column( - ForeignKey('deal_statuses.id'), + ForeignKey('card_statuses.id'), nullable=False, comment='Предыдущий статус', ) - from_status: Mapped[DealStatus] = relationship( - 'DealStatus', + from_status: Mapped[CardStatus] = relationship( + 'CardStatus', foreign_keys=[from_status_id], lazy='joined', ) to_status_id: Mapped[int] = mapped_column( - ForeignKey('deal_statuses.id'), + ForeignKey('card_statuses.id'), nullable=False, comment='Новый статус', ) - to_status: Mapped[DealStatus] = relationship( - 'DealStatus', + to_status: Mapped[CardStatus] = relationship( + 'CardStatus', foreign_keys=[to_status_id], lazy='joined', ) - next_status_deadline = Column(DateTime, - comment='Дедлайн до которого сделку нужно перевести на следующий этап') - comment = Column(String, nullable=False, comment='Коментарий', server_default='') + next_status_deadline: Mapped[datetime] = mapped_column( + comment='Дедлайн до которого сделку нужно перевести на следующий этап', + ) + comment: Mapped[str] = mapped_column(nullable=False, comment='Комментарий', server_default='') diff --git a/parsers/deal_parser.py b/parsers/deal_parser.py index 631b620..1dcbec2 100644 --- a/parsers/deal_parser.py +++ b/parsers/deal_parser.py @@ -9,7 +9,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from models import ProductBarcode, Product, ShippingWarehouse, BaseMarketplace -from schemas.deal import ParsedProductRowSchema, ParseDealsExcelResponse, ParsedCityBreakdownSchema, \ +from schemas.card import ParsedProductRowSchema, ParseCardsExcelResponse, ParsedCityBreakdownSchema, \ OptionalShippingWarehouseSchema from schemas.marketplace import MarketplaceSchema, BaseMarketplaceSchema from schemas.product import ProductSchema @@ -133,7 +133,7 @@ class DealParser: return rows - async def parse(self, file_bytes: bytes) -> ParseDealsExcelResponse: + async def parse(self, file_bytes: bytes) -> ParseCardsExcelResponse: p = mock.patch('openpyxl.styles.fonts.Font.family.max', new=100) p.start() @@ -144,4 +144,4 @@ class DealParser: rows = await self._parse_barcodes(ws) - return ParseDealsExcelResponse(rows=rows, errors=self._errors) + return ParseCardsExcelResponse(rows=rows, errors=self._errors) diff --git a/routers/__init__.py b/routers/__init__.py index 3fa94a5..e223896 100644 --- a/routers/__init__.py +++ b/routers/__init__.py @@ -1,6 +1,6 @@ from .auth import auth_router -from .deal import deal_router -from .group import deal_group_router +from .card import card_router +from .group import card_group_router from .client import client_router from .service import service_router from .product import product_router diff --git a/routers/billing.py b/routers/billing.py index 1d7a3b5..d1d2447 100644 --- a/routers/billing.py +++ b/routers/billing.py @@ -29,34 +29,34 @@ async def webhook( @billing_router.post( '/create-deal-bill', operation_id='create_deal_bill', - response_model=CreateDealBillResponse + response_model=CreateCardBillResponse ) async def create_deal_bill( session: SessionDependency, - request: CreateDealBillRequest, + request: CreateCardBillRequest, user: CurrentUserDependency ): - return await BillingService(session).create_deal_billing(user, request) + return await BillingService(session).create_card_billing(user, request) @billing_router.post( '/cancel-deal-bill', operation_id='cancel_deal_bill', - response_model=CancelDealBillResponse + response_model=CancelCardBillResponse ) async def cancel_deal_billing( session: SessionDependency, - request: CancelDealBillRequest, + request: CancelCardBillRequest, user: CurrentUserDependency ): - return await BillingService(session).cancel_deal_billing(user, request) + return await BillingService(session).cancel_card_billing(user, request) @billing_router.get( '/deal-bill-request/{deal_id}', - response_model=GetDealBillById, + response_model=GetCardBillById, operation_id='get_deal_bill_by_id' ) async def get_deal_bill_by_id( deal_id: int, session: SessionDependency ): - return await BillingService(session).get_deal_bill_by_id(deal_id) + return await BillingService(session).get_card_bill_by_id(deal_id) diff --git a/routers/deal.py b/routers/card.py similarity index 52% rename from routers/deal.py rename to routers/card.py index 5fc7569..0236d9d 100644 --- a/routers/deal.py +++ b/routers/card.py @@ -10,170 +10,169 @@ from backend.session import get_session from generators.deal_pdf_generator.generator import DealTechSpecPdfGenerator from models import User from parsers import DealParser -from schemas.barcode import GetDealProductsBarcodesPdfRequest, GetDealProductsBarcodesPdfResponse -from schemas.deal import * +from schemas.barcode import GetCardProductsBarcodesPdfRequest, GetCardProductsBarcodesPdfResponse +from schemas.card import * from services.auth import get_current_user, authorized_user, guest_user from services.barcode import BarcodeService from services.billing import BillingService -from services.deal import DealService +from services.card import CardsService -deal_router = APIRouter( - prefix='/deal', - tags=['deal'], +card_router = APIRouter( + prefix='/card', + tags=['card'], ) -# region Deal +# region Card -@deal_router.post( +@card_router.post( '/delete', - response_model=DealDeleteResponse, - operation_id='deleteDeal', + response_model=CardDeleteResponse, + operation_id='deleteCard', dependencies=[Depends(authorized_user)] ) async def delete( - request: DealDeleteRequest, + request: CardDeleteRequest, session: Annotated[AsyncSession, Depends(get_session)] ): - return await DealService(session).delete(request) + return await CardsService(session).delete(request) -@deal_router.post( +@card_router.post( '/complete', - response_model=DealCompleteResponse, - operation_id='completeDeal', + response_model=CardCompleteResponse, + operation_id='completeCard', dependencies=[Depends(authorized_user)] ) async def complete( - request: DealCompleteRequest, + request: CardCompleteRequest, session: SessionDependency, user: CurrentUserDependency ): - return await DealService(session).complete(user, request) + return await CardsService(session).complete(user, request) -@deal_router.post( +@card_router.post( '/quickCreate', - response_model=DealQuickCreateResponse, + response_model=CardQuickCreateResponse, dependencies=[Depends(authorized_user)] ) async def quick_create( - request: DealQuickCreateRequest, + request: CardQuickCreateRequest, session: Annotated[AsyncSession, Depends(get_session)], user: Annotated[User, Depends(get_current_user)] ): - return await DealService(session).quick_create(request, user) + return await CardsService(session).quick_create(request, user) -@deal_router.post( +@card_router.post( '/changeStatus', - response_model=DealChangeStatusResponse, + response_model=CardChangeStatusResponse, dependencies=[Depends(authorized_user)] ) async def change_status( - request: DealChangeStatusRequest, + request: CardChangeStatusRequest, session: Annotated[AsyncSession, Depends(get_session)], user: Annotated[User, Depends(get_current_user)] ): - return await DealService(session).change_status_manual(request, user) + return await CardsService(session).change_status_manual(request, user) -@deal_router.get( +@card_router.get( '/summaries', - response_model=DealSummaryResponse, - operation_id='getDealSummaries', + response_model=CardSummaryResponse, + operation_id='getCardSummaries', dependencies=[Depends(authorized_user)] ) async def get_summary( session: Annotated[AsyncSession, Depends(get_session)], full: Optional[bool] ): - return await DealService(session).get_summary(full) + return await CardsService(session).get_summary(full) -@deal_router.post( +@card_router.post( '/summaries/reorder', - response_model=DealSummaryResponse, - operation_id='reorderDealSummaries', + response_model=CardSummaryResponse, + operation_id='reorderCardSummaries', dependencies=[Depends(authorized_user)] ) async def reorder( session: Annotated[AsyncSession, Depends(get_session)], - request: DealSummaryReorderRequest, + request: CardSummaryReorderRequest, user: Annotated[User, Depends(get_current_user)] ): - return await DealService(session).reorder(request, user) + return await CardsService(session).reorder(request, user) -@deal_router.get( +@card_router.get( '/get-all', - response_model=DealGetAllResponse, - operation_id='getAllDeals', + response_model=CardGetAllResponse, + operation_id='getAllCards', dependencies=[Depends(authorized_user)] ) async def get_all( session: Annotated[AsyncSession, Depends(get_session)] ): - return await DealService(session).get_all() + return await CardsService(session).get_all() -# endpoint to get deal by id -@deal_router.get( - '/get/{deal_id}', - response_model=DealSchema, - operation_id='getDealById', +# endpoint to get card by id +@card_router.get( + '/get/{card_id}', + response_model=CardSchema, + operation_id='getCardById', dependencies=[Depends(guest_user)] ) -async def get_deal_by_id( - deal_id: int, +async def get_card_by_id( + card_id: int, user: CurrentUserDependency, session: Annotated[AsyncSession, Depends(get_session)] ): - return await DealService(session).get_by_id(user, deal_id) + return await CardsService(session).get_by_id(user, card_id) -@deal_router.post( +@card_router.post( '/update-general-info', - response_model=DealUpdateGeneralInfoResponse, - operation_id='updateDealGeneralInfo', + response_model=CardUpdateGeneralInfoResponse, + operation_id='updateCardGeneralInfo', dependencies=[Depends(authorized_user)] ) async def update_general_info( - request: DealUpdateGeneralInfoRequest, + request: CardUpdateGeneralInfoRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency, ): - return await DealService(session).update_general_info(request, user) + return await CardsService(session).update_general_info(request, user) -@deal_router.post( +@card_router.post( '/add-kit', - response_model=DealAddKitResponse, - operation_id='add_kit_to_deal' - + response_model=CardAddKitResponse, + operation_id='add_kit_to_card' ) -async def add_kit_to_deal( +async def add_kit_to_card( session: SessionDependency, - request: DealAddKitRequest + request: CardAddKitRequest ): - return await DealService(session).add_kit_to_deal(request) + return await CardsService(session).add_kit_to_card(request) -@deal_router.post( +@card_router.post( '/create-guest-url', - response_model=DealCreateGuestUrlResponse, + response_model=CardCreateGuestUrlResponse, operation_id='create_deal_guest_url', dependencies=[Depends(authorized_user)] ) async def create_guest_url( session: SessionDependency, - request: DealCreateGuestUrlRequest, + request: CardCreateGuestUrlRequest, user: CurrentUserDependency ): - return DealService(session).create_guest_url(user, request) + return CardsService(session).create_guest_url(user, request) -@deal_router.get( +@card_router.get( '/billing-document/{deal_id}', operation_id='get_billing_document', # dependencies=[Depends(authorized_user)], @@ -186,7 +185,7 @@ async def get_billing_document( return Response(pdf_file.getvalue(), media_type='application/pdf') -@deal_router.get( +@card_router.get( '/tech-spec/{deal_id}', operation_id='get_deal_tech_spec', # dependencies=[Depends(authorized_user)], @@ -199,33 +198,33 @@ async def get_deal_tech_spec( return Response(pdf_file.getvalue(), media_type='application/pdf') -@deal_router.post( +@card_router.post( '/prefill', - response_model=DealPrefillResponse, - operation_id='prefill_deal', + response_model=CardPrefillResponse, + operation_id='prefill_card', dependencies=[Depends(authorized_user)] ) -async def post_prefill_deal( +async def post_prefill_card( session: SessionDependency, - request: DealPrefillRequest, + request: CardPrefillRequest, user: CurrentUserDependency ): - return await DealService(session).prefill_deal(user, request) + return await CardsService(session).prefill_card_products_and_services(user, request) -@deal_router.post( +@card_router.post( '/recalculate-price', - response_model=DealRecalculatePriceResponse, - operation_id='recalculate_deal_price', + response_model=CardRecalculatePriceResponse, + operation_id='recalculate_card_price', ) -async def recalculate_deal_price( +async def recalculate_card_price( session: SessionDependency, - request: DealRecalculatePriceRequest, + request: CardRecalculatePriceRequest, ): - return await DealService(session).recalculate_price(request) + return await CardsService(session).recalculate_price(request) -@deal_router.post( +@card_router.post( '/employee', response_model=ManageEmployeeResponse, operation_id='manage_employee', @@ -234,24 +233,24 @@ async def manage_employee( session: SessionDependency, request: ManageEmployeeRequest, ): - return await DealService(session).manage_employee(request) + return await CardsService(session).manage_employee(request) -@deal_router.get( - '/employee/available/{deal_id}', +@card_router.get( + '/employee/available/{card_id}', response_model=GetAvailableEmployeesToAssignResponse, operation_id='get_available_employees_to_assign', ) async def get_available_employees_to_assign( session: Annotated[AsyncSession, Depends(get_session)], - deal_id: int, + card_id: int, ): - return await DealService(session).get_available_employees_to_assign(deal_id) + return await CardsService(session).get_available_employees_to_assign(card_id) -@deal_router.post( +@card_router.post( '/prefill/excel/parse', - response_model=ParseDealsExcelResponse, + response_model=ParseCardsExcelResponse, operation_id='parse_deals_excel', ) async def parse_deals_excel( @@ -262,221 +261,220 @@ async def parse_deals_excel( return await DealParser(session).parse(file_bytes) -@deal_router.post( +@card_router.post( '/prefill/excel/create', - response_model=CreateDealsFromExcelResponse, + response_model=CreateCardsFromExcelResponse, operation_id='create_deals_excel', ) async def create_deals_from_excel( session: Annotated[AsyncSession, Depends(get_session)], - request: CreateDealsFromExcelRequest, + request: CreateCardsFromExcelRequest, user: CurrentUserDependency, ): - return await DealService(session).create_deals_from_excel(request, user) + return await CardsService(session).create_cards_from_excel(request, user) # endregion -# region Deal services +# region Card services -@deal_router.post( +@card_router.post( '/services/add/multiple', - response_model=DealAddServicesResponse, - operation_id='addMultipleDealServices', + response_model=CardAddServicesResponse, + operation_id='add_multiple_card_services', dependencies=[Depends(guest_user)] ) async def services_add( - request: DealAddServicesRequest, + request: CardAddServicesRequest, session: Annotated[AsyncSession, Depends(get_session)], ): - return await DealService(session).add_services(request) + return await CardsService(session).add_services(request) -@deal_router.post( +@card_router.post( '/services/add', - response_model=DealAddServiceResponse, - operation_id='addDealService', + response_model=CardAddServiceResponse, + operation_id='add_card_service', dependencies=[Depends(guest_user)] ) async def services_add( - request: DealAddServiceRequest, + request: CardAddServiceRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).add_service(user, request) + return await CardsService(session).add_service(user, request) -@deal_router.post( +@card_router.post( '/services/update-quantity', - response_model=DealUpdateServiceQuantityResponse, - operation_id='updateDealServiceQuantity', + response_model=CardUpdateServiceQuantityResponse, + operation_id='update_card_service_quantity', dependencies=[Depends(guest_user)] ) async def services_update_quantity( - request: DealUpdateServiceQuantityRequest, + request: CardUpdateServiceQuantityRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).update_service_quantity(user, request) + return await CardsService(session).update_service_quantity(user, request) -@deal_router.post( +@card_router.post( '/services/update', - response_model=DealUpdateServiceResponse, - operation_id='updateDealService', + response_model=CardUpdateServiceResponse, + operation_id='update_card_service', dependencies=[Depends(guest_user)] ) async def services_update( - request: DealUpdateServiceRequest, + request: CardUpdateServiceRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).update_service(user, request) + return await CardsService(session).update_service(user, request) -@deal_router.post( +@card_router.post( '/services/delete', - response_model=DealDeleteServiceResponse, - operation_id='deleteDealService', + response_model=CardDeleteServiceResponse, + operation_id='delete_card_service', dependencies=[Depends(guest_user)] ) async def services_delete( - request: DealDeleteServiceRequest, + request: CardDeleteServiceRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).delete_service(user, request) + return await CardsService(session).delete_service(user, request) -@deal_router.post( +@card_router.post( '/services/delete/multiple', - response_model=DealDeleteServicesResponse, - operation_id='deleteMultipleDealServices', + response_model=CardDeleteServicesResponse, + operation_id='delete_multiple_card_services', dependencies=[Depends(guest_user)] ) async def services_delete( - request: DealDeleteServicesRequest, + request: CardDeleteServicesRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).delete_services(user, request) + return await CardsService(session).delete_services(user, request) -@deal_router.post( +@card_router.post( '/services/copy', - response_model=DealServicesCopyResponse, + response_model=CardServicesCopyResponse, operation_id='copy_product_services', dependencies=[Depends(guest_user)] ) async def services_copy( session: SessionDependency, - request: DealServicesCopyRequest, + request: CardServicesCopyRequest, user: CurrentUserDependency ): - return await DealService(session).copy_services(user, request) + return await CardsService(session).copy_services(user, request) # endregion -# region Deal products -@deal_router.post( +# region Card products +@card_router.post( '/products/update-quantity', - response_model=DealUpdateProductQuantityResponse, - operation_id='updateDealProductQuantity', + response_model=CardUpdateProductQuantityResponse, + operation_id='update_card_product_quantity', dependencies=[Depends(guest_user)] ) async def products_update( - request: DealUpdateProductQuantityRequest, + request: CardUpdateProductQuantityRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).update_product_quantity(user, request) + return await CardsService(session).update_product_quantity(user, request) -@deal_router.post( +@card_router.post( '/products/add', - response_model=DealAddProductResponse, - operation_id='addDealProduct', + response_model=CardAddProductResponse, + operation_id='add_card_product', dependencies=[Depends(guest_user)] ) async def products_add( - request: DealAddProductRequest, + request: CardAddProductRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).add_product(user, request) + return await CardsService(session).add_product(user, request) -@deal_router.post( +@card_router.post( '/products/delete', - response_model=DealDeleteProductResponse, - operation_id='deleteDealProduct', + response_model=CardDeleteProductResponse, + operation_id='delete_card_product', dependencies=[Depends(guest_user)] ) async def products_delete( - request: DealDeleteProductRequest, + request: CardDeleteProductRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency - ): - return await DealService(session).delete_product(user, request) + return await CardsService(session).delete_product(user, request) -@deal_router.post( +@card_router.post( '/products/delete/multiple', - response_model=DealDeleteProductsResponse, - operation_id='deleteMultipleDealProducts', + response_model=CardDeleteProductsResponse, + operation_id='delete_multiple_card_products', dependencies=[Depends(guest_user)] ) async def products_delete( - request: DealDeleteProductsRequest, + request: CardDeleteProductsRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).delete_products(user, request) + return await CardsService(session).delete_products(user, request) -@deal_router.post( +@card_router.post( '/product/update', - response_model=DealUpdateProductResponse, - operation_id='updateDealProduct', + response_model=CardUpdateProductResponse, + operation_id='update_card_product', dependencies=[Depends(guest_user)] ) async def products_update( - request: DealUpdateProductRequest, + request: CardUpdateProductRequest, session: Annotated[AsyncSession, Depends(get_session)], user: CurrentUserDependency ): - return await DealService(session).update_product(user, request) + return await CardsService(session).update_product(user, request) -@deal_router.post( +@card_router.post( '/product/add-kit', - response_model=DealProductAddKitResponse, - operation_id='add_kit_to_deal_product', + response_model=CardProductAddKitResponse, + operation_id='add_kit_to_card_product', dependencies=[Depends(guest_user)] ) -async def add_kit_to_deal_product( +async def add_kit_to_card_product( session: SessionDependency, - request: DealProductAddKitRequest, + request: CardProductAddKitRequest, user: CurrentUserDependency ): - return await DealService(session).add_kit_to_deal_product(user, request) + return await CardsService(session).add_kit_to_card_product(user, request) -@deal_router.post( +@card_router.post( '/barcodes/get-pdf', - operation_id='get_deal_products_barcodes_pdf', - response_model=GetDealProductsBarcodesPdfResponse + operation_id='get_card_products_barcodes_pdf', + response_model=GetCardProductsBarcodesPdfResponse ) -async def get_deal_products_barcodes_pdf( - request: GetDealProductsBarcodesPdfRequest, +async def get_card_products_barcodes_pdf( + request: GetCardProductsBarcodesPdfRequest, session: Annotated[AsyncSession, Depends(get_session)] ): - filename, pdf_buffer = await BarcodeService(session).get_deal_barcodes_pdf(request) + filename, pdf_buffer = await BarcodeService(session).get_card_barcodes_pdf(request) pdf_buffer: BytesIO base64_string = base64.b64encode(pdf_buffer.read()).decode('utf-8') - return GetDealProductsBarcodesPdfResponse( + return GetCardProductsBarcodesPdfResponse( base64_string=base64_string, filename=filename, mime_type='application/pdf' diff --git a/routers/group.py b/routers/group.py index 01ca1aa..7543185 100644 --- a/routers/group.py +++ b/routers/group.py @@ -3,77 +3,77 @@ from fastapi import APIRouter, Depends from backend.dependecies import SessionDependency, CurrentUserDependency from schemas.group import * from services.auth import authorized_user -from services.deal_group import DealGroupService +from services.card_group import CardGroupService -deal_group_router = APIRouter( - prefix='/deal-group', - tags=['deal-group'], +card_group_router = APIRouter( + prefix='/card-group', + tags=['card-group'], ) -@deal_group_router.patch( +@card_group_router.patch( '/', - response_model=DealGroupUpdateResponse, - operation_id='update_deal_group', + response_model=CardGroupUpdateResponse, + operation_id='update_card_group', dependencies=[Depends(authorized_user)] ) async def update_group( - request: DealGroupUpdateRequest, + request: CardGroupUpdateRequest, session: SessionDependency, ): - return await DealGroupService(session).update_group(request) + return await CardGroupService(session).update_group(request) -@deal_group_router.post( +@card_group_router.post( '/', - response_model=DealCreateGroupResponse, - operation_id='create_deal_group', + response_model=CardCreateGroupResponse, + operation_id='create_card_group', dependencies=[Depends(authorized_user)] ) async def create_group( - request: DealCreateGroupRequest, + request: CreateCardGroupRequest, session: SessionDependency, user: CurrentUserDependency ): - return await DealGroupService(session).create_group(user, request) + return await CardGroupService(session).create_group(user, request) -@deal_group_router.patch( +@card_group_router.patch( '/change-status', - response_model=DealGroupChangeStatusResponse, + response_model=CardGroupChangeStatusResponse, operation_id='change_status', dependencies=[Depends(authorized_user)] ) async def change_status( - request: DealGroupChangeStatusRequest, + request: CardGroupChangeStatusRequest, session: SessionDependency, user: CurrentUserDependency ): - return await DealGroupService(session).change_group_status(user,request) + return await CardGroupService(session).change_group_status(user,request) -@deal_group_router.post( - '/deal', - response_model=DealAddToGroupResponse, - operation_id='add_deal', +@card_group_router.post( + '/card', + response_model=CardAddToGroupResponse, + operation_id='add_card', dependencies=[Depends(authorized_user)] ) -async def add_deal( - request: DealAddToGroupRequest, +async def add_card( + request: CardAddToGroupRequest, session: SessionDependency, user: CurrentUserDependency ): - return await DealGroupService(session).add_deal(user, request) + return await CardGroupService(session).add_card(user, request) -@deal_group_router.delete( - '/deal', - response_model=DealRemoveFromGroupResponse, - operation_id='remove_deal', +@card_group_router.delete( + '/card', + response_model=CardRemoveFromGroupResponse, + operation_id='remove_card', dependencies=[Depends(authorized_user)] ) -async def remove_deal( - request: DealRemoveFromGroupRequest, +async def remove_card( + request: CardRemoveFromGroupRequest, session: SessionDependency, ): - return await DealGroupService(session).remove_deal(request) + return await CardGroupService(session).remove_card(request) diff --git a/routers/shipping.py b/routers/shipping.py index d8ff2f7..697e609 100644 --- a/routers/shipping.py +++ b/routers/shipping.py @@ -15,16 +15,16 @@ shipping_router = APIRouter( @shipping_router.post( - '/pallet/{deal_id}', + '/pallet/{card_id}', response_model=CreatePalletResponse, operation_id='create_pallet', dependencies=[Depends(authorized_user)], ) async def create_pallet( session: SessionDependency, - deal_id: int, + card_id: int, ): - return await ShippingService(session).create_pallet(deal_id) + return await ShippingService(session).create_pallet(card_id) @shipping_router.delete( diff --git a/schemas/attribute.py b/schemas/attribute.py new file mode 100644 index 0000000..c645d20 --- /dev/null +++ b/schemas/attribute.py @@ -0,0 +1,43 @@ +import pickle +from datetime import datetime, date +from typing import Optional + +from pydantic import field_validator + +from schemas.base import BaseSchema + + +# region Entities + +class AttributeTypeSchema(BaseSchema): + id: int + type: str + name: str + is_deleted: bool + + +class AttributeSchema(BaseSchema): + id: int + label: str + name: str + is_applicable_to_group: bool + is_nullable: bool + default_value: Optional[bool | int | float | str | date | datetime] + type: AttributeTypeSchema + + @field_validator("default_value", mode="before") + def validate_default_value(cls, value: Optional[bytes]): + return pickle.loads(value) if value else None + + +class CardAttributeSchema(BaseSchema): + value: Optional[bool | int | float | str | date | datetime] + card_id: int + attribute: AttributeSchema + + @field_validator("value", mode="before") + def validate_value(cls, value: Optional[bytes]): + return pickle.loads(value) if value else None + + +# endregion diff --git a/schemas/barcode.py b/schemas/barcode.py index d794ad6..c8ffebf 100644 --- a/schemas/barcode.py +++ b/schemas/barcode.py @@ -81,8 +81,8 @@ class GetProductBarcodePdfRequest(GetProductBarcodeRequest): quantity: int -class GetDealProductsBarcodesPdfRequest(BaseSchema): - deal_id: int +class GetCardProductsBarcodesPdfRequest(BaseSchema): + card_id: int # endregion @@ -131,7 +131,7 @@ class GetProductBarcodePdfResponse(BaseSchema): mime_type: str -class GetDealProductsBarcodesPdfResponse(BaseSchema): +class GetCardProductsBarcodesPdfResponse(BaseSchema): base64_string: str filename: str mime_type: str diff --git a/schemas/billing.py b/schemas/billing.py index 21d6b6b..de8cafa 100644 --- a/schemas/billing.py +++ b/schemas/billing.py @@ -3,14 +3,16 @@ from typing import Optional from schemas.base import BaseSchema, OkMessageSchema + # region Entities -class DealBillRequestSchema(BaseSchema): - deal_id: int +class CardBillRequestSchema(BaseSchema): + card_id: int created_at: datetime.datetime paid: bool pdf_url: Optional[str] invoice_number: Optional[str] + class GroupBillRequestSchema(BaseSchema): group_id: int created_at: datetime.datetime @@ -18,28 +20,30 @@ class GroupBillRequestSchema(BaseSchema): pdf_url: Optional[str] invoice_number: Optional[str] + # endregion # region Requests -class CreateDealBillRequest(BaseSchema): - deal_id: int +class CreateCardBillRequest(BaseSchema): + card_id: int -class CancelDealBillRequest(BaseSchema): - deal_id: int +class CancelCardBillRequest(BaseSchema): + card_id: int + # endregion # region Responses -class CreateDealBillResponse(OkMessageSchema): +class CreateCardBillResponse(OkMessageSchema): pass -class CancelDealBillResponse(OkMessageSchema): +class CancelCardBillResponse(OkMessageSchema): pass -class GetDealBillById(BaseSchema): - deal_bill: DealBillRequestSchema +class GetCardBillById(BaseSchema): + card_bill: CardBillRequestSchema # endregion diff --git a/schemas/board.py b/schemas/board.py index 9295e12..bbf6ef5 100644 --- a/schemas/board.py +++ b/schemas/board.py @@ -13,7 +13,7 @@ class BaseBoardSchema(BaseSchema): class BoardSchema(BaseBoardSchema): id: int ordinal_number: int - deal_statuses: list[StatusSchema] + statuses: list[StatusSchema] project: ProjectSchema diff --git a/schemas/card.py b/schemas/card.py new file mode 100644 index 0000000..c76214f --- /dev/null +++ b/schemas/card.py @@ -0,0 +1,421 @@ +from datetime import datetime, date +from typing import List, Optional, Union + +from pydantic import constr + +from schemas.attribute import CardAttributeSchema +from schemas.base import BaseSchema, OkMessageSchema +from schemas.billing import CardBillRequestSchema +from schemas.board import BoardSchema +from schemas.client import ClientSchema +from schemas.group import CardGroupSchema +from schemas.marketplace import BaseMarketplaceSchema +from schemas.product import ProductSchema +from schemas.service import ServiceSchema +from schemas.shipping import PalletSchema, BoxSchema +from schemas.shipping_warehouse import ShippingWarehouseSchema, BaseShippingWarehouseSchema +from schemas.status import StatusSchema, CardStatusHistorySchema +from schemas.user import UserSchema + + +# region Entities + +class BaseSchemaWithAttributes(BaseSchema): + attributes: dict[str, Union[bool, str, int, float, date, datetime, None]] = None + + def validate_attributes(cls, value): + for attr_name, attr_value in value.items(): + if not isinstance(attr_value, (str, int, float, date, datetime, None)): + raise ValueError(f"Invalid type for attribute '{attr_name}': {type(attr_value)}") + return value + + +class CardSummary(BaseSchema): + id: int + name: str + client_name: str + created_at: datetime + status: StatusSchema + board: BoardSchema + total_price: int + rank: int + base_marketplace: Optional[BaseMarketplaceSchema] = None + total_products: int + + shipment_warehouse_id: Optional[int] + shipment_warehouse_name: Optional[str] + + bill_request: Optional[CardBillRequestSchema] = None + group: Optional[CardGroupSchema] = None + + +class CardServiceSchema(BaseSchema): + service: ServiceSchema + quantity: int + price: int + employees: List[UserSchema] + is_fixed_price: bool + + +class CardProductServiceSchema(BaseSchema): + service: ServiceSchema + price: int + employees: List[UserSchema] + is_fixed_price: bool + + +class CardProductSchema(BaseSchema): + product: ProductSchema + services: List[CardProductServiceSchema] + quantity: int + comment: str = "" + + +class CardEmployeesSchema(BaseSchema): + user: UserSchema + created_at: datetime + + +class BaseCardSchema(BaseSchema): + id: int + name: str + comment: str + created_at: datetime + status: StatusSchema + board: BoardSchema + status_history: List[CardStatusHistorySchema] + is_deleted: bool + is_completed: bool + + is_locked: bool + services: List[CardServiceSchema] + products: List[CardProductSchema] + client_id: int + client: ClientSchema + shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None + bill_request: Optional[CardBillRequestSchema] = None + group: Optional[CardGroupSchema] = None + manager: Optional[UserSchema] = None + pallets: List[PalletSchema] = [] + boxes: List[BoxSchema] = [] + employees: List[CardEmployeesSchema] = [] + + +class CardSchema(BaseCardSchema): + attributes: list[CardAttributeSchema] + + +class CardGeneralInfoSchema(BaseSchemaWithAttributes): + name: str + is_deleted: bool + is_completed: bool + comment: str + shipping_warehouse: Optional[str] = None + manager: Optional[UserSchema] = None + board_id: int + status_id: int + + +class OptionalShippingWarehouseSchema(BaseShippingWarehouseSchema): + id: Optional[int] = None + + +class ParsedCityBreakdownSchema(BaseSchema): + base_marketplace: BaseMarketplaceSchema + shipping_warehouse: OptionalShippingWarehouseSchema + quantity: int + + +class ParsedProductRowSchema(BaseSchema): + barcode: str + products: list[ProductSchema] + breakdowns: list[ParsedCityBreakdownSchema] + + +class CityBreakdownFromExcelSchema(BaseSchema): + base_marketplace: BaseMarketplaceSchema + shipping_warehouse: OptionalShippingWarehouseSchema + quantity: int + + +class ProductFromExcelSchema(BaseSchema): + product_id: int + cities_breakdown: list[CityBreakdownFromExcelSchema] + + +# endregion Entities + +# region Requests +class CardChangeStatusRequest(BaseSchema): + card_id: int + new_status: int + + +class CardCreateRequest(BaseSchema): + name: str + status_id: int + + +class CardQuickCreateRequest(BaseSchema): + name: constr(strip_whitespace=True) + client_name: constr(strip_whitespace=True) + comment: str + acceptance_date: datetime + shipping_warehouse: constr(strip_whitespace=True) + base_marketplace: BaseMarketplaceSchema + status_id: int + + +class CardSummaryRequest(BaseSchema): + pass + + +class CardAddServicesRequest(BaseSchema): + card_id: int + services: list[CardServiceSchema] + + +class CardUpdateServiceQuantityRequest(BaseSchema): + card_id: int + service_id: int + quantity: int + + +class CardUpdateServiceRequest(BaseSchema): + card_id: int + service: CardServiceSchema + + +class CardAddServiceRequest(BaseSchema): + card_id: int + service_id: int + quantity: int + price: int + + +class CardDeleteServiceRequest(BaseSchema): + card_id: int + service_id: int + + +class CardDeleteServicesRequest(BaseSchema): + card_id: int + service_ids: List[int] + + +class CardUpdateProductQuantityRequest(BaseSchema): + card_id: int + product_id: int + quantity: int + + +class CardAddProductRequest(BaseSchema): + card_id: int + product: CardProductSchema + + +class CardDeleteProductRequest(BaseSchema): + card_id: int + product_id: int + + +class CardDeleteProductsRequest(BaseSchema): + card_id: int + product_ids: List[int] + + +class CardUpdateGeneralInfoRequest(BaseSchema): + card_id: int + data: CardGeneralInfoSchema + + +class CardSummaryReorderRequest(BaseSchema): + card_id: int + status_id: int + index: int + deadline: datetime | None = None + comment: str | None = None + + +class CardDeleteRequest(BaseSchema): + card_id: int + + +class CardUpdateProductRequest(BaseSchema): + card_id: int + product: CardProductSchema + + +class CardServicesCopyRequest(BaseSchema): + card_id: int + source_product_id: int + destination_product_ids: List[int] + + +class CardProductAddKitRequest(BaseSchema): + card_id: int + product_id: int + kit_id: int + + +class CardAddKitRequest(BaseSchema): + card_id: int + kit_id: int + + +class CardCreateGuestUrlRequest(BaseSchema): + card_id: int + + +class CardCompleteRequest(BaseSchema): + card_id: int + + +class CardPrefillRequest(BaseSchema): + old_card_id: int + new_card_id: int + + +class CardRecalculatePriceRequest(BaseSchema): + card_id: int + + +class ManageEmployeeRequest(BaseSchema): + card_id: int + user_id: int + is_assign: bool + + +class CreateCardsFromExcelRequest(BaseSchema): + client_id: int + status_id: int + products: list[ProductFromExcelSchema] + + +# endregion Requests + +# region Responses +class CardUpdateProductQuantityResponse(OkMessageSchema): + pass + + +class CardDeleteServicesResponse(OkMessageSchema): + pass + + +class CardGetAllResponse(BaseSchema): + cards: List[CardSchema] + + +class CardChangeStatusResponse(BaseSchema): + ok: bool + + +class CardCreateResponse(BaseSchema): + ok: bool + + +class CardQuickCreateResponse(BaseSchema): + card_id: int + + +class CardSummaryResponse(BaseSchema): + summaries: List[CardSummary] + + +class CardAddServicesResponse(BaseSchema): + ok: bool + message: str + + +class CardUpdateServiceQuantityResponse(BaseSchema): + ok: bool + message: str + + +class CardUpdateServiceResponse(OkMessageSchema): + pass + + +class CardAddServiceResponse(OkMessageSchema): + pass + + +class CardDeleteServiceResponse(OkMessageSchema): + pass + + +class CardDeleteProductResponse(OkMessageSchema): + pass + + +class CardDeleteProductsResponse(OkMessageSchema): + pass + + +class CardAddProductResponse(OkMessageSchema): + pass + + +class CardUpdateGeneralInfoResponse(OkMessageSchema): + pass + + +class CardSummaryReorderResponse(OkMessageSchema): + pass + + +class CardDeleteResponse(OkMessageSchema): + pass + + +class CardUpdateProductResponse(OkMessageSchema): + pass + + +class CardServicesCopyResponse(OkMessageSchema): + pass + + +class CardProductAddKitResponse(OkMessageSchema): + pass + + +class CardAddKitResponse(OkMessageSchema): + pass + + +class CardCreateGuestUrlResponse(OkMessageSchema): + url: str + + +class CardCompleteResponse(OkMessageSchema): + pass + + +class CardPrefillResponse(OkMessageSchema): + pass + + +class CardRecalculatePriceResponse(OkMessageSchema): + pass + + +class ManageEmployeeResponse(OkMessageSchema): + pass + + +class GetAvailableEmployeesToAssignResponse(BaseSchema): + employees: list[UserSchema] + + +class ParseCardsExcelResponse(BaseSchema): + rows: list[ParsedProductRowSchema] + errors: list[str] + + +class CreateCardsFromExcelResponse(OkMessageSchema): + pass + + +# endregion Responses diff --git a/schemas/deal.py b/schemas/deal.py deleted file mode 100644 index f2df444..0000000 --- a/schemas/deal.py +++ /dev/null @@ -1,420 +0,0 @@ -import datetime -from typing import List, Optional, Union - -from pydantic import constr - -from schemas.base import BaseSchema, OkMessageSchema -from schemas.billing import DealBillRequestSchema -from schemas.board import BoardSchema -from schemas.client import ClientSchema -from schemas.group import DealGroupSchema -from schemas.marketplace import BaseMarketplaceSchema -from schemas.product import ProductSchema -from schemas.service import ServiceSchema -from schemas.shipping import PalletSchema, BoxSchema -from schemas.shipping_warehouse import ShippingWarehouseSchema, BaseShippingWarehouseSchema -from schemas.status import StatusSchema, DealStatusHistorySchema -from schemas.user import UserSchema - - -# region Entities -class FastDeal(BaseSchema): - name: str - client: ClientSchema - comment: str - acceptance_date: datetime.datetime - - -class DealSummary(BaseSchema): - id: int - name: str - client_name: str - created_at: datetime.datetime - status: StatusSchema - board: BoardSchema - total_price: int - rank: int - base_marketplace: Optional[BaseMarketplaceSchema] = None - total_products: int - - shipment_warehouse_id: Optional[int] - shipment_warehouse_name: Optional[str] - - delivery_date: Optional[datetime.datetime] = None - receiving_slot_date: Optional[datetime.datetime] = None - bill_request: Optional[DealBillRequestSchema] = None - group: Optional[DealGroupSchema] = None - - -class DealServiceSchema(BaseSchema): - service: ServiceSchema - quantity: int - price: int - employees: List[UserSchema] - is_fixed_price: bool - - -class DealProductServiceSchema(BaseSchema): - service: ServiceSchema - price: int - employees: List[UserSchema] - is_fixed_price: bool - - -class DealProductSchema(BaseSchema): - product: ProductSchema - services: List[DealProductServiceSchema] - quantity: int - comment: str = "" - - -class DealEmployeesSchema(BaseSchema): - user: UserSchema - created_at: datetime.datetime - - -class DealSchema(BaseSchema): - id: int - name: str - client_id: int - created_at: datetime.datetime - status: StatusSchema - board: BoardSchema - services: List[DealServiceSchema] - products: List[DealProductSchema] - status_history: List[DealStatusHistorySchema] - is_deleted: bool - is_completed: bool - is_locked: bool - is_accounted: bool - client: ClientSchema - comment: str - shipping_warehouse: Optional[Union[ShippingWarehouseSchema, str]] = None - bill_request: Optional[DealBillRequestSchema] = None - group: Optional[DealGroupSchema] = None - manager: Optional[UserSchema] = None - pallets: List[PalletSchema] = [] - boxes: List[BoxSchema] = [] - employees: List[DealEmployeesSchema] = [] - - delivery_date: Optional[datetime.datetime] = None - receiving_slot_date: Optional[datetime.datetime] = None - - -class DealGeneralInfoSchema(BaseSchema): - name: str - is_deleted: bool - is_completed: bool - is_accounted: bool - comment: str - shipping_warehouse: Optional[str] = None - delivery_date: Optional[datetime.datetime] = None - receiving_slot_date: Optional[datetime.datetime] = None - manager: Optional[UserSchema] = None - board: BoardSchema - status: StatusSchema - - -class OptionalShippingWarehouseSchema(BaseShippingWarehouseSchema): - id: Optional[int] = None - - -class ParsedCityBreakdownSchema(BaseSchema): - base_marketplace: BaseMarketplaceSchema - shipping_warehouse: OptionalShippingWarehouseSchema - quantity: int - - -class ParsedProductRowSchema(BaseSchema): - barcode: str - products: list[ProductSchema] - breakdowns: list[ParsedCityBreakdownSchema] - - -class CityBreakdownFromExcelSchema(BaseSchema): - base_marketplace: BaseMarketplaceSchema - shipping_warehouse: OptionalShippingWarehouseSchema - quantity: int - - -class ProductFromExcelSchema(BaseSchema): - product_id: int - cities_breakdown: list[CityBreakdownFromExcelSchema] - - -# endregion Entities - -# region Requests -class DealChangeStatusRequest(BaseSchema): - deal_id: int - new_status: int - - -class DealCreateRequest(BaseSchema): - name: str - status_id: int - - -class DealQuickCreateRequest(BaseSchema): - name: constr(strip_whitespace=True) - client_name: constr(strip_whitespace=True) - comment: str - acceptance_date: datetime.datetime - shipping_warehouse: constr(strip_whitespace=True) - base_marketplace: BaseMarketplaceSchema - status_id: int - - -class DealSummaryRequest(BaseSchema): - pass - - -class DealAddServicesRequest(BaseSchema): - deal_id: int - services: list[DealServiceSchema] - - -class DealUpdateServiceQuantityRequest(BaseSchema): - deal_id: int - service_id: int - quantity: int - - -class DealUpdateServiceRequest(BaseSchema): - deal_id: int - service: DealServiceSchema - - -class DealAddServiceRequest(BaseSchema): - deal_id: int - service_id: int - quantity: int - price: int - - -class DealDeleteServiceRequest(BaseSchema): - deal_id: int - service_id: int - - -class DealDeleteServicesRequest(BaseSchema): - deal_id: int - service_ids: List[int] - - -class DealUpdateProductQuantityRequest(BaseSchema): - deal_id: int - product_id: int - quantity: int - - -class DealAddProductRequest(BaseSchema): - deal_id: int - product: DealProductSchema - - -class DealDeleteProductRequest(BaseSchema): - deal_id: int - product_id: int - - -class DealDeleteProductsRequest(BaseSchema): - deal_id: int - product_ids: List[int] - - -class DealUpdateGeneralInfoRequest(BaseSchema): - deal_id: int - data: DealGeneralInfoSchema - - -class DealSummaryReorderRequest(BaseSchema): - deal_id: int - status_id: int - index: int - deadline: datetime.datetime | None = None - comment: str | None = None - - -class DealDeleteRequest(BaseSchema): - deal_id: int - - -class DealUpdateProductRequest(BaseSchema): - deal_id: int - product: DealProductSchema - - -class DealServicesCopyRequest(BaseSchema): - deal_id: int - source_product_id: int - destination_product_ids: List[int] - - -class DealProductAddKitRequest(BaseSchema): - deal_id: int - product_id: int - kit_id: int - - -class DealAddKitRequest(BaseSchema): - deal_id: int - kit_id: int - - -class DealCreateGuestUrlRequest(BaseSchema): - deal_id: int - - -class DealCompleteRequest(BaseSchema): - deal_id: int - - -class DealPrefillRequest(BaseSchema): - old_deal_id: int - new_deal_id: int - - -class DealRecalculatePriceRequest(BaseSchema): - deal_id: int - - -class ManageEmployeeRequest(BaseSchema): - deal_id: int - user_id: int - is_assign: bool - - -class CreateDealsFromExcelRequest(BaseSchema): - client_id: int - status_id: int - products: list[ProductFromExcelSchema] - - -# endregion Requests - -# region Responses -class DealUpdateProductQuantityResponse(OkMessageSchema): - pass - - -class DealDeleteServicesResponse(OkMessageSchema): - pass - - -class DealGetAllResponse(BaseSchema): - deals: List[DealSchema] - - -class DealChangeStatusResponse(BaseSchema): - ok: bool - - -class DealCreateResponse(BaseSchema): - ok: bool - - -class DealQuickCreateResponse(BaseSchema): - deal_id: int - - -class DealSummaryResponse(BaseSchema): - summaries: List[DealSummary] - - -class DealAddServicesResponse(BaseSchema): - ok: bool - message: str - - -class DealUpdateServiceQuantityResponse(BaseSchema): - ok: bool - message: str - - -class DealUpdateServiceResponse(OkMessageSchema): - pass - - -class DealAddServiceResponse(OkMessageSchema): - pass - - -class DealDeleteServiceResponse(OkMessageSchema): - pass - - -class DealDeleteProductResponse(OkMessageSchema): - pass - - -class DealDeleteProductsResponse(OkMessageSchema): - pass - - -class DealAddProductResponse(OkMessageSchema): - pass - - -class DealUpdateGeneralInfoResponse(OkMessageSchema): - pass - - -class DealSummaryReorderResponse(OkMessageSchema): - pass - - -class DealDeleteResponse(OkMessageSchema): - pass - - -class DealUpdateProductResponse(OkMessageSchema): - pass - - -class DealServicesCopyResponse(OkMessageSchema): - pass - - -class DealProductAddKitResponse(OkMessageSchema): - pass - - -class DealAddKitResponse(OkMessageSchema): - pass - - -class DealCreateGuestUrlResponse(OkMessageSchema): - url: str - - -class DealCompleteResponse(OkMessageSchema): - pass - - -class DealPrefillResponse(OkMessageSchema): - pass - - -class DealRecalculatePriceResponse(OkMessageSchema): - pass - - -class ManageEmployeeResponse(OkMessageSchema): - pass - - -class GetAvailableEmployeesToAssignResponse(BaseSchema): - employees: list[UserSchema] - - -class ParseDealsExcelResponse(BaseSchema): - rows: list[ParsedProductRowSchema] - errors: list[str] - - -class CreateDealsFromExcelResponse(OkMessageSchema): - pass - - -# endregion Responses diff --git a/schemas/group.py b/schemas/group.py index b2102e3..56519db 100644 --- a/schemas/group.py +++ b/schemas/group.py @@ -6,7 +6,7 @@ from schemas.billing import GroupBillRequestSchema # region Entities -class DealGroupSchema(BaseSchema): +class CardGroupSchema(BaseSchema): id: int name: Optional[str] = None lexorank: str @@ -17,50 +17,50 @@ class DealGroupSchema(BaseSchema): # region Requests -class DealGroupUpdateRequest(BaseSchema): - data: DealGroupSchema +class CardGroupUpdateRequest(BaseSchema): + data: CardGroupSchema -class DealCreateGroupRequest(BaseSchema): - dragging_deal_id: int - hovered_deal_id: int +class CreateCardGroupRequest(BaseSchema): + dragging_card_id: int + hovered_card_id: int -class DealGroupChangeStatusRequest(BaseSchema): +class CardGroupChangeStatusRequest(BaseSchema): group_id: int new_status: int -class DealAddToGroupRequest(BaseSchema): - deal_id: int +class CardAddToGroupRequest(BaseSchema): + card_id: int group_id: int -class DealRemoveFromGroupRequest(BaseSchema): - deal_id: int +class CardRemoveFromGroupRequest(BaseSchema): + card_id: int # endregion # region Responses -class DealCreateGroupResponse(OkMessageSchema): +class CardCreateGroupResponse(OkMessageSchema): pass -class DealGroupUpdateResponse(OkMessageSchema): +class CardGroupUpdateResponse(OkMessageSchema): pass -class DealGroupChangeStatusResponse(OkMessageSchema): +class CardGroupChangeStatusResponse(OkMessageSchema): pass -class DealAddToGroupResponse(OkMessageSchema): +class CardAddToGroupResponse(OkMessageSchema): pass -class DealRemoveFromGroupResponse(OkMessageSchema): +class CardRemoveFromGroupResponse(OkMessageSchema): pass # endregion diff --git a/schemas/module.py b/schemas/module.py new file mode 100644 index 0000000..1de42a6 --- /dev/null +++ b/schemas/module.py @@ -0,0 +1,11 @@ +from schemas.base import BaseSchema + + +# region Entities + +class ModuleSchema(BaseSchema): + id: int + key: str + is_deleted: bool + +# endregion diff --git a/schemas/project.py b/schemas/project.py index c6c5199..e0dc51c 100644 --- a/schemas/project.py +++ b/schemas/project.py @@ -1,4 +1,6 @@ +from schemas.attribute import AttributeSchema from schemas.base import BaseSchema, OkMessageSchema +from schemas.module import ModuleSchema # region Entities @@ -10,9 +12,11 @@ class BaseProjectSchema(BaseSchema): class ProjectSchema(BaseProjectSchema): id: int + attributes: list[AttributeSchema] + modules: list[ModuleSchema] -class ProjectSchemaWithCount(ProjectSchema): +class FullProjectSchema(ProjectSchema): boards_count: int @@ -27,13 +31,14 @@ class CreateProjectRequest(BaseSchema): class UpdateProjectRequest(BaseSchema): project: ProjectSchema + # endregion # region Responses class GetProjectsResponse(BaseSchema): - projects: list[ProjectSchemaWithCount] + projects: list[FullProjectSchema] class CreateProjectResponse(OkMessageSchema): diff --git a/schemas/service.py b/schemas/service.py index 9afda0f..5e1d07e 100644 --- a/schemas/service.py +++ b/schemas/service.py @@ -16,7 +16,7 @@ class ServicePriceRangeSchema(BaseSchema): class ServiceCategorySchema(BaseSchema): id: int name: str - deal_service_rank: str + card_service_rank: str product_service_rank: str diff --git a/schemas/shipping.py b/schemas/shipping.py index b0acb8f..a0e8f50 100644 --- a/schemas/shipping.py +++ b/schemas/shipping.py @@ -16,7 +16,7 @@ class BoxSchema(BaseSchema): quantity: int product: Optional[ProductSchema] pallet_id: Optional[int] - deal_id: Optional[int] + card_id: Optional[int] class ShippingProductSchema(BaseSchema): @@ -44,8 +44,8 @@ class CreateBoxInPalletSchema(BaseSchema): pallet_id: Optional[int] -class CreateBoxInDealSchema(BaseSchema): - deal_id: Optional[int] +class CreateBoxInCardSchema(BaseSchema): + card_id: Optional[int] class UpdateBoxSchema(ProductAndQuantitySchema): @@ -61,7 +61,7 @@ class UpdateShippingProductRequest(BaseSchema): class UpdateBoxRequest(BaseSchema): - data: CreateBoxInDealSchema | CreateBoxInPalletSchema | UpdateBoxSchema + data: CreateBoxInCardSchema | CreateBoxInPalletSchema | UpdateBoxSchema # endregion diff --git a/schemas/statistics.py b/schemas/statistics.py index ff4587f..65e4c65 100644 --- a/schemas/statistics.py +++ b/schemas/statistics.py @@ -11,7 +11,7 @@ class ProfitChartDataItem(BaseSchema): revenue: float profit: float expenses: float - deals_count: int + cards_count: int class ProfitTableDataItem(BaseSchema): @@ -19,7 +19,7 @@ class ProfitTableDataItem(BaseSchema): revenue: float profit: float expenses: Optional[float] = 0 - deals_count: int + cards_count: int # endregion @@ -31,7 +31,7 @@ class CommonProfitFilters(BaseSchema): base_marketplace_key: str project_id: int board_id: int - deal_status_id: int + card_status_id: int manager_id: int expense_tag_id: int income_tag_id: int diff --git a/schemas/status.py b/schemas/status.py index 3369e7c..0bbe5fc 100644 --- a/schemas/status.py +++ b/schemas/status.py @@ -17,7 +17,7 @@ class StatusSchema(BaseStatusSchema): is_deleted: bool = False -class DealStatusHistorySchema(BaseSchema): +class CardStatusHistorySchema(BaseSchema): user: UserSchema changed_at: datetime from_status: StatusSchema diff --git a/services/barcode.py b/services/barcode.py index e7253d5..82a913d 100644 --- a/services/barcode.py +++ b/services/barcode.py @@ -8,7 +8,7 @@ from barcodes.attributes import AttributeWriterFactory from barcodes.generator.default_generator import DefaultBarcodeGenerator from barcodes.images_uploader import BarcodeImagesUploader from models import BarcodeTemplate, BarcodeTemplateAttribute, barcode_template_attribute_link, Product, \ - BarcodeTemplateAdditionalField, BarcodeTemplateSize, Deal, DealProduct + BarcodeTemplateAdditionalField, BarcodeTemplateSize, Card, CardProduct from schemas.barcode import * from services.base import BaseService @@ -113,44 +113,44 @@ class BarcodeService(BaseService): ) return filename, pdf_buffer - async def get_deal_barcodes_pdf(self, request: GetDealProductsBarcodesPdfRequest) -> Tuple[str, BytesIO]: + async def get_card_barcodes_pdf(self, request: GetCardProductsBarcodesPdfRequest) -> Tuple[str, BytesIO]: stmt = ( - select(Deal) + select(Card) .options( - selectinload(Deal.products).joinedload(DealProduct.product).selectinload(Product.client), - selectinload(Deal.products).joinedload(DealProduct.product).joinedload(Product.barcodes), + selectinload(Card.products).joinedload(CardProduct.product).selectinload(Product.client), + selectinload(Card.products).joinedload(CardProduct.product).joinedload(Product.barcodes), ) - .filter(Deal.id == request.deal_id) + .filter(Card.id == request.card_id) ) query = await self.session.execute(stmt) - deal: Deal = query.scalar() - if not deal: + card: Card = query.scalar() + if not card: raise ValueError('Сделка не найдена') uploader = BarcodeImagesUploader() barcodes_data: List[Dict[str, str | Product | BarcodeTemplate | int]] = [] - for deal_product in deal.products: - if deal_product.product.barcode_image: + for card_product in card.products: + if card_product.product.barcode_image: barcodes_data.append({ - "barcode_image_url": uploader.get_abs_path(deal_product.product.barcode_image.filename), - "num_duplicates": deal_product.quantity + "barcode_image_url": uploader.get_abs_path(card_product.product.barcode_image.filename), + "num_duplicates": card_product.quantity }) else: product_request = GetProductBarcodeRequest( - product_id=deal_product.product_id, + product_id=card_product.product_id, barcode="", - barcode_template_id=deal_product.product.barcode_template_id, + barcode_template_id=card_product.product.barcode_template_id, ) - barcode_template = await self._get_barcode_template(product_request, deal_product.product) + barcode_template = await self._get_barcode_template(product_request, card_product.product) barcodes_data.append({ - "barcode": deal_product.product.barcodes[0].barcode, - "product": deal_product.product, + "barcode": card_product.product.barcodes[0].barcode, + "product": card_product.product, "template": barcode_template, - "num_duplicates": deal_product.quantity + "num_duplicates": card_product.quantity }) default_generator = DefaultBarcodeGenerator() - filename = f'{deal.id}_deal_barcodes.pdf' + filename = f'{card.id}_deal_barcodes.pdf' pdf_buffer = default_generator.generate(barcodes_data) return filename, pdf_buffer diff --git a/services/billing.py b/services/billing.py index d5d0a2e..d36b016 100644 --- a/services/billing.py +++ b/services/billing.py @@ -1,4 +1,3 @@ -import logging from io import BytesIO from typing import List from uuid import uuid4 @@ -16,24 +15,24 @@ from constants import MONTHS, ENV from external.billing import BillingClient, CreateBillingRequestValue, CreateBillRequestSchema, CreateBillRequestItems, \ BillStatusUpdateRequest, NotificationChannel, NotifyReceivedBillRequestSchema, DeleteBillRequestSchema, \ ProductBillingDocumentPdf, ServiceBillingDocumentPdf -from models import DealBillRequest, Deal, DealProduct, DealService as DealServiceModel, DealProductService, DealGroup, \ +from models import CardBillRequest, Card, CardProduct, CardService as CardServiceModel, CardGroup, \ GroupBillRequest from schemas.billing import * from services.base import BaseService -from services.deal import DealService +from services.card import CardsService from utils.list_utils import to_locale_number class BillingService(BaseService): - async def _process_deal_update_details( + async def _process_card_update_details( self, request: BillStatusUpdateRequest, ): - deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) - if not deal_bill_request: + bill_request = await self._get_card_bill_by_id(request.listener_transaction_id) + if not bill_request: return - deal_bill_request.pdf_url = request.info.pdf_url - deal_bill_request.invoice_number = request.info.invoice_number + bill_request.pdf_url = request.info.pdf_url + bill_request.invoice_number = request.info.invoice_number async def _process_group_update_details( self, @@ -63,19 +62,19 @@ class BillingService(BaseService): if not response.ok: return if type(request.listener_transaction_id) is int: - await self._process_deal_update_details(request) + await self._process_card_update_details(request) else: await self._process_group_update_details(request) await self.session.commit() - async def _process_deal_update_verification( + async def _process_card_update_verification( self, request: BillStatusUpdateRequest ): - deal_bill_request = await self._get_deal_bill_by_id(request.listener_transaction_id) - if not deal_bill_request: + card_bill_request = await self._get_card_bill_by_id(request.listener_transaction_id) + if not card_bill_request: return - deal_bill_request.paid = request.info.payed + card_bill_request.paid = request.info.payed async def _process_group_update_verification( self, @@ -104,7 +103,7 @@ class BillingService(BaseService): if not response.ok: return if type(request.listener_transaction_id) is int: - await self._process_deal_update_verification(request) + await self._process_card_update_verification(request) else: await self._process_group_update_verification(request) await self.session.commit() @@ -118,37 +117,37 @@ class BillingService(BaseService): elif request.channel == NotificationChannel.PAYMENT_VERIFICATION: await self._process_update_verification(request) - async def create_deal_bill_request(self, deal: Deal): - deal_bill_request = DealBillRequest( - deal_id=deal.id, + async def create_card_bill_request(self, card: Card): + card_bill_request = CardBillRequest( + card_id=card.id, created_at=datetime.datetime.now() ) - self.session.add(deal_bill_request) - deal.is_locked = True + self.session.add(card_bill_request) + card.is_locked = True await self.session.commit() - async def create_group_bill_request(self, group: DealGroup): + async def create_group_bill_request(self, group: CardGroup): group_bill_request = GroupBillRequest( group_id=group.id, created_at=datetime.datetime.now() ) self.session.add(group_bill_request) - for deal in group.deals: - deal.is_locked = True + for card in group.cards: + card.is_locked = True await self.session.commit() - async def create_deal_billing(self, user, request: CreateDealBillRequest) -> CreateDealBillResponse: + async def create_card_billing(self, user, request: CreateCardBillRequest) -> CreateCardBillResponse: try: - deal_service = DealService(self.session) + card_service = CardsService(self.session) billing_client = BillingClient(backend.config.BILLING_API_KEY) - basic_deal: Deal = await deal_service.get_by_id(user, request.deal_id, return_raw=True) - if basic_deal.group: - deals = await self._get_deals_by_group_id(basic_deal.group.id) + basic_card: Card = await card_service.get_by_id(user, request.card_id, return_raw=True) + if basic_card.group: + cards = await self._get_cards_by_group_id(basic_card.group.id) else: - deals = [basic_deal] + cards = [basic_card] - (services, products, is_size_needed) = await self._get_products_for_deal(deals) + (services, products, is_size_needed) = await self._get_products_for_card(cards) services: dict[str, ServiceBillingDocumentPdf] products: dict[str, ProductBillingDocumentPdf] is_size_needed: bool @@ -163,72 +162,72 @@ class BillingService(BaseService): ) ) - deal = basic_deal - listener_transaction_id = deal.id - if deal.group: - listener_transaction_id = f"group-{basic_deal.group.id}" - inn: str = deal.client.details.inn + card = basic_card + listener_transaction_id = card.id + if card.group: + listener_transaction_id = f"group-{basic_card.group.id}" + inn: str = card.client.details.inn create_bill_request = CreateBillRequestSchema( listener_transaction_id=listener_transaction_id, - payer_name=deal.client.name, + payer_name=card.client.name, payer_inn=inn.strip(), - payer_phone=deal.client.details.phone_number, + payer_phone=card.client.details.phone_number, items=CreateBillRequestItems( values=billing_request_values ) ) create_bill_response = await billing_client.create(create_bill_request) if not create_bill_response.ok: - return CreateDealBillResponse(ok=create_bill_response.ok, message='Ошибка!') + return CreateCardBillResponse(ok=create_bill_response.ok, message='Ошибка!') - if basic_deal.group: - await self.create_group_bill_request(basic_deal.group) + if basic_card.group: + await self.create_group_bill_request(basic_card.group) else: - await self.create_deal_bill_request(basic_deal) + await self.create_card_bill_request(basic_card) - return CreateDealBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!') + return CreateCardBillResponse(ok=create_bill_response.ok, message='Заявка успешно создана!') except Exception as e: - return CreateDealBillResponse(ok=False, message=str(e)) + return CreateCardBillResponse(ok=False, message=str(e)) - async def _get_deal_bill_by_id(self, deal_id: int) -> Optional[DealBillRequest]: - return await self.session.scalar(select(DealBillRequest).where(DealBillRequest.deal_id == deal_id)) + async def _get_card_bill_by_id(self, card_id: int) -> Optional[CardBillRequest]: + return await self.session.scalar(select(CardBillRequest).where(CardBillRequest.card_id == card_id)) async def _get_group_bill_by_id(self, group_id: int) -> Optional[GroupBillRequest]: return await self.session.scalar(select(GroupBillRequest).where(GroupBillRequest.group_id == group_id)) - async def get_deal_bill_by_id(self, deal_id: int) -> GetDealBillById: - deal_bill = await self._get_deal_bill_by_id(deal_id) - if not deal_bill: - raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Deal bill was not found') - return GetDealBillById(deal_bill=DealBillRequestSchema.model_validate(deal_bill)) + async def get_card_bill_by_id(self, card_id: int) -> GetCardBillById: + bill = await self._get_card_bill_by_id(card_id) + if not bill: + raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail='Card bill was not found') + return GetCardBillById(card_bill=CardBillRequestSchema.model_validate(bill)) - async def cancel_deal_billing(self, user, request: CancelDealBillRequest) -> CancelDealBillResponse: + async def cancel_card_billing(self, user, request: CancelCardBillRequest) -> CancelCardBillResponse: try: - deal = await self._get_deal_by_id(request.deal_id) - if not deal: - return CancelDealBillResponse(ok=False, message='Сделка не найдена') + card = await self._get_card_by_id(request.card_id) + if not card: + return CancelCardBillResponse(ok=False, message='Сделка не найдена') - if deal.group: - bill = await self._get_group_bill_by_id(deal.group.id) + if card.group: + bill = await self._get_group_bill_by_id(card.group.id) if not bill: - return CancelDealBillResponse(ok=False, message='Заявка не найдена') + return CancelCardBillResponse(ok=False, message='Заявка не найдена') billing_client = BillingClient(backend.config.BILLING_API_KEY) - response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=deal.group.id)) + response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=card.group.id)) else: - bill = await self._get_deal_bill_by_id(request.deal_id) + bill = await self._get_card_bill_by_id(request.card_id) if not bill: - return CancelDealBillResponse(ok=False, message='Заявка не найдена') + return CancelCardBillResponse(ok=False, message='Заявка не найдена') billing_client = BillingClient(backend.config.BILLING_API_KEY) - response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=request.deal_id)) + response = await billing_client.delete(DeleteBillRequestSchema(listener_transaction_id=request.card_id)) if not response.ok: - return CancelDealBillResponse(ok=False, message='Ошибка') + return CancelCardBillResponse(ok=False, message='Ошибка') await self.session.delete(bill) await self.session.commit() - return CancelDealBillResponse(ok=True, message='Заявка успешно отозвана') + return CancelCardBillResponse(ok=True, message='Заявка успешно отозвана') except Exception as e: - return CancelDealBillResponse(ok=False, message=str(e)) + return CancelCardBillResponse(ok=False, message=str(e)) def _gen_key_for_service(self, service: ServiceBillingDocumentPdf) -> str: return f"{service.name}-{service.price}" @@ -237,13 +236,13 @@ class BillingService(BaseService): article = product.article if product.article else uuid4() return f"{article}-{product.size}-{product.price}" - async def _get_products_for_deal(self, deals: list[Deal]) -> tuple[dict, dict, bool]: + async def _get_products_for_card(self, cards: list[Card]) -> tuple[dict, dict, bool]: services: dict[str, ServiceBillingDocumentPdf] = {} products: dict[str, ProductBillingDocumentPdf] = {} is_size_needed: bool = False - for deal in deals: - for product in deal.products: + for card in cards: + for product in card.products: product_price = 0 for service in product.services: service_data = ServiceBillingDocumentPdf( @@ -269,7 +268,7 @@ class BillingService(BaseService): products[product_key].quantity += product_data.quantity else: products[product_key] = product_data - for service in deal.services: + for service in card.services: service_data = ServiceBillingDocumentPdf( name=service.service.name, price=service.price, @@ -283,49 +282,49 @@ class BillingService(BaseService): return services, products, is_size_needed - async def _get_deal_by_id(self, deal_id: int) -> Optional[Deal]: - deal: Deal | None = await self.session.scalar( - select(Deal) - .where(Deal.id == deal_id) + async def _get_card_by_id(self, card_id: int) -> Optional[Card]: + card: Card | None = await self.session.scalar( + select(Card) + .where(Card.id == card_id) .options( - selectinload(Deal.products).selectinload(DealProduct.services), - selectinload(Deal.services).selectinload(DealServiceModel.service), - joinedload(Deal.shipping_warehouse), - joinedload(Deal.client), - selectinload(Deal.group).selectinload(DealGroup.deals), + selectinload(Card.products).selectinload(CardProduct.services), + selectinload(Card.services).selectinload(CardServiceModel.service), + joinedload(Card.shipping_warehouse), + joinedload(Card.client), + selectinload(Card.group).selectinload(CardGroup.cards), ) ) - return deal + return card - async def _get_deals_by_group_id(self, group_id: int) -> List[Deal]: - group: DealGroup | None = await self.session.scalar( - select(DealGroup) - .where(DealGroup.id == group_id) + async def _get_cards_by_group_id(self, group_id: int) -> List[Card]: + group: CardGroup | None = await self.session.scalar( + select(CardGroup) + .where(CardGroup.id == group_id) .options( - selectinload(DealGroup.deals).selectinload(Deal.products).selectinload(DealProduct.services), - selectinload(DealGroup.deals).selectinload(Deal.services).selectinload(DealServiceModel.service), - selectinload(DealGroup.deals).joinedload(Deal.shipping_warehouse), - selectinload(DealGroup.deals).joinedload(Deal.client), - selectinload(DealGroup.deals).selectinload(Deal.group).selectinload(DealGroup.deals), + selectinload(CardGroup.cards).selectinload(Card.products).selectinload(CardProduct.services), + selectinload(CardGroup.cards).selectinload(Card.services).selectinload(CardServiceModel.service), + selectinload(CardGroup.cards).joinedload(Card.shipping_warehouse), + selectinload(CardGroup.cards).joinedload(Card.client), + selectinload(CardGroup.cards).selectinload(Card.group).selectinload(CardGroup.cards), ) ) - return group.deals if group else [] + return group.cards if group else [] - async def _create_billing_document_html(self, deal_id: int): - deal = await self._get_deal_by_id(deal_id) - if not deal: + async def _create_billing_document_html(self, card_id: int): + card = await self._get_card_by_id(card_id) + if not card: return "" - if deal.group: - deals = await self._get_deals_by_group_id(deal.group.id) + if card.group: + cards = await self._get_cards_by_group_id(card.group.id) else: - deals = [deal] + cards = [card] - (services, products, is_size_needed) = await self._get_products_for_deal(deals) + (services, products, is_size_needed) = await self._get_products_for_card(cards) - deal_price = sum((service.price * service.quantity for service in services.values())) - deal_price_words = get_string_by_number(deal_price)[0:-10] - deal_price = to_locale_number(deal_price) + price = sum((service.price * service.quantity for service in services.values())) + price_words = get_string_by_number(price)[0:-10] + price = to_locale_number(price) template = ENV.get_template("bill-of-payment.html") now = datetime.datetime.now() @@ -334,14 +333,14 @@ class BillingService(BaseService): "products": products, "services": services, "is_size_needed": is_size_needed, - "deal_price": deal_price, - "deal_price_words": deal_price_words, - "deal": deal, + "deal_price": price, + "deal_price_words": price_words, + "deal": card, "curr_date": curr_date }) - async def create_billing_document_pdf(self, deal_id) -> BytesIO: - doc = await self._create_billing_document_html(deal_id) + async def create_billing_document_pdf(self, card_id) -> BytesIO: + doc = await self._create_billing_document_html(card_id) pdf_file = BytesIO() HTML(string=doc).write_pdf(pdf_file, stylesheets=[CSS(constants.APP_PATH + '/static/css/bill-of-payment.css')]) diff --git a/services/board.py b/services/board.py index 26aad60..f8d9e84 100644 --- a/services/board.py +++ b/services/board.py @@ -3,7 +3,7 @@ from typing import Optional from sqlalchemy import select, and_, func -from models import Board, Deal +from models import Board, Card from schemas.board import * from services.base import BaseService @@ -76,12 +76,12 @@ class BoardService(BaseService): async def _count_deals_in_progress(self, board_id: int) -> int: stmt = ( - select(func.count(Deal.id)) + select(func.count(Card.id)) .where( and_( - Deal.board_id == board_id, - Deal.is_deleted == False, - Deal.is_completed == False, + Card.board_id == board_id, + Card.is_deleted == False, + Card.is_completed == False, ) ) ) @@ -89,8 +89,8 @@ class BoardService(BaseService): async def _count_deals(self, board_id: int) -> int: stmt = ( - select(func.count(Deal.id)) - .where(Deal.board_id == board_id) + select(func.count(Card.id)) + .where(Card.board_id == board_id) ) return (await self.session.scalars(stmt)).first() @@ -111,7 +111,7 @@ class BoardService(BaseService): await self.session.delete(board) else: board.is_deleted = True - for status in board.deal_statuses: + for status in board.statuses: status.is_deleted = True await self.session.commit() diff --git a/services/card.py b/services/card.py new file mode 100644 index 0000000..34da0da --- /dev/null +++ b/services/card.py @@ -0,0 +1,1317 @@ +from collections import defaultdict +from typing import Union + +import lexorank +from fastapi import HTTPException +from sqlalchemy import select, func, update, delete, insert, and_ +from sqlalchemy.orm import joinedload, selectinload +from starlette import status + +from card_attributes import CardAttributesCommandHandler +from card_attributes.exceptions import CardAttributeException +from models import * +from schemas.card import * +from schemas.client import ClientDetailsSchema +from services import card_group +from services.auth import AuthService +from services.base import BaseService +from services.client import ClientService +from services.service import ServiceService +from services.shipping_warehouse import ShippingWarehouseService + + +class CardsService(BaseService): + + # region Card + + @staticmethod + def grant_access(user: Union[User, dict], card_id: int): + if type(user) is User: + return + user_card_id = user['deal_id'] + if int(user_card_id) != int(card_id): + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') + + async def _get_card_by_id(self, card_id) -> Union[Card, None]: + return await self.session.get(Card, card_id) + + async def _get_rank_for_card(self, status_id: int) -> str: + stmt = await self.session.execute( + select(Card) + .where(Card.current_status_id == status_id) + .order_by(Card.lexorank.desc()) + .limit(1) + ) + card = stmt.scalar_one_or_none() + if not card: + prev = lexorank.middle(lexorank.Bucket.BUCEKT_0) + return str(prev.next()) + return str(lexorank.parse(card.lexorank).next()) + + async def change_status(self, card: Card, + status_id: int, + user: User, + deadline: datetime = None, + rank=None, + comment: str = ''): + if not card.current_status_id == status_id: + deadline = deadline + status_change = CardStatusHistory( + card_id=card.id, + user_id=user.id, + changed_at=datetime.now(), + from_status_id=card.current_status_id, + to_status_id=status_id, + next_status_deadline=deadline, + comment=comment + ) + self.session.add(status_change) + card.current_status_id = status_id + if not rank: + rank = await self._get_rank_for_card(status_id) + if rank: + card.lexorank = rank + await self.session.flush() + + async def delete(self, request: CardDeleteRequest) -> CardDeleteResponse: + card = await self._get_card_by_id(request.card_id) + if not card: + return CardDeleteResponse(ok=False, message="Карточка не найдена") + if card.group: + await card_group.CardGroupService(self.session).delete_group(card.group.id) + else: + card.is_deleted = True + await self.session.commit() + return CardDeleteResponse(ok=True, message="Карточка успешно удалена") + + async def quick_create(self, request: CardQuickCreateRequest, user: User) -> CardQuickCreateResponse: + card_status = await self.session.get(CardStatus, request.status_id) + if not card_status: + raise HTTPException(status_code=400, detail="Указан некорректный статус") + + client_service = ClientService(self.session) + client = await client_service.get_by_name(request.client_name) + + if not client: + client = await client_service.create_client_raw( + user, + request.client_name, + ClientDetailsSchema() + ) + + shipping_warehouse_service = ShippingWarehouseService(self.session) + shipping_warehouse = await shipping_warehouse_service.get_by_name(name=request.shipping_warehouse) + if not shipping_warehouse: + shipping_warehouse = await shipping_warehouse_service.create_by_name(name=request.shipping_warehouse) + + rank = await self._get_rank_for_card(request.status_id) + card = Card( + name=request.name, + created_at=datetime.now(), + client_id=client.id, + current_status_id=request.status_id, + board_id=card_status.board_id, + lexorank=rank, + shipping_warehouse_id=shipping_warehouse.id, + base_marketplace_key=request.base_marketplace.key + ) + self.session.add(card) + await self.session.flush() + + try: + card_attrs_handler = CardAttributesCommandHandler(self.session) + await card_attrs_handler.set_attributes_after_creation(card) + await self.session.commit() + except CardAttributeException: + await self.session.rollback() + + return CardQuickCreateResponse(card_id=card.id) + + async def change_status_manual(self, request: CardChangeStatusRequest, + user: User) -> CardChangeStatusResponse: + card = await self._get_card_by_id(request.card_id) + if not card: + return CardChangeStatusResponse(ok=False) + await self.change_status(card, request.new_status, user) + await self.session.commit() + return CardChangeStatusResponse(ok=True) + + def _get_price_subquery(self): + card_services_subquery = ( + select( + CardService.card_id, + func.sum(CardService.quantity * CardService.price).label( + 'total_price') + ) + .join(Service) + .group_by(CardService.card_id) + ) + product_services_subquery = select( + select( + CardProductService.card_id, + func.sum(CardProduct.quantity * CardProductService.price).label('total_price') + ) + .join(CardProduct) + .group_by(CardProductService.card_id) + .subquery() + ) + union_subqueries = card_services_subquery.union_all(product_services_subquery).subquery() + final_subquery = ( + select( + union_subqueries.c.card_id, + func.sum(union_subqueries.c.total_price).label('total_price') + ) + .group_by(union_subqueries.c.card_id) + .subquery() + ) + return final_subquery + + def _get_products_quantity_subquery(self): + return ( + select( + CardProduct.card_id, + func.sum(CardProduct.quantity).label('total_quantity') + ) + .group_by(CardProduct.card_id) + .subquery() + ) + + async def get_summary(self, full: bool = False) -> CardSummaryResponse: + price_subquery = self._get_price_subquery() + products_quantity_subquery = self._get_products_quantity_subquery() + q = ( + select( + Card, + func.coalesce(price_subquery.c.total_price, 0), + func.row_number().over( + partition_by=Card.current_status_id, + order_by=Card.lexorank + ).label('rank'), + func.coalesce(products_quantity_subquery.c.total_quantity, 0) + ) + .options( + selectinload(Card.status_history), + joinedload(Card.client), + joinedload(Card.shipping_warehouse), + joinedload(Card.bill_request), + joinedload(Card.status), + joinedload(Card.board), + ) + .outerjoin( + price_subquery, Card.id == price_subquery.c.card_id, + ) + .outerjoin( + products_quantity_subquery, Card.id == products_quantity_subquery.c.card_id + ) + .where( + Card.is_deleted == False, + ) + ) + if not full: + q = q.where( + Card.is_completed == False, + # Card.current_status != CardStatus.COMPLETED + ) + else: + q = q.order_by(Card.created_at.desc()) + cards_query = await self.session.execute(q) + summaries = [] + for card, total_price, rank, products_count in cards_query.all(): + card: Card + base_marketplace = None + if card.base_marketplace: + base_marketplace = BaseMarketplaceSchema.model_validate(card.base_marketplace) + shipment_warehouse_name = card.shipping_warehouse.name if card.shipping_warehouse else None + summaries.append( + CardSummary( + id=card.id, + client_name=card.client.name, + name=card.name, + status=card.status, + board=card.board, + total_price=total_price, + rank=rank, + base_marketplace=base_marketplace, + created_at=card.created_at, + shipment_warehouse_id=card.shipping_warehouse_id, + shipment_warehouse_name=shipment_warehouse_name, + total_products=products_count, + bill_request=card.bill_request, + group=card.group + ) + ) + return CardSummaryResponse(summaries=summaries) + + async def get_all(self) -> CardGetAllResponse: + cards_stmt = ( + await self.session.scalars( + select(Card) + .options( + joinedload(Card.shipping_warehouse), + joinedload(Card.client) + .joinedload(Client.details), + selectinload(Card.services) + .options( + joinedload(CardService.service).joinedload(Service.category), + selectinload(CardService.employees) + ), + selectinload(Card.products) + .joinedload(CardProduct.product) + .joinedload(Product.client), + selectinload(Card.products) + .joinedload(CardProduct.product) + .joinedload(Product.barcodes), + selectinload(Card.products) + .joinedload(CardProduct.services) + .options( + joinedload(CardProductService.service), + selectinload(CardProductService.employees) + ), + selectinload(Card.status_history) + .joinedload(CardStatusHistory.user), + selectinload(Card.status_history) + .noload(CardStatusHistory.card), + + ) + ) + ) + cards = cards_stmt.all() + result = [] + for card in cards: + result.append(CardSchema.model_validate(card)) + return CardGetAllResponse(cards=result) + + async def get_by_id(self, user: Union[User, dict], card_id: int, return_raw=False) -> Union[ + CardSchema, Card]: + self.grant_access(user, card_id) + + card: Optional[Card] = await self.session.scalar( + select(Card) + .options( + joinedload(Card.status), + joinedload(Card.board) + .joinedload(Board.project), + joinedload(Card.shipping_warehouse), + joinedload(Card.client) + .joinedload(Client.details), + selectinload(Card.services) + .options( + joinedload(CardService.service).joinedload(Service.category), + selectinload(CardService.employees) + ), + selectinload(Card.products) + .joinedload(CardProduct.product) + .joinedload(Product.client), + selectinload(Card.products) + .joinedload(CardProduct.product) + .joinedload(Product.barcodes), + selectinload(Card.products) + .joinedload(CardProduct.services) + .options( + joinedload(CardProductService.service), + selectinload(CardProductService.employees) + ), + selectinload(Card.status_history) + .joinedload(CardStatusHistory.user), + selectinload(Card.status_history) + .noload(CardStatusHistory.card), + selectinload(Card.pallets) + .selectinload(Pallet.shipping_products) + .selectinload(ShippingProduct.product) + .noload(Product.barcodes), + selectinload(Card.boxes) + .selectinload(Box.product) + .noload(Product.barcodes), + selectinload(Card.employees) + .joinedload(CardEmployees.user), + ) + .where(Card.id == card_id) + ) + if return_raw: + return card + if not card: + raise HTTPException(status_code=404, detail="Карточка не найдена") + + return CardSchema.model_validate(card) + + async def update_general_info(self, request: CardUpdateGeneralInfoRequest, + user: User) -> CardUpdateGeneralInfoResponse: + try: + card: Card = await self.session.scalar( + select(Card) + .options( + selectinload(Card.group) + .selectinload(CardGroup.cards) + ) + .where(Card.id == request.card_id) + ) + if not card: + raise HTTPException(status_code=404, detail="Карточка не найдена") + card.name = request.data.name + card.comment = request.data.comment + card.is_deleted = request.data.is_deleted + card.is_completed = request.data.is_completed + + if card.board_id != request.data.board_id or card.current_status_id != request.data.status_id: + if card.group: + for card in card.group.cards: + card.board_id = request.data.board_id + await self.change_status(card, request.data.status_id, user) + else: + card.board_id = request.data.board_id + await self.change_status(card, request.data.status_id, user) + + # Updating shipping warehouse + shipping_warehouse_service = ShippingWarehouseService(self.session) + shipping_warehouse = await shipping_warehouse_service.get_by_name(request.data.shipping_warehouse) + if not shipping_warehouse and request.data.shipping_warehouse: + shipping_warehouse = await shipping_warehouse_service.create_by_name(request.data.shipping_warehouse) + + card.shipping_warehouse = shipping_warehouse + + # Updating manager + if request.data.manager: + card.manager_id = request.data.manager.id + else: + card.manager = None + + card_attrs_handler = CardAttributesCommandHandler(self.session) + await card_attrs_handler.set_attributes(card, request.data.attributes) + + await self.session.commit() + return CardUpdateGeneralInfoResponse(ok=True, message='Данные карточки успешно обновлены') + except Exception as e: + await self.session.rollback() + return CardUpdateGeneralInfoResponse(ok=False, message=str(e)) + + async def reorder(self, request: CardSummaryReorderRequest, user: User) -> CardSummaryResponse: + card: Card = await self.session.scalar(select(Card).where(Card.id == request.card_id)) + if request.index == 1: + request.index = 0 + is_first = request.index == 0 + stmt = ( + select(Card) + .where( + Card.current_status_id == request.status_id, + Card.id != request.card_id, + Card.is_deleted == False, + Card.is_completed == False + ) + .order_by(Card.lexorank) + .offset(max([request.index - 2, 0])) + .limit(2 if not is_first else 1) + + ) + query = await self.session.execute(stmt) + boundaries = query.scalars().all() + top_boundary: Union[Card, None] = boundaries[0] if not is_first else None + bottom_boundary: Union[Card, None] = boundaries[1] if len(boundaries) == 2 else None + # working when between two elements + if top_boundary and bottom_boundary: + top_lexorank = lexorank.parse(top_boundary.lexorank) + bottom_lexorank = lexorank.parse(bottom_boundary.lexorank) + new_rank = lexorank.between(top_lexorank, bottom_lexorank) + + # working when at the bottom + elif top_boundary and not bottom_boundary: + new_rank = lexorank.parse(top_boundary.lexorank).next() + # working when at the top + elif bottom_boundary and not top_boundary: + new_rank = lexorank.parse(bottom_boundary.lexorank).prev() + elif not top_boundary and not bottom_boundary and len(boundaries) > 0: + new_rank = lexorank.parse(boundaries[0].lexorank).prev() + else: + new_rank = lexorank.middle(lexorank.Bucket.BUCEKT_0) + + await self.change_status(card, request.status_id, user, + deadline=request.deadline, + comment=request.comment, + rank=str(new_rank)) + await self.session.commit() + return await self.get_summary() + + async def add_kit_to_card(self, request: CardAddKitRequest) -> CardAddKitResponse: + try: + card = await self._get_card_by_id(request.card_id) + if not card: + return CardAddKitResponse(ok=False, message="Указанная карточка не найдена") + kit = await ServiceService(self.session).get_kit_by_id(request.kit_id) + if not kit: + return CardAddKitResponse(ok=False, message="Указанный набор услуг не найден") + services: list[Service] = kit.services + insert_data = [] + for service in services: + price = self.get_service_price(service, 1) + insert_data.append({ + 'card_id': card.id, + 'service_id': service.id, + 'quantity': 1, + 'price': price + }) + if not insert_data: + return CardAddKitResponse(ok=True, message="Набор услуг успешно добавлен") + # Deleting previous services + delete_stmt = ( + delete( + CardService + ) + .where( + CardService.card_id == request.card_id + ) + ) + await self.session.execute(delete_stmt) + await self.session.flush() + + await self.session.execute( + insert(CardService), + insert_data + ) + await self.session.flush() + await self.session.commit() + return CardAddKitResponse(ok=True, message="Набор услуг успешно добавлен") + except Exception as e: + return CardAddKitResponse(ok=False, message=str(e)) + + def create_guest_url(self, user: User, request: CardCreateGuestUrlRequest) -> CardCreateGuestUrlResponse: + # if not user.is_admin: + # return CardCreateGuestUrlResponse(ok=False, message='Создать ссылку может только администратор', url="") + access_token = AuthService(self.session).create_deal_guest_token(request.card_id) + url = f"deals/{request.card_id}?accessToken={access_token}" + return CardCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url) + + async def _create_card_services_for_prefilling(self, old_card: Card, new_card: Card): + for service in old_card.services: + card_service = CardService( + service_id=service.service_id, + card_id=new_card.id, + quantity=service.quantity, + price=service.price, + ) + self.session.add(card_service) + + async def _create_card_products_for_prefilling(self, old_card: Card, new_card: Card): + for old_card_product in old_card.products: + card_product = CardProduct( + card_id=new_card.id, + product_id=old_card_product.product.id, + quantity=old_card_product.quantity, + comment=old_card_product.comment, + ) + self.session.add(card_product) + await self.session.flush() + for old_service in old_card_product.services: + card_product_service = CardProductService( + card_id=new_card.id, + product_id=old_card_product.product.id, + service_id=old_service.service.id, + price=old_service.price + ) + self.session.add(card_product_service) + + async def prefill_card_products_and_services(self, user, request: CardPrefillRequest) -> CardPrefillResponse: + old_card: Card = await self.get_by_id(user, request.old_card_id, return_raw=True) + new_card: Card = await self.get_by_id(user, request.new_card_id, return_raw=True) + + await self._create_card_services_for_prefilling(old_card, new_card) + await self._create_card_products_for_prefilling(old_card, new_card) + await self.session.commit() + + return CardPrefillResponse(ok=True, message="Карточка успешно предзаполнена товарами и услугами") + + # endregion + + # region Card services + async def add_services(self, request: CardAddServicesRequest): + card: Card = await self.session.scalar( + select(Card) + .options(selectinload(Card.services)) + .where(Card.id == request.card_id) + ) + if not card: + raise HTTPException(status_code=404, detail="Card is not found") + + services_ids = [service.id for service in request.services] + existing_service_ids = {service.service_id for service in card.services} + request_services_dict = {service.id: service.quantity for service in request.services} + + services_query = await self.session.scalars(select(Service).where(Service.id.in_(services_ids))) + services = services_query.all() + if len(services) != len(services_ids): + raise HTTPException(status_code=404, detail="Some of services is not found") + + # Adding quantity + for card_service in card.services: + card_service: CardService + if card_service.service_id not in services_ids: + continue + card_service.quantity += request_services_dict[card_service.service_id] + + # Adding new services + for service in services: + if service.id in existing_service_ids: + continue + quantity = request_services_dict[service.id] + card.services.append( + CardService( + service_id=service.id, + card_id=card.id, + quantity=quantity + ) + ) + await self.session.commit() + return CardAddServicesResponse(ok=True, message='Услуги успешно добавлены') + + async def update_service_quantity( + self, + user: Union[User, dict], + request: CardUpdateServiceQuantityRequest + ) -> CardUpdateServiceQuantityResponse: + try: + self.grant_access(user, request.card_id) + card_service = await self.session.scalar( + select(CardService) + .where(CardService.card_id == request.card_id, + CardService.service_id == request.service_id) + ) + if not card_service: + raise HTTPException(status_code=404, detail="Карточка не найдена") + card_service.quantity = request.quantity + await self.session.commit() + return CardUpdateServiceQuantityResponse(ok=True, message='Количество успешно обновлено') + except Exception as e: + await self.session.rollback() + return CardUpdateServiceQuantityResponse(ok=False, message=str(e)) + + async def add_service( + self, + user: Union[User, dict], + request: CardAddServiceRequest + ) -> CardAddServiceResponse: + try: + self.grant_access(user, request.card_id) + card = await self.session.scalar(select(Card).where(Card.id == request.card_id)) + if not card: + raise HTTPException(status_code=404, detail="Карточка не найдена") + service: Service = await self.session.scalar(select(Service).where(Service.id == request.service_id)) + if not service: + raise HTTPException(status_code=404, detail="Услуга не найдена") + # Preventing duplicates + card_service = await self.session.scalar( + select(CardService) + .where(CardService.card_id == request.card_id, + CardService.service_id == request.service_id) + ) + if card_service: + raise HTTPException(status_code=400, detail="Услуга уже добавлена") + card_service = CardService( + card_id=request.card_id, + service_id=request.service_id, + quantity=request.quantity, + price=request.price + ) + self.session.add(card_service) + await self.session.commit() + return CardAddServiceResponse(ok=True, message='Услуга успешно добавлена') + except Exception as e: + await self.session.rollback() + return CardAddServiceResponse(ok=False, message=str(e)) + + async def delete_service( + self, + user: Union[User, dict], + request: CardDeleteServiceRequest + ) -> CardDeleteServiceResponse: + try: + self.grant_access(user, request.card_id) + card_service = await self.session.scalar( + select(CardService) + .where(CardService.card_id == request.card_id, + CardService.service_id == request.service_id) + ) + if not card_service: + raise HTTPException(status_code=404, detail="Карточка не найдена") + await self.session.delete(card_service) + await self.session.commit() + return CardDeleteServiceResponse(ok=True, message='Услуга успешно удалена') + except Exception as e: + await self.session.rollback() + return CardDeleteServiceResponse(ok=False, message=str(e)) + + async def delete_services( + self, + user: Union[User, dict], + request: CardDeleteServicesRequest + ) -> CardDeleteServicesResponse: + try: + self.grant_access(user, request) + card_services = await self.session.scalars( + select(CardService) + .where(CardService.card_id == request.card_id, + CardService.service_id.in_(request.service_ids)) + ) + for card_service in card_services: + await self.session.delete(card_service) + await self.session.commit() + return CardDeleteServicesResponse(ok=True, message='Услуги успешно удалены') + except Exception as e: + await self.session.rollback() + return CardDeleteServicesResponse(ok=False, message=str(e)) + + async def update_service( + self, + user: Union[User, dict], + request: CardUpdateServiceRequest + ) -> CardUpdateServiceResponse: + try: + self.grant_access(user, request.card_id) + card_service = await self.session.scalar( + select(CardService) + .where(CardService.card_id == request.card_id, + CardService.service_id == request.service.service.id) + ) + if not card_service: + 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(CardService) + .where(CardService.card_id == request.card_id, + CardService.service_id == request.service.service.id) + .values(**service_dict) + ) + + # Updating deleting previous employees + delete_stmt = ( + delete( + card_service_employees + ) + .where( + card_service_employees.c.card_id == request.card_id, + card_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({ + 'card_id': request.card_id, + 'service_id': request.service.service.id, + 'user_id': employee.id + }) + if insert_data: + await self.session.execute( + insert(card_service_employees), + insert_data + ) + await self.session.flush() + + await self.session.commit() + return CardUpdateServiceQuantityResponse(ok=True, message='Услуга успешно обновлена') + except Exception as e: + await self.session.rollback() + return CardUpdateServiceQuantityResponse(ok=False, message=str(e)) + + async def copy_services( + self, + user: Union[User, dict], + request: CardServicesCopyRequest + ) -> CardServicesCopyResponse: + try: + self.grant_access(user, request.card_id) + source_services_stmt = ( + select( + CardProductService + ) + .where( + CardProductService.product_id == request.source_product_id, + CardProductService.card_id == request.card_id, + ) + ) + card_product_services = ( + ( + await self.session.scalars( + source_services_stmt + ) + ) + .all() + ) + + destination_card_products_stmt = ( + select( + CardProduct + ) + .where( + CardProduct.product_id.in_(request.destination_product_ids), + CardProduct.card_id == request.card_id + ) + ) + destination_card_products = (await self.session.scalars(destination_card_products_stmt)).all() + insert_data = [] + for card_product in destination_card_products: + for service in card_product_services: + insert_data.append({ + 'card_id': request.card_id, + 'product_id': card_product.product_id, + 'service_id': service.service.id, + 'price': service.price, + 'is_fixed_price': service.is_fixed_price + }) + if not insert_data: + return CardServicesCopyResponse(ok=True, message='Услуги успешно перенесены') + # Deleting previous CardProductService-s + delete_stmt = ( + delete( + CardProductService + ) + .where( + CardProductService.product_id.in_(request.destination_product_ids), + CardProductService.card_id == request.card_id + ) + ) + await self.session.execute(delete_stmt) + await self.session.flush() + + await self.session.execute( + insert(CardProductService), + insert_data + ) + await self.session.flush() + await self.session.commit() + return CardServicesCopyResponse(ok=True, message='Услуги успешно перенесены') + except Exception as e: + return CardServicesCopyResponse(ok=False, message=str(e)) + + @staticmethod + def get_service_price(service: Service, quantity: int): + price = 0 + price_ranges: list[ServicePriceRange] = service.price_ranges + for price_range in price_ranges: + if price_range.from_quantity <= quantity <= price_range.to_quantity: + price = price_range.price + break + + if not price and len(price_ranges) > 0: + price = price_ranges[0].price + if not price: + price = service.price + return price + + # endregion + + # region Card products + async def update_product_quantity( + self, + user: Union[User, dict], + request: CardUpdateProductQuantityRequest + ) -> CardUpdateProductQuantityResponse: + try: + self.grant_access(user, request.card_id) + # check if there is no card or no product with different exceptions + card_product = await self.session.scalar( + select(CardProduct) + .where(CardProduct.card_id == request.card_id, + CardProduct.product_id == request.product_id) + ) + if not card_product: + raise HTTPException(status_code=404, detail="Карточка или товар не найден") + card_product.quantity = request.quantity + await self.session.commit() + return CardUpdateProductQuantityResponse(ok=True, message='Количество успешно обновлено') + except Exception as e: + await self.session.rollback() + return CardUpdateProductQuantityResponse(ok=False, message=str(e)) + + async def add_product( + self, + user: Union[User, dict], + + request: CardAddProductRequest + ) -> CardAddProductResponse: + try: + self.grant_access(user, request.card_id) + + card = await self.session.scalar(select(Card).where(Card.id == request.card_id)) + if not card: + raise HTTPException(status_code=404, detail="Карточка не найдена") + product = await self.session.scalar( + select(Product).where(Product.id == request.product.product.id)) + if not product: + raise HTTPException(status_code=404, detail="Товар не найден") + # Preventing duplicates + card_product = await self.session.scalar( + select(CardProduct) + .where(CardProduct.card_id == request.card_id, + CardProduct.product_id == request.product.product.id) + ) + if card_product: + raise HTTPException(status_code=400, detail="Товар уже добавлен") + card_product = CardProduct( + card_id=request.card_id, + product_id=request.product.product.id, + quantity=request.product.quantity + ) + self.session.add(card_product) + await self.session.flush() + for service in request.product.services: + card_product_service = CardProductService( + card_id=request.card_id, + product_id=request.product.product.id, + service_id=service.service.id, + price=service.price + ) + self.session.add(card_product_service) + await self.session.commit() + return CardAddProductResponse(ok=True, message='Товар успешно добавлен') + except Exception as e: + await self.session.rollback() + return CardAddProductResponse(ok=False, message=str(e)) + + async def delete_product( + self, + user: Union[User, dict], + request: CardDeleteProductRequest + ) -> CardDeleteProductResponse: + try: + self.grant_access(user, request.card_id) + card_product = await self.session.scalar( + select(CardProduct) + .where(CardProduct.card_id == request.card_id, + CardProduct.product_id == request.product_id) + ) + if not card_product: + raise HTTPException(status_code=404, detail="Карточка не найдена") + await self.session.delete(card_product) + await self.session.commit() + return CardDeleteProductResponse(ok=True, message='Товар успешно удален') + except Exception as e: + await self.session.rollback() + return CardDeleteProductResponse(ok=False, message=str(e)) + + async def delete_products( + self, + user: Union[User, dict], + request: CardDeleteProductsRequest + ) -> CardDeleteProductsResponse: + try: + self.grant_access(user, request.card_id) + card_products = await self.session.scalars( + select(CardProduct) + .where(CardProduct.card_id == request.card_id, + CardProduct.product_id.in_(request.product_ids)) + ) + for card_product in card_products: + await self.session.delete(card_product) + await self.session.commit() + return CardDeleteProductsResponse(ok=True, message='Товары успешно удалены') + except Exception as e: + await self.session.rollback() + return CardDeleteProductsResponse(ok=False, message=str(e)) + + async def update_product( + self, + user: Union[User, dict], + request: CardUpdateProductRequest + ): + try: + self.grant_access(user, request.card_id) + card_product: CardProduct = await self.session.scalar( + select(CardProduct) + .where(CardProduct.card_id == request.card_id, + CardProduct.product_id == request.product.product.id) + ) + if not card_product: + raise HTTPException(status_code=404, detail="Указанный товар не найден") + # getting new services and deleted + database_services = set([service.service_id for service in card_product.services]) + request_services = set([service.service.id for service in request.product.services]) + + new_services = request_services.difference(database_services) + deleted_services = database_services.difference(request_services) + services_dict = {service.service.id: service for service in request.product.services} + + # Deleting and updating existing services + for service in card_product.services: + service: CardProductService + if service.service_id in deleted_services: + await self.session.delete(service) + await self.session.flush() + continue + request_service = services_dict[service.service_id] + service.price = request_service.price + service.is_fixed_price = request_service.is_fixed_price + await self.session.flush() + + # Creating services + for service in request.product.services: + if service.service.id not in new_services: + continue + card_product_service = CardProductService( + card_id=request.card_id, + product_id=request.product.product.id, + service_id=service.service.id, + price=service.price + ) + self.session.add(card_product_service) + await self.session.flush() + + # Updating product + card_product.quantity = request.product.quantity + card_product.comment = request.product.comment + + # Updating deleting old employees + delete_stmt = ( + delete( + card_product_service_employees + ) + .where( + card_product_service_employees.c.card_id == request.card_id, + card_product_service_employees.c.service_id.in_(request_services.union(database_services)), + card_product_service_employees.c.product_id == request.product.product.id + ) + ) + await self.session.execute(delete_stmt) + await self.session.flush() + + insert_data = [] + for product_service in request.product.services: + product_service: CardProductServiceSchema + for employee in product_service.employees: + insert_data.append({ + 'card_id': request.card_id, + 'service_id': product_service.service.id, + 'product_id': request.product.product.id, + 'user_id': employee.id + }) + if insert_data: + await self.session.execute(insert(card_product_service_employees), + insert_data) + await self.session.flush() + + await self.session.commit() + return CardUpdateProductResponse(ok=True, message='Товар успешно обновлен') + + except Exception as e: + await self.session.rollback() + return CardUpdateProductResponse(ok=False, message=str(e)) + + async def add_kit_to_card_product( + self, + user: Union[User, dict], + request: CardProductAddKitRequest + ) -> CardProductAddKitResponse: + try: + self.grant_access(user, request.card_id) + service_service = ServiceService(self.session) + kit = await service_service.get_kit_by_id(request.kit_id) + if not kit: + return CardProductAddKitResponse(ok=False, message='Указанный набор услуг не найден') + services: list[Service] = kit.services + card_product_stmt = ( + select( + CardProduct + ) + .where( + CardProduct.card_id == request.card_id, + CardProduct.product_id == request.product_id + ) + ) + card_product: Optional[CardProduct] = await self.session.scalar(card_product_stmt) + if not card_product: + return CardProductAddKitResponse(ok=False, message='Указанный товар не найден') + insert_data = [] + for service in services: + service_price = self.get_service_price(service, card_product.quantity) + insert_data.append({ + 'card_id': request.card_id, + 'product_id': card_product.product_id, + 'service_id': service.id, + 'price': service_price, + }) + if not insert_data: + return CardProductAddKitResponse(ok=True, message='Набор услуг успешно добавлен к товару') + # Deleting previous CardProductService-s + delete_stmt = ( + delete( + CardProductService + ) + .where( + CardProductService.product_id == card_product.product_id, + CardProductService.card_id == request.card_id + ) + ) + await self.session.execute(delete_stmt) + await self.session.flush() + + await self.session.execute( + insert(CardProductService), + insert_data + ) + await self.session.flush() + await self.session.commit() + return CardProductAddKitResponse(ok=True, message='Набор услуг успешно добавлен к товару') + except Exception as e: + return CardProductAddKitResponse(ok=False, message=str(e)) + + # endregion + + async def complete(self, user: User, request: CardCompleteRequest) -> CardCompleteResponse: + try: + # check for admin + if not user.is_admin: + return CardCompleteResponse(ok=False, message='Завершить может только администратор') + + card = await self._get_card_by_id(request.card_id) + if not card: + return CardCompleteResponse(ok=False, message="Карточка не найдена") + + if card.group: + await card_group.CardGroupService(self.session).complete_group(card.group.id) + else: + card.is_completed = True + await self.session.commit() + + return CardCompleteResponse(ok=True, message="Успешно завершена") + except Exception as e: + await self.session.rollback() + return CardCompleteResponse(ok=False, message=str(e)) + + async def get_quantity_dict(self, cards: List[Card]): + services_quantity = defaultdict(lambda: 0) + for card in cards: + for product in card.products: + product: CardProduct + for service in product.services: + service: CardProductService + services_quantity[service.service_id] += product.quantity + for service in card.services: + service: CardService + services_quantity[service.service_id] += service.quantity + return services_quantity + + async def _recalculate_price_single(self, card: Card, quantity_dict: dict): + services_quantity = quantity_dict + services_prices = {} + for product in card.products: + for service in product.services: + if service.is_fixed_price: + continue + quantity = services_quantity[service.service_id] + if service.service_id in services_prices: + service.price = services_prices[service.service_id] + continue + price = self.get_service_price( + service=service.service, + quantity=quantity + ) + service.price = price + services_prices[service.service_id] = price + for service in card.services: + service: CardService + if service.is_fixed_price: + continue + quantity = services_quantity[service.service_id] + price = self.get_service_price( + service=service.service, + quantity=quantity + ) + print(service.service_id, price) + service.price = price + + async def _recalculate_price_group(self, group: CardGroup): + cards = await self.session.scalars( + select(Card) + .options( + selectinload(Card.services) + .joinedload(CardService.service), + selectinload(Card.products) + .selectinload(CardProduct.services) + .joinedload(CardProductService.service), + ) + .where(Card.group == group) + ) + cards = list(cards.all()) + services_quantity = await self.get_quantity_dict(cards) + for card in cards: + await self._recalculate_price_single(card, services_quantity) + + async def recalculate_price(self, request: CardRecalculatePriceRequest) -> CardRecalculatePriceResponse: + try: + card_stmt = ( + select( + Card + ) + .options( + selectinload(Card.services) + .joinedload(CardService.service), + selectinload(Card.products) + .selectinload(CardProduct.services) + .joinedload(CardProductService.service), + joinedload(Card.group) + ) + .where(Card.id == request.card_id) + ) + card: Card = await self.session.scalar(card_stmt) + if not card.group: + quantity_dict = await self.get_quantity_dict([card]) + await self._recalculate_price_single(card, quantity_dict) + else: + await self._recalculate_price_group(card.group) + await self.session.commit() + return CardRecalculatePriceResponse(ok=True, message="Цены успешно пересчитаны") + except Exception as e: + return CardRecalculatePriceResponse(ok=False, message=str(e)) + + async def _assign_employee(self, card: Card, user: User) -> tuple[bool, str]: + assigned_employee_ids = [assignment.user_id for assignment in card.employees] + if user.id in assigned_employee_ids: + return False, "Работник уже назначен" + + assignment = CardEmployees(user_id=user.id, card_id=card.id, created_at=datetime.now()) + self.session.add(assignment) + await self.session.commit() + + return True, "Работник успешно назначен" + + async def _unassign_employee(self, card: Card, user: User) -> tuple[bool, str]: + assigned_employee_ids = [assignment.user_id for assignment in card.employees] + if user.id not in assigned_employee_ids: + return False, "Работник еще не назначен" + + stmt = delete(CardEmployees).where(and_(CardEmployees.user_id == user.id, CardEmployees.card_id == card.id)) + await self.session.execute(stmt) + await self.session.commit() + + return True, "Работник успешно удален" + + async def manage_employee(self, request: ManageEmployeeRequest) -> ManageEmployeeResponse: + card: Optional[Card] = await self._get_card_by_id(request.card_id) + if not card: + return ManageEmployeeResponse(ok=False, message=f"Карточка с ID {request.card_id} не найдена") + + user: Optional[User] = await self.session.get(User, request.user_id) + if not user: + return ManageEmployeeResponse(ok=False, message=f"Пользователь с ID {request.user_id} не найден") + + if request.is_assign: + ok, message = await self._assign_employee(card, user) + else: + ok, message = await self._unassign_employee(card, user) + + return ManageEmployeeResponse(ok=ok, message=message) + + async def get_available_employees_to_assign(self, card_id: int) -> GetAvailableEmployeesToAssignResponse: + assigned_users = select(CardEmployees.user_id).where(CardEmployees.card_id == card_id) + + stmt_free_employees = ( + select(User) + .where(and_( + User.is_deleted == False, + User.role_key == "employee", + User.id.not_in(assigned_users), + )) + ) + + free_employees = (await self.session.execute(stmt_free_employees)).scalars().all() + return GetAvailableEmployeesToAssignResponse(employees=free_employees) + + async def _create_card_from_excel( + self, + client: Client, + card_status: CardStatus, + breakdown: CityBreakdownFromExcelSchema, + user: User, + ) -> Card: + rank = await self._get_rank_for_card(card_status.id) + card = Card( + name=f"{client.name} - {breakdown.base_marketplace.key.upper()} - {breakdown.shipping_warehouse.name}", + created_at=datetime.now(), + current_status_id=card_status.id, + lexorank=rank, + client_id=client.id, + base_marketplace_key=breakdown.base_marketplace.key, + shipping_warehouse_id=breakdown.shipping_warehouse.id, + ) + + self.session.add(card) + await self.session.flush() + return card + + async def _get_or_create_warehouse( + self, + shipping_warehouse: OptionalShippingWarehouseSchema, + ) -> OptionalShippingWarehouseSchema: + if not shipping_warehouse.id: + stmt = select(ShippingWarehouse).where(ShippingWarehouse.name == shipping_warehouse.name) + row = (await self.session.execute(stmt)).first() + warehouse_model: Optional[ShippingWarehouse] = row[0] if row else None + if warehouse_model: + shipping_warehouse.id = warehouse_model.id + else: + warehouse = await ShippingWarehouseService(self.session).create_by_name(shipping_warehouse.name) + shipping_warehouse.id = warehouse.id + + return shipping_warehouse + + async def create_cards_from_excel( + self, + request: CreateCardsFromExcelRequest, + user: User, + ) -> CreateCardsFromExcelResponse: + client: Optional[Client] = await self.session.get(Client, request.client_id) + if not client: + return CreateCardsFromExcelResponse(ok=False, message=f"Клиент с ID {request.client_id} не найден") + + card_status: Optional[CardStatus] = await self.session.get(CardStatus, request.status_id) + if not card_status: + return CreateCardsFromExcelResponse(ok=False, message=f"Статус с ID {request.status_id} не найден") + + cards_dict: dict[str, Card] = {} + group = await card_group.CardGroupService(self.session).create_group_model() + + for product_data in request.products: + for breakdown in product_data.cities_breakdown: + breakdown.shipping_warehouse = await self._get_or_create_warehouse(breakdown.shipping_warehouse) + + key = f"{breakdown.shipping_warehouse.id} - {breakdown.base_marketplace.key}" + card = cards_dict.get(key) + if not card: + card = await self._create_card_from_excel(client, card_status, breakdown, user) + cards_dict[key] = card + + insert_stmt = insert(card_relations).values({ + 'card_id': card.id, + 'group_id': group.id + }) + await self.session.execute(insert_stmt) + + card_product = CardProduct( + card_id=card.id, + product_id=product_data.product_id, + quantity=breakdown.quantity, + ) + self.session.add(card_product) + + await self.session.commit() + return CreateCardsFromExcelResponse(ok=True, message="Карточки успешно созданы") + + async def get_cards_grouped(self, card: Card) -> List[Card]: + if not card.group: + return [card] + cards = await self.session.scalars( + select(Card) + .options( + selectinload(Card.services) + .joinedload(CardService.service), + selectinload(Card.products) + .selectinload(CardProduct.services) + .joinedload(CardProductService.service), + ) + .where(Card.group == card.group) + ) + cards = list(cards.all()) + return cards diff --git a/services/card_group.py b/services/card_group.py new file mode 100644 index 0000000..dac2bd4 --- /dev/null +++ b/services/card_group.py @@ -0,0 +1,168 @@ +from lexorank import lexorank +from sqlalchemy import select, insert, update, delete +from sqlalchemy.orm import selectinload + +from models import CardService as DealServiceModel, User, Card, CardProduct, Product, GroupBillRequest +from models.card_group import CardGroup, card_relations +from schemas.group import * +from services.base import BaseService +from services.card import CardsService + + +class CardGroupService(BaseService): + async def get_cards_by_group_id(self, group_id) -> list[Card]: + group: CardGroup | None = await self.session.scalar( + select(CardGroup) + .where(CardGroup.id == group_id) + .options( + selectinload(CardGroup.cards).selectinload(Card.products).selectinload(CardProduct.services), + selectinload(CardGroup.cards).selectinload(Card.products) + .selectinload(CardProduct.product).selectinload(Product.barcodes), + selectinload(CardGroup.cards).selectinload(Card.services).selectinload(DealServiceModel.service), + selectinload(CardGroup.cards).selectinload(Card.status_history), + selectinload(CardGroup.cards).selectinload(Card.group).selectinload(CardGroup.cards), + selectinload(CardGroup.cards).joinedload(Card.client), + selectinload(CardGroup.cards).joinedload(Card.shipping_warehouse), + ) + ) + return group.cards if group else [] + + async def create_group_model(self) -> CardGroup: + group = CardGroup( + name='', + lexorank=lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__(), + ) + self.session.add(group) + await self.session.flush() + return group + + async def create_group(self, user: User, request: CreateCardGroupRequest) -> CardCreateGroupResponse: + try: + group = await self.create_group_model() + + for card_id in [request.dragging_card_id, request.hovered_card_id]: + insert_stmt = insert(card_relations).values({ + 'card_id': card_id, + 'group_id': group.id + }) + await self.session.execute(insert_stmt) + # changing status if needed on draggable card + dragging_card = await self.session.scalar( + select(Card).where(Card.id == request.dragging_card_id) + ) + dropped_card = await self.session.scalar( + select(Card).where(Card.id == request.hovered_card_id) + ) + if dragging_card.current_status_id != dropped_card.current_status_id: + card_service = CardsService(self.session) + await card_service.change_status(dragging_card, dropped_card.current_status_id, user) + await self.session.commit() + return CardCreateGroupResponse(ok=True, message="Группа успешно создана") + except Exception as e: + return CardCreateGroupResponse(ok=False, message=str(e)) + + async def update_group(self, request: CardGroupUpdateRequest) -> CardGroupUpdateResponse: + try: + group = await self.session.scalar( + select(CardGroup).where(CardGroup.id == request.data.id) + ) + if not group: + return CardGroupUpdateResponse(ok=False, message="Группа не найдена") + # update by dictionary + request_dict = request.data.model_dump() + request_dict.pop("bill_request", None) + + update_stmt = ( + update(CardGroup) + .where(CardGroup.id == request.data.id) + .values(**request_dict) + ) + await self.session.execute(update_stmt) + await self.session.commit() + return CardGroupUpdateResponse(ok=True, message="Группа успешно обновлена") + except Exception as e: + await self.session.rollback() + return CardGroupUpdateResponse(ok=False, message=str(e)) + + async def complete_group(self, group_id: int) -> list[Card]: + cards = await self.get_cards_by_group_id(group_id) + for card in cards: + card.is_completed = True + return cards + + async def delete_group(self, group_id: int) -> None: + cards = await self.get_cards_by_group_id(group_id) + for card in cards: + card.is_deleted = True + await self.session.commit() + + async def change_group_status( + self, + user: User, + request: CardGroupChangeStatusRequest, + ) -> CardGroupChangeStatusResponse: + try: + # getting all cards in group + cards = await self.session.scalars( + select(card_relations.c.card_id) + .where(card_relations.c.group_id == request.group_id) + ) + + card_service = CardsService(self.session) + for card_id in cards: + card = await self.session.scalar( + select(Card).where(Card.id == card_id) + ) + await card_service.change_status(card, request.new_status, user) + await self.session.commit() + return CardGroupChangeStatusResponse(ok=True, message="Статус группы успешно изменен") + except Exception as e: + await self.session.rollback() + return CardGroupChangeStatusResponse(ok=False, message=str(e)) + + async def add_card(self, user: User, request: CardAddToGroupRequest) -> CardAddToGroupResponse: + try: + group_bill_request = await self.session.get(GroupBillRequest, request.group_id) + if group_bill_request: + raise Exception("Нельзя добавить сделку, так как на группу выставлен счёт.") + + # changing status if needed + card_id = await self.session.scalar( + select(card_relations.c.card_id) + .where(card_relations.c.group_id == request.group_id) + ) + group_card_status = await self.session.scalar( + select(Card.current_status_id) + .where(Card.id == card_id) + ) + request_card = await self.session.scalar( + select(Card).where(Card.id == request.card_id) + ) + if group_card_status != request_card.current_status_id: + await CardsService(self.session).change_status(request_card, group_card_status, user) + insert_stmt = insert(card_relations).values({ + 'card_id': request.card_id, + 'group_id': request.group_id + }) + await self.session.execute(insert_stmt) + await self.session.commit() + + return CardAddToGroupResponse(ok=True, message="Сделка успешно добавлена в группу") + except Exception as e: + await self.session.rollback() + return CardAddToGroupResponse(ok=False, message=str(e)) + + async def remove_card(self, request: CardRemoveFromGroupRequest) -> CardRemoveFromGroupResponse: + try: + delete_stmt = ( + delete(card_relations) + .where( + card_relations.c.card_id == request.card_id, + ) + ) + await self.session.execute(delete_stmt) + await self.session.commit() + return CardRemoveFromGroupResponse(ok=True, message="Сделка успешно удалена из группы") + except Exception as e: + await self.session.rollback() + return CardRemoveFromGroupResponse(ok=False, message=str(e)) diff --git a/services/deal.py b/services/deal.py deleted file mode 100644 index 0988360..0000000 --- a/services/deal.py +++ /dev/null @@ -1,1317 +0,0 @@ -from collections import defaultdict - -import lexorank -from fastapi import HTTPException -from sqlalchemy import select, func, update, delete, insert, and_ -from sqlalchemy.orm import joinedload, selectinload -from starlette import status - -from models import User, Service, Client, DealProductService, deal_relations, DealStatusHistory, Product, DealProduct -import models.deal -import models.secondary -from models.deal import * -from models.deal_group import DealGroup -from models.shipping import ShippingProduct -from schemas.client import ClientDetailsSchema -from schemas.deal import * -from services.auth import AuthService -from services.base import BaseService -from services.client import ClientService -from services.service import ServiceService -from services.shipping_warehouse import ShippingWarehouseService -from services import deal_group - - -class DealService(BaseService): - - # region Deal - - @staticmethod - def grant_access(user: Union[models.User, dict], deal_id): - if type(user) is models.User: - return - user_deal_id = user['deal_id'] - if int(user_deal_id) != int(deal_id): - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail='Invalid token') - - async def _get_deal_by_id(self, deal_id) -> Union[Deal, None]: - return await self.session.get(Deal, deal_id) - - async def _get_rank_for_deal(self, status_id: int) -> str: - deal_query = await self.session.execute( - select(Deal) - .where(Deal.current_status_id == status_id) - .order_by(Deal.lexorank.desc()) - .limit(1) - ) - deal = deal_query.scalar_one_or_none() - if not deal: - prev = lexorank.middle(lexorank.Bucket.BUCEKT_0) - return str(prev.next()) - return str(lexorank.parse(deal.lexorank).next()) - - async def change_status(self, deal: Deal, - status_id: int, - user: models.User, - deadline: datetime.datetime = None, - rank=None, - comment: str = ''): - if not deal.current_status_id == status_id: - deadline = deadline - status_change = DealStatusHistory( - deal_id=deal.id, - user_id=user.id, - changed_at=datetime.datetime.now(), - from_status_id=deal.current_status_id, - to_status_id=status_id, - next_status_deadline=deadline, - comment=comment - ) - self.session.add(status_change) - deal.current_status_id = status_id - if not rank: - rank = await self._get_rank_for_deal(status_id) - if rank: - deal.lexorank = rank - await self.session.flush() - - async def delete(self, request: DealDeleteRequest) -> DealDeleteResponse: - deal = await self._get_deal_by_id(request.deal_id) - if not deal: - return DealDeleteResponse(ok=False, message="Сделка не найдена") - if deal.group: - await deal_group.DealGroupService(self.session).delete_group(deal.group.id) - else: - deal.is_deleted = True - await self.session.commit() - return DealDeleteResponse(ok=True, message="Сделка успешно удалена") - - async def quick_create(self, request: DealQuickCreateRequest, user: models.User) -> DealQuickCreateResponse: - deal_status = await self.session.get(DealStatus, request.status_id) - if not deal_status: - raise HTTPException(status_code=400, detail="Указан некорректный статус") - - client_service = ClientService(self.session) - client = await client_service.get_by_name(request.client_name) - - if not client: - client = await client_service.create_client_raw( - user, - request.client_name, - ClientDetailsSchema() - ) - - shipping_warehouse_service = ShippingWarehouseService(self.session) - shipping_warehouse = await shipping_warehouse_service.get_by_name(name=request.shipping_warehouse) - if not shipping_warehouse: - shipping_warehouse = await shipping_warehouse_service.create_by_name(name=request.shipping_warehouse) - - rank = await self._get_rank_for_deal(request.status_id) - deal = Deal( - name=request.name, - created_at=datetime.datetime.now(), - client_id=client.id, - current_status_id=request.status_id, - board_id=deal_status.board_id, - lexorank=rank, - shipping_warehouse_id=shipping_warehouse.id, - base_marketplace_key=request.base_marketplace.key - ) - self.session.add(deal) - await self.session.commit() - return DealQuickCreateResponse(deal_id=deal.id) - - async def change_status_manual(self, request: DealChangeStatusRequest, user: models.User) -> DealChangeStatusResponse: - # Changing current status - deal = await self._get_deal_by_id(request.deal_id) - if not deal: - return DealChangeStatusResponse(ok=False) - await self.change_status(deal, request.new_status, user) - await self.session.commit() - return DealChangeStatusResponse(ok=True) - - def _get_price_subquery(self): - deal_services_subquery = ( - select( - models.secondary.DealService.deal_id, - func.sum(models.secondary.DealService.quantity * models.secondary.DealService.price).label( - 'total_price') - ) - .join(Service) - .group_by(models.secondary.DealService.deal_id) - ) - product_services_subquery = select( - select( - models.secondary.DealProductService.deal_id, - func.sum(models.DealProduct.quantity * models.secondary.DealProductService.price).label('total_price') - ) - .join(models.secondary.DealProduct) - .group_by(models.secondary.DealProductService.deal_id) - .subquery() - ) - union_subqueries = deal_services_subquery.union_all(product_services_subquery).subquery() - final_subquery = ( - select( - union_subqueries.c.deal_id, - func.sum(union_subqueries.c.total_price).label('total_price') - ) - .group_by(union_subqueries.c.deal_id) - .subquery() - ) - return final_subquery - - def _get_products_quantity_subquery(self): - deal_products_subquery = ( - select( - models.secondary.DealProduct.deal_id, - func.sum(models.secondary.DealProduct.quantity).label('total_quantity') - ) - .group_by(models.secondary.DealProduct.deal_id) - .subquery() - ) - return deal_products_subquery - - async def get_summary(self, full: bool = False) -> DealSummaryResponse: - price_subquery = self._get_price_subquery() - products_quantity_subquery = self._get_products_quantity_subquery() - q = ( - select( - Deal, - func.coalesce(price_subquery.c.total_price, 0), - func.row_number().over( - partition_by=Deal.current_status_id, - order_by=Deal.lexorank - ).label('rank'), - func.coalesce(products_quantity_subquery.c.total_quantity, 0) - ) - .options( - selectinload(Deal.status_history), - joinedload(Deal.client), - joinedload(Deal.shipping_warehouse), - joinedload(Deal.bill_request), - joinedload(Deal.status), - joinedload(Deal.board), - ) - .outerjoin( - price_subquery, Deal.id == price_subquery.c.deal_id, - ) - .outerjoin( - products_quantity_subquery, Deal.id == products_quantity_subquery.c.deal_id - ) - .where( - Deal.is_deleted == False, - ) - ) - if not full: - q = q.where( - Deal.is_completed == False, - # Deal.current_status != DealStatus.COMPLETED - ) - else: - q = q.order_by(Deal.created_at.desc()) - deals_query = await self.session.execute(q) - summaries = [] - for deal, total_price, rank, products_count in deals_query.all(): - deal: Deal - base_marketplace = None - if deal.base_marketplace: - base_marketplace = BaseMarketplaceSchema.model_validate(deal.base_marketplace) - shipment_warehouse_name = deal.shipping_warehouse.name if deal.shipping_warehouse else None - summaries.append( - DealSummary( - id=deal.id, - client_name=deal.client.name, - name=deal.name, - status=deal.status, - board=deal.board, - total_price=total_price, - rank=rank, - base_marketplace=base_marketplace, - created_at=deal.created_at, - shipment_warehouse_id=deal.shipping_warehouse_id, - shipment_warehouse_name=shipment_warehouse_name, - total_products=products_count, - delivery_date=deal.delivery_date, - receiving_slot_date=deal.receiving_slot_date, - bill_request=deal.bill_request, - group=deal.group - ) - ) - return DealSummaryResponse(summaries=summaries) - - async def get_all(self) -> DealGetAllResponse: - deals_query = ( - await self.session.scalars( - select( - Deal - ) - .options( - joinedload(Deal.shipping_warehouse), - joinedload(Deal.client) - .joinedload(Client.details), - selectinload(Deal.services) - .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), - selectinload(Deal.products) - .joinedload(models.secondary.DealProduct.product) - .joinedload(models.Product.barcodes), - selectinload(Deal.products) - .joinedload(models.secondary.DealProduct.services) - .options( - joinedload(models.secondary.DealProductService.service), - selectinload(models.secondary.DealProductService.employees) - ), - selectinload(Deal.status_history) - .joinedload(DealStatusHistory.user), - selectinload(Deal.status_history) - .noload(DealStatusHistory.deal), - - ) - ) - ) - deals = deals_query.all() - result = [] - for deal in deals: - result.append(DealSchema.model_validate(deal)) - return DealGetAllResponse(deals=result) - - async def get_by_id(self, user: Union[models.User, dict], deal_id: int, return_raw=False) -> Union[ - DealSchema, models.Deal]: - self.grant_access(user, deal_id) - - deal = await self.session.scalar( - select(Deal) - .options( - joinedload(Deal.status), - joinedload(Deal.board) - .joinedload(Board.project), - joinedload(Deal.shipping_warehouse), - joinedload(Deal.client) - .joinedload(Client.details), - selectinload(Deal.services) - .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), - selectinload(Deal.products) - .joinedload(models.secondary.DealProduct.product) - .joinedload(models.Product.barcodes), - selectinload(Deal.products) - .joinedload(models.secondary.DealProduct.services) - .options( - joinedload(models.secondary.DealProductService.service), - selectinload(models.secondary.DealProductService.employees) - ), - selectinload(Deal.status_history) - .joinedload(DealStatusHistory.user), - selectinload(Deal.status_history) - .noload(DealStatusHistory.deal), - selectinload(Deal.pallets) - .selectinload(Pallet.shipping_products) - .selectinload(ShippingProduct.product) - .noload(Product.barcodes), - selectinload(Deal.boxes) - .selectinload(Box.product) - .noload(Product.barcodes), - selectinload(Deal.employees) - .joinedload(DealEmployees.user), - ) - .where(Deal.id == deal_id) - ) - if return_raw: - return deal - if not deal: - raise HTTPException(status_code=404, detail="Сделка не найдена") - return DealSchema.model_validate(deal) - - async def update_general_info(self, request: DealUpdateGeneralInfoRequest, user: User) -> DealUpdateGeneralInfoResponse: - try: - deal: Deal = await self.session.scalar( - select(Deal) - .options( - selectinload(Deal.group) - .selectinload(DealGroup.deals) - ) - .where(Deal.id == request.deal_id) - ) - if not deal: - raise HTTPException(status_code=404, detail="Сделка не найдена") - deal.name = request.data.name - deal.comment = request.data.comment - deal.is_deleted = request.data.is_deleted - deal.is_completed = request.data.is_completed - deal.delivery_date = request.data.delivery_date - deal.receiving_slot_date = request.data.receiving_slot_date - - if deal.board_id != request.data.board.id or deal.current_status_id != request.data.status.id: - if deal.group: - for deal in deal.group.deals: - deal.board_id = request.data.board.id - await self.change_status(deal, request.data.status.id, user) - else: - deal.board_id = request.data.board.id - await self.change_status(deal, request.data.status.id, user) - - if deal.group: - for deal in deal.group.deals: - deal.is_accounted = request.data.is_accounted - else: - deal.is_accounted = request.data.is_accounted - - # Updating shipping warehouse - shipping_warehouse_service = ShippingWarehouseService(self.session) - shipping_warehouse = await shipping_warehouse_service.get_by_name(request.data.shipping_warehouse) - if not shipping_warehouse and request.data.shipping_warehouse: - shipping_warehouse = await shipping_warehouse_service.create_by_name(request.data.shipping_warehouse) - - deal.shipping_warehouse = shipping_warehouse - - # Updating manager - if request.data.manager: - deal.manager_id = request.data.manager.id - else: - deal.manager = None - await self.session.commit() - return DealUpdateGeneralInfoResponse(ok=True, message='Данные о сделке успешно обновлены') - except Exception as e: - await self.session.rollback() - return DealUpdateGeneralInfoResponse(ok=False, message=str(e)) - - async def reorder(self, request: DealSummaryReorderRequest, user: User) -> DealSummaryResponse: - deal: Deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) - if request.index == 1: - request.index = 0 - is_first = request.index == 0 - stmt = ( - select(Deal) - .where( - Deal.current_status_id == request.status_id, - Deal.id != request.deal_id, - Deal.is_deleted == False, - Deal.is_completed == False - ) - .order_by(Deal.lexorank) - .offset(max([request.index - 2, 0])) - .limit(2 if not is_first else 1) - - ) - query = await self.session.execute(stmt) - boundaries = query.scalars().all() - top_boundary: Union[Deal, None] = boundaries[0] if not is_first else None - bottom_boundary: Union[Deal, None] = boundaries[1] if len(boundaries) == 2 else None - # working when between two elements - if top_boundary and bottom_boundary: - top_lexorank = lexorank.parse(top_boundary.lexorank) - bottom_lexorank = lexorank.parse(bottom_boundary.lexorank) - new_rank = lexorank.between(top_lexorank, bottom_lexorank) - - # working when at the bottom - elif top_boundary and not bottom_boundary: - new_rank = lexorank.parse(top_boundary.lexorank).next() - # working when at the top - elif bottom_boundary and not top_boundary: - new_rank = lexorank.parse(bottom_boundary.lexorank).prev() - elif not top_boundary and not bottom_boundary and len(boundaries) > 0: - new_rank = lexorank.parse(boundaries[0].lexorank).prev() - else: - new_rank = lexorank.middle(lexorank.Bucket.BUCEKT_0) - - await self.change_status(deal, request.status_id, user, - deadline=request.deadline, - comment=request.comment, - rank=str(new_rank)) - await self.session.commit() - return await self.get_summary() - - async def add_kit_to_deal(self, request: DealAddKitRequest) -> DealAddKitResponse: - try: - deal = await self._get_deal_by_id(request.deal_id) - if not deal: - return DealAddKitResponse(ok=False, message="Указанная сделка не найдена") - kit = await ServiceService(self.session).get_kit_by_id(request.kit_id) - if not kit: - return DealAddKitResponse(ok=False, message="Указанный набор услуг не найден") - services: list[models.Service] = kit.services - insert_data = [] - for service in services: - price = self.get_service_price(service, 1) - insert_data.append({ - 'deal_id': deal.id, - 'service_id': service.id, - 'quantity': 1, - 'price': price - }) - if not insert_data: - return DealAddKitResponse(ok=True, message="Набор услуг успешно добавлен к сделке") - # Deleting previous services - delete_stmt = ( - delete( - models.DealService - ) - .where( - models.DealService.deal_id == request.deal_id - ) - ) - await self.session.execute(delete_stmt) - await self.session.flush() - - await self.session.execute( - insert(models.DealService), - insert_data - ) - await self.session.flush() - await self.session.commit() - return DealAddKitResponse(ok=True, message="Набор услуг успешно добавлен к сделке") - except Exception as e: - return DealAddKitResponse(ok=False, message=str(e)) - - def create_guest_url(self, user: models.User, request: DealCreateGuestUrlRequest) -> DealCreateGuestUrlResponse: - # if not user.is_admin: - # return DealCreateGuestUrlResponse(ok=False, message='Создать ссылку может только администратор', url="") - access_token = AuthService(self.session).create_deal_guest_token(request.deal_id) - url = f"deals/{request.deal_id}?accessToken={access_token}" - return DealCreateGuestUrlResponse(ok=True, message='Ссылка успешно создана!', url=url) - - async def _create_deal_services_for_prefilling(self, old_deal: models.Deal, new_deal: models.Deal): - for service in old_deal.services: - deal_service = models.secondary.DealService( - service_id=service.service_id, - deal_id=new_deal.id, - quantity=service.quantity, - price=service.price, - ) - self.session.add(deal_service) - - async def _create_deal_products_for_prefilling(self, old_deal: models.Deal, new_deal: models.Deal): - for old_deal_product in old_deal.products: - deal_product = models.secondary.DealProduct( - deal_id=new_deal.id, - product_id=old_deal_product.product.id, - quantity=old_deal_product.quantity, - comment=old_deal_product.comment, - ) - self.session.add(deal_product) - await self.session.flush() - for old_service in old_deal_product.services: - deal_product_service = models.secondary.DealProductService( - deal_id=new_deal.id, - product_id=old_deal_product.product.id, - service_id=old_service.service.id, - price=old_service.price - ) - self.session.add(deal_product_service) - - async def prefill_deal(self, user, request: DealPrefillRequest) -> DealPrefillResponse: - old_deal: models.Deal = await self.get_by_id(user, request.old_deal_id, return_raw=True) - new_deal: models.Deal = await self.get_by_id(user, request.new_deal_id, return_raw=True) - - await self._create_deal_services_for_prefilling(old_deal, new_deal) - await self._create_deal_products_for_prefilling(old_deal, new_deal) - await self.session.commit() - - return DealPrefillResponse(ok=True, message="Сделка успешно предзаполнена") - - # endregion - - # region Deal services - async def add_services(self, request: DealAddServicesRequest): - deal: Deal = await self.session.scalar( - select(Deal) - .options(selectinload(Deal.services)) - .where(Deal.id == request.deal_id) - ) - if not deal: - raise HTTPException(status_code=404, detail="Deal is not found") - - services_ids = [service.id for service in request.services] - existing_service_ids = {service.service_id for service in deal.services} - request_services_dict = {service.id: service.quantity for service in request.services} - - services_query = await self.session.scalars(select(Service).where(Service.id.in_(services_ids))) - services = services_query.all() - if len(services) != len(services_ids): - raise HTTPException(status_code=404, detail="Some of services is not found") - - # Adding quantity - for deal_service in deal.services: - deal_service: models.secondary.DealService - if deal_service.service_id not in services_ids: - continue - deal_service.quantity += request_services_dict[deal_service.service_id] - - # Adding new services - for service in services: - if service.id in existing_service_ids: - continue - quantity = request_services_dict[service.id] - deal.services.append( - models.secondary.DealService( - service_id=service.id, - deal_id=deal.id, - quantity=quantity - ) - ) - await self.session.commit() - return DealAddServicesResponse(ok=True, message='Услуги успешно добавлены') - - async def update_service_quantity( - self, - user: Union[models.User, dict], - request: DealUpdateServiceQuantityRequest - ) -> DealUpdateServiceQuantityResponse: - try: - self.grant_access(user, request.deal_id) - deal_service = await self.session.scalar( - select(models.secondary.DealService) - .where(models.secondary.DealService.deal_id == request.deal_id, - models.secondary.DealService.service_id == request.service_id) - ) - if not deal_service: - raise HTTPException(status_code=404, detail="Сделка не найдена") - deal_service.quantity = request.quantity - await self.session.commit() - return DealUpdateServiceQuantityResponse(ok=True, message='Количество успешно обновлено') - except Exception as e: - await self.session.rollback() - return DealUpdateServiceQuantityResponse(ok=False, message=str(e)) - - async def add_service(self, - user: Union[models.User, dict], - request: DealAddServiceRequest - ) -> DealAddServiceResponse: - try: - self.grant_access(user, request.deal_id) - deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) - if not deal: - raise HTTPException(status_code=404, detail="Сделка не найдена") - service: models.Service = await self.session.scalar(select(Service).where(Service.id == request.service_id)) - if not service: - raise HTTPException(status_code=404, detail="Услуга не найдена") - # Preventing duplicates - deal_service = await self.session.scalar( - select(models.secondary.DealService) - .where(models.secondary.DealService.deal_id == request.deal_id, - models.secondary.DealService.service_id == request.service_id) - ) - if deal_service: - raise HTTPException(status_code=400, detail="Услуга уже добавлена") - deal_service = models.secondary.DealService( - deal_id=request.deal_id, - service_id=request.service_id, - quantity=request.quantity, - price=request.price - ) - self.session.add(deal_service) - await self.session.commit() - return DealAddServiceResponse(ok=True, message='Услуга успешно добавлена') - except Exception as e: - await self.session.rollback() - return DealAddServiceResponse(ok=False, message=str(e)) - - async def delete_service( - self, - user: Union[models.User, dict], - request: DealDeleteServiceRequest - ) -> DealDeleteServiceResponse: - try: - self.grant_access(user, request.deal_id) - deal_service = await self.session.scalar( - select(models.secondary.DealService) - .where(models.secondary.DealService.deal_id == request.deal_id, - models.secondary.DealService.service_id == request.service_id) - ) - if not deal_service: - raise HTTPException(status_code=404, detail="Сделка не найдена") - await self.session.delete(deal_service) - await self.session.commit() - return DealDeleteServiceResponse(ok=True, message='Услуга успешно удалена') - except Exception as e: - await self.session.rollback() - return DealDeleteServiceResponse(ok=False, message=str(e)) - - async def delete_services( - self, - user: Union[models.User, dict], - request: DealDeleteServicesRequest - ) -> DealDeleteServicesResponse: - try: - self.grant_access(user, request) - deal_services = await self.session.scalars( - select(models.secondary.DealService) - .where(models.secondary.DealService.deal_id == request.deal_id, - models.secondary.DealService.service_id.in_(request.service_ids)) - ) - for deal_service in deal_services: - await self.session.delete(deal_service) - await self.session.commit() - return DealDeleteServicesResponse(ok=True, message='Услуги успешно удалены') - except Exception as e: - await self.session.rollback() - return DealDeleteServicesResponse(ok=False, message=str(e)) - - async def update_service( - self, - user: Union[models.User, dict], - request: DealUpdateServiceRequest - ) -> DealUpdateServiceResponse: - try: - self.grant_access(user, request.deal_id) - deal_service = await self.session.scalar( - select(models.secondary.DealService) - .where(models.secondary.DealService.deal_id == request.deal_id, - models.secondary.DealService.service_id == request.service.service.id) - ) - if not deal_service: - 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: - await self.session.rollback() - return DealUpdateServiceQuantityResponse(ok=False, message=str(e)) - - async def copy_services( - self, - user: Union[models.User, dict], - request: DealServicesCopyRequest - ) -> DealServicesCopyResponse: - try: - self.grant_access(user, request.deal_id) - source_services_stmt = ( - select( - models.DealProductService - ) - .where( - models.DealProductService.product_id == request.source_product_id, - models.DealProductService.deal_id == request.deal_id, - ) - ) - deal_product_services = ( - ( - await self.session.scalars( - source_services_stmt - ) - ) - .all() - ) - - destination_deal_products_stmt = ( - select( - models.DealProduct - ) - .where( - models.DealProduct.product_id.in_(request.destination_product_ids), - models.DealProduct.deal_id == request.deal_id - ) - ) - destination_deal_products = (await self.session.scalars(destination_deal_products_stmt)).all() - insert_data = [] - for deal_product in destination_deal_products: - for service in deal_product_services: - insert_data.append({ - 'deal_id': request.deal_id, - 'product_id': deal_product.product_id, - 'service_id': service.service.id, - 'price': service.price, - 'is_fixed_price': service.is_fixed_price - }) - if not insert_data: - return DealServicesCopyResponse(ok=True, message='Услуги успешно перенесены') - # Deleting previous DealProductService-s - delete_stmt = ( - delete( - models.DealProductService - ) - .where( - models.DealProductService.product_id.in_(request.destination_product_ids), - models.DealProductService.deal_id == request.deal_id - ) - ) - await self.session.execute(delete_stmt) - await self.session.flush() - - await self.session.execute( - insert(models.DealProductService), - insert_data - ) - await self.session.flush() - await self.session.commit() - return DealServicesCopyResponse(ok=True, message='Услуги успешно перенесены') - except Exception as e: - return DealServicesCopyResponse(ok=False, message=str(e)) - - @staticmethod - def get_service_price(service: models.Service, quantity: int): - price = 0 - price_ranges: list[models.ServicePriceRange] = service.price_ranges - for price_range in price_ranges: - if price_range.from_quantity <= quantity <= price_range.to_quantity: - price = price_range.price - break - - if not price and len(price_ranges) > 0: - price = price_ranges[0].price - if not price: - price = service.price - return price - - # endregion - - # region Deal products - async def update_product_quantity( - self, - user: Union[models.User, dict], - request: DealUpdateProductQuantityRequest - ) -> DealUpdateProductQuantityResponse: - try: - self.grant_access(user, request.deal_id) - # check if there is no deal or no product with different exceptions - deal_product = await self.session.scalar( - select(models.secondary.DealProduct) - .where(models.secondary.DealProduct.deal_id == request.deal_id, - models.secondary.DealProduct.product_id == request.product_id) - ) - if not deal_product: - raise HTTPException(status_code=404, detail="Сделка или товар не найдена") - deal_product.quantity = request.quantity - await self.session.commit() - return DealUpdateProductQuantityResponse(ok=True, message='Количество успешно обновлено') - except Exception as e: - await self.session.rollback() - return DealUpdateProductQuantityResponse(ok=False, message=str(e)) - - async def add_product( - self, - user: Union[models.User, dict], - - request: DealAddProductRequest - ) -> DealAddProductResponse: - try: - self.grant_access(user, request.deal_id) - - deal = await self.session.scalar(select(Deal).where(Deal.id == request.deal_id)) - if not deal: - raise HTTPException(status_code=404, detail="Сделка не найдена") - product = await self.session.scalar( - select(models.Product).where(models.Product.id == request.product.product.id)) - if not product: - raise HTTPException(status_code=404, detail="Товар не найден") - # Preventing duplicates - deal_product = await self.session.scalar( - select(models.secondary.DealProduct) - .where(models.secondary.DealProduct.deal_id == request.deal_id, - models.secondary.DealProduct.product_id == request.product.product.id) - ) - if deal_product: - raise HTTPException(status_code=400, detail="Товар уже добавлен") - deal_product = models.secondary.DealProduct( - deal_id=request.deal_id, - product_id=request.product.product.id, - quantity=request.product.quantity - ) - self.session.add(deal_product) - await self.session.flush() - for service in request.product.services: - deal_product_service = models.secondary.DealProductService( - deal_id=request.deal_id, - product_id=request.product.product.id, - service_id=service.service.id, - price=service.price - ) - self.session.add(deal_product_service) - await self.session.commit() - return DealAddProductResponse(ok=True, message='Товар успешно добавлен') - except Exception as e: - await self.session.rollback() - return DealAddProductResponse(ok=False, message=str(e)) - - async def delete_product( - self, - user: Union[models.User, dict], - request: DealDeleteProductRequest - ) -> DealDeleteProductResponse: - try: - self.grant_access(user, request.deal_id) - deal_product = await self.session.scalar( - select(models.secondary.DealProduct) - .where(models.secondary.DealProduct.deal_id == request.deal_id, - models.secondary.DealProduct.product_id == request.product_id) - ) - if not deal_product: - raise HTTPException(status_code=404, detail="Сделка не найдена") - await self.session.delete(deal_product) - await self.session.commit() - return DealDeleteProductResponse(ok=True, message='Товар успешно удален') - except Exception as e: - await self.session.rollback() - return DealDeleteProductResponse(ok=False, message=str(e)) - - async def delete_products( - self, - user: Union[models.User, dict], - request: DealDeleteProductsRequest - ) -> DealDeleteProductsResponse: - try: - self.grant_access(user, request.deal_id) - deal_products = await self.session.scalars( - select(models.secondary.DealProduct) - .where(models.secondary.DealProduct.deal_id == request.deal_id, - models.secondary.DealProduct.product_id.in_(request.product_ids)) - ) - for deal_product in deal_products: - await self.session.delete(deal_product) - await self.session.commit() - return DealDeleteProductsResponse(ok=True, message='Товары успешно удалены') - except Exception as e: - await self.session.rollback() - return DealDeleteProductsResponse(ok=False, message=str(e)) - - async def update_product( - self, - user: Union[models.User, dict], - request: DealUpdateProductRequest - ): - try: - self.grant_access(user, request.deal_id) - deal_product: models.DealProduct = await self.session.scalar( - select(models.secondary.DealProduct) - .where(models.secondary.DealProduct.deal_id == request.deal_id, - models.secondary.DealProduct.product_id == request.product.product.id) - ) - if not deal_product: - raise HTTPException(status_code=404, detail="Указанный товар не найден") - # getting new services and deleted - database_services = set([service.service_id for service in deal_product.services]) - request_services = set([service.service.id for service in request.product.services]) - - new_services = request_services.difference(database_services) - deleted_services = database_services.difference(request_services) - services_dict = {service.service.id: service for service in request.product.services} - - # Deleting and updating existing services - for service in deal_product.services: - service: models.DealProductService - if service.service_id in deleted_services: - await self.session.delete(service) - await self.session.flush() - continue - request_service = services_dict[service.service_id] - service.price = request_service.price - service.is_fixed_price = request_service.is_fixed_price - await self.session.flush() - - # Creating services - for service in request.product.services: - if service.service.id not in new_services: - continue - deal_product_service = models.DealProductService( - deal_id=request.deal_id, - product_id=request.product.product.id, - service_id=service.service.id, - price=service.price - ) - self.session.add(deal_product_service) - await self.session.flush() - - # Updating product - deal_product.quantity = request.product.quantity - deal_product.comment = request.product.comment - - # 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='Товар успешно обновлен') - - except Exception as e: - await self.session.rollback() - return DealUpdateProductResponse(ok=False, message=str(e)) - - async def add_kit_to_deal_product( - self, - user: Union[models.User, dict], - request: DealProductAddKitRequest - ) -> DealProductAddKitResponse: - try: - self.grant_access(user, request.deal_id) - service_service = ServiceService(self.session) - kit = await service_service.get_kit_by_id(request.kit_id) - if not kit: - return DealProductAddKitResponse(ok=False, message='Указанный набор услуг не найден') - services: list[models.Service] = kit.services - deal_product_stmt = ( - select( - models.DealProduct - ) - .where( - models.DealProduct.deal_id == request.deal_id, - models.DealProduct.product_id == request.product_id - ) - ) - deal_product: Optional[models.DealProduct] = await self.session.scalar(deal_product_stmt) - if not deal_product: - return DealProductAddKitResponse(ok=False, message='Указанный товар не найден') - insert_data = [] - for service in services: - service_price = self.get_service_price(service, deal_product.quantity) - insert_data.append({ - 'deal_id': request.deal_id, - 'product_id': deal_product.product_id, - 'service_id': service.id, - 'price': service_price, - }) - if not insert_data: - return DealProductAddKitResponse(ok=True, message='Набор услуг успешно добавлен к товару') - # Deleting previous DealProductService-s - delete_stmt = ( - delete( - models.DealProductService - ) - .where( - models.DealProductService.product_id == deal_product.product_id, - models.DealProductService.deal_id == request.deal_id - ) - ) - await self.session.execute(delete_stmt) - await self.session.flush() - - await self.session.execute( - insert(models.DealProductService), - insert_data - ) - await self.session.flush() - await self.session.commit() - return DealProductAddKitResponse(ok=True, message='Набор услуг успешно добавлен к товару') - except Exception as e: - return DealProductAddKitResponse(ok=False, message=str(e)) - - # endregion - - async def complete(self, user: User, request: DealCompleteRequest) -> DealCompleteResponse: - try: - # check for admin - if not user.is_admin: - return DealCompleteResponse(ok=False, message='Завершить сделку может только администратор') - - deal = await self._get_deal_by_id(request.deal_id) - if not deal: - return DealCompleteResponse(ok=False, message="Сделка не найдена") - - if deal.group: - await deal_group.DealGroupService(self.session).complete_group(deal.group.id) - else: - deal.is_completed = True - await self.session.commit() - - return DealCompleteResponse(ok=True, message="Сделка успешно завершена") - except Exception as e: - await self.session.rollback() - return DealCompleteResponse(ok=False, message=str(e)) - - async def get_quantity_dict(self, deals: List[models.Deal]): - services_quantity = defaultdict(lambda: 0) - for deal in deals: - for product in deal.products: - product: DealProduct - for service in product.services: - service: DealProductService - services_quantity[service.service_id] += product.quantity - for service in deal.services: - service: models.DealService - services_quantity[service.service_id] += service.quantity - return services_quantity - - async def _recalculate_price_single(self, deal: models.Deal, quantity_dict: dict): - services_quantity = quantity_dict - services_prices = {} - for product in deal.products: - for service in product.services: - if service.is_fixed_price: - continue - quantity = services_quantity[service.service_id] - if service.service_id in services_prices: - service.price = services_prices[service.service_id] - continue - price = self.get_service_price( - service=service.service, - quantity=quantity - ) - service.price = price - services_prices[service.service_id] = price - for service in deal.services: - service: models.DealService - if service.is_fixed_price: - continue - quantity = services_quantity[service.service_id] - price = self.get_service_price( - service=service.service, - quantity=quantity - ) - print(service.service_id, price) - service.price = price - - async def _recalculate_price_group(self, group: models.DealGroup): - deals = await self.session.scalars( - select(Deal) - .options( - selectinload(Deal.services) - .joinedload(models.DealService.service), - selectinload(Deal.products) - .selectinload(DealProduct.services) - .joinedload(DealProductService.service), - ) - .where(Deal.group == group) - ) - deals = list(deals.all()) - services_quantity = await self.get_quantity_dict(deals) - for deal in deals: - await self._recalculate_price_single(deal, services_quantity) - - async def recalculate_price(self, request: DealRecalculatePriceRequest) -> DealRecalculatePriceResponse: - try: - deal_stmt = ( - select( - Deal - ) - .options( - selectinload(Deal.services) - .joinedload(models.DealService.service), - selectinload(Deal.products) - .selectinload(DealProduct.services) - .joinedload(DealProductService.service), - joinedload(Deal.group) - ) - .where(Deal.id == request.deal_id) - ) - deal: Deal = await self.session.scalar(deal_stmt) - if not deal.group: - quantity_dict = await self.get_quantity_dict([deal]) - await self._recalculate_price_single(deal, quantity_dict) - else: - await self._recalculate_price_group(deal.group) - await self.session.commit() - return DealRecalculatePriceResponse(ok=True, message="Цены успешно пересчитаны") - except Exception as e: - return DealRecalculatePriceResponse(ok=False, message=str(e)) - - async def _assign_employee(self, deal: Deal, user: User) -> tuple[bool, str]: - assigned_employee_ids = [assignment.user_id for assignment in deal.employees] - if user.id in assigned_employee_ids: - return False, "Работник уже назначен" - - assignment = DealEmployees(user_id=user.id, deal_id=deal.id, created_at=datetime.datetime.now()) - self.session.add(assignment) - await self.session.commit() - - return True, "Работник успешно назначен" - - async def _unassign_employee(self, deal: Deal, user: User) -> tuple[bool, str]: - assigned_employee_ids = [assignment.user_id for assignment in deal.employees] - if user.id not in assigned_employee_ids: - return False, "Работник еще не назначен" - - stmt = delete(DealEmployees).where(and_(DealEmployees.user_id == user.id, DealEmployees.deal_id == deal.id)) - await self.session.execute(stmt) - await self.session.commit() - - return True, "Работник успешно удален" - - async def manage_employee(self, request: ManageEmployeeRequest) -> ManageEmployeeResponse: - deal: Optional[Deal] = await self._get_deal_by_id(request.deal_id) - if not deal: - return ManageEmployeeResponse(ok=False, message=f"Сделка с ID {request.deal_id} не найдена") - - user: Optional[User] = await self.session.get(User, request.user_id) - if not user: - return ManageEmployeeResponse(ok=False, message=f"Пользователь с ID {request.user_id} не найден") - - if request.is_assign: - ok, message = await self._assign_employee(deal, user) - else: - ok, message = await self._unassign_employee(deal, user) - - return ManageEmployeeResponse(ok=ok, message=message) - - async def get_available_employees_to_assign(self, deal_id: int) -> GetAvailableEmployeesToAssignResponse: - assigned_users = select(DealEmployees.user_id).where(DealEmployees.deal_id == deal_id) - - stmt_free_employees = ( - select(User) - .where(and_( - User.is_deleted == False, - User.role_key == "employee", - User.id.not_in(assigned_users), - )) - ) - - free_employees = (await self.session.execute(stmt_free_employees)).scalars().all() - return GetAvailableEmployeesToAssignResponse(employees=free_employees) - - async def _create_deal_from_excel( - self, - client: Client, - deal_status: DealStatus, - breakdown: CityBreakdownFromExcelSchema, - user: User, - ) -> Deal: - rank = await self._get_rank_for_deal(deal_status.id) - deal = Deal( - name=f"{client.name} - {breakdown.base_marketplace.key.upper()} - {breakdown.shipping_warehouse.name}", - created_at=datetime.datetime.now(), - current_status_id=deal_status.id, - lexorank=rank, - client_id=client.id, - base_marketplace_key=breakdown.base_marketplace.key, - shipping_warehouse_id=breakdown.shipping_warehouse.id, - ) - - self.session.add(deal) - await self.session.flush() - return deal - - async def _get_or_create_warehouse( - self, - shipping_warehouse: OptionalShippingWarehouseSchema, - ) -> OptionalShippingWarehouseSchema: - if not shipping_warehouse.id: - stmt = select(ShippingWarehouse).where(ShippingWarehouse.name == shipping_warehouse.name) - row = (await self.session.execute(stmt)).first() - warehouse_model: Optional[ShippingWarehouse] = row[0] if row else None - if warehouse_model: - shipping_warehouse.id = warehouse_model.id - else: - warehouse = await ShippingWarehouseService(self.session).create_by_name(shipping_warehouse.name) - shipping_warehouse.id = warehouse.id - - return shipping_warehouse - - async def create_deals_from_excel( - self, - request: CreateDealsFromExcelRequest, - user: User, - ) -> CreateDealsFromExcelResponse: - client: Optional[Client] = await self.session.get(Client, request.client_id) - if not client: - return CreateDealsFromExcelResponse(ok=False, message=f"Клиент с ID {request.client_id} не найден") - - deal_status: Optional[DealStatus] = await self.session.get(DealStatus, request.status_id) - if not deal_status: - return CreateDealsFromExcelResponse(ok=False, message=f"Статус с ID {request.status_id} не найден") - - deals_dict: dict[str, Deal] = {} - group = await deal_group.DealGroupService(self.session).create_group_model() - - for product_data in request.products: - for breakdown in product_data.cities_breakdown: - breakdown.shipping_warehouse = await self._get_or_create_warehouse(breakdown.shipping_warehouse) - - key = f"{breakdown.shipping_warehouse.id} - {breakdown.base_marketplace.key}" - deal = deals_dict.get(key) - if not deal: - deal = await self._create_deal_from_excel(client, deal_status, breakdown, user) - deals_dict[key] = deal - - insert_stmt = insert(deal_relations).values({ - 'deal_id': deal.id, - 'group_id': group.id - }) - await self.session.execute(insert_stmt) - - deal_product = DealProduct( - deal_id=deal.id, - product_id=product_data.product_id, - quantity=breakdown.quantity, - ) - self.session.add(deal_product) - - await self.session.commit() - return CreateDealsFromExcelResponse(ok=True, message="Сделки успешно созданы") - - async def get_deals_grouped(self, deal: models.Deal) -> List[models.Deal]: - if not deal.group: - return [deal] - deals = await self.session.scalars( - select(Deal) - .options( - selectinload(Deal.services) - .joinedload(models.DealService.service), - selectinload(Deal.products) - .selectinload(DealProduct.services) - .joinedload(DealProductService.service), - ) - .where(Deal.group == deal.group) - ) - deals = list(deals.all()) - return deals diff --git a/services/deal_group.py b/services/deal_group.py deleted file mode 100644 index c206b9f..0000000 --- a/services/deal_group.py +++ /dev/null @@ -1,168 +0,0 @@ -from lexorank import lexorank -from sqlalchemy import select, insert, update, delete -from sqlalchemy.orm import selectinload - -from models import DealService as DealServiceModel, User, Deal, DealProduct, Product, GroupBillRequest -from models.deal_group import DealGroup, deal_relations -from schemas.group import * -from services.base import BaseService -from services.deal import DealService - - -class DealGroupService(BaseService): - async def get_deals_by_group_id(self, group_id) -> list[Deal]: - group: DealGroup | None = await self.session.scalar( - select(DealGroup) - .where(DealGroup.id == group_id) - .options( - selectinload(DealGroup.deals).selectinload(Deal.products).selectinload(DealProduct.services), - selectinload(DealGroup.deals).selectinload(Deal.products) - .selectinload(DealProduct.product).selectinload(Product.barcodes), - selectinload(DealGroup.deals).selectinload(Deal.services).selectinload(DealServiceModel.service), - selectinload(DealGroup.deals).selectinload(Deal.status_history), - selectinload(DealGroup.deals).selectinload(Deal.group).selectinload(DealGroup.deals), - selectinload(DealGroup.deals).joinedload(Deal.client), - selectinload(DealGroup.deals).joinedload(Deal.shipping_warehouse), - ) - ) - return group.deals if group else [] - - async def create_group_model(self) -> DealGroup: - group = DealGroup( - name='', - lexorank=lexorank.middle(lexorank.Bucket.BUCEKT_0).__str__(), - ) - self.session.add(group) - await self.session.flush() - return group - - async def create_group(self, user: User, request: DealCreateGroupRequest) -> DealCreateGroupResponse: - try: - group = await self.create_group_model() - - for deal_id in [request.dragging_deal_id, request.hovered_deal_id]: - insert_stmt = insert(deal_relations).values({ - 'deal_id': deal_id, - 'group_id': group.id - }) - await self.session.execute(insert_stmt) - # changing status if needed on draggable deal - dragging_deal = await self.session.scalar( - select(Deal).where(Deal.id == request.dragging_deal_id) - ) - dropped_deal = await self.session.scalar( - select(Deal).where(Deal.id == request.hovered_deal_id) - ) - if dragging_deal.current_status_id != dropped_deal.current_status_id: - deal_service = DealService(self.session) - await deal_service.change_status(dragging_deal, dropped_deal.current_status_id, user) - await self.session.commit() - return DealCreateGroupResponse(ok=True, message="Группа успешно создана") - except Exception as e: - return DealCreateGroupResponse(ok=False, message=str(e)) - - async def update_group(self, request: DealGroupUpdateRequest) -> DealGroupUpdateResponse: - try: - group = await self.session.scalar( - select(DealGroup).where(DealGroup.id == request.data.id) - ) - if not group: - return DealGroupUpdateResponse(ok=False, message="Группа не найдена") - # update by dictionary - request_dict = request.data.model_dump() - request_dict.pop("bill_request", None) - - update_stmt = ( - update(DealGroup) - .where(DealGroup.id == request.data.id) - .values(**request_dict) - ) - await self.session.execute(update_stmt) - await self.session.commit() - return DealGroupUpdateResponse(ok=True, message="Группа успешно обновлена") - except Exception as e: - await self.session.rollback() - return DealGroupUpdateResponse(ok=False, message=str(e)) - - async def complete_group(self, group_id: int) -> list[Deal]: - deals = await self.get_deals_by_group_id(group_id) - for deal in deals: - deal.is_completed = True - return deals - - async def delete_group(self, group_id: int) -> None: - deals = await self.get_deals_by_group_id(group_id) - for deal in deals: - deal.is_deleted = True - await self.session.commit() - - async def change_group_status( - self, - user: User, - request: DealGroupChangeStatusRequest, - ) -> DealGroupChangeStatusResponse: - try: - # getting all deals in group - deals = await self.session.scalars( - select(deal_relations.c.deal_id) - .where(deal_relations.c.group_id == request.group_id) - ) - - deal_service = DealService(self.session) - for deal_id in deals: - deal = await self.session.scalar( - select(Deal).where(Deal.id == deal_id) - ) - await deal_service.change_status(deal, request.new_status, user) - await self.session.commit() - return DealGroupChangeStatusResponse(ok=True, message="Статус группы успешно изменен") - except Exception as e: - await self.session.rollback() - return DealGroupChangeStatusResponse(ok=False, message=str(e)) - - async def add_deal(self, user: User, request: DealAddToGroupRequest) -> DealAddToGroupResponse: - try: - group_bill_request = await self.session.get(GroupBillRequest, request.group_id) - if group_bill_request: - raise Exception("Нельзя добавить сделку, так как на группу выставлен счёт.") - - # changing status if needed - deal_id = await self.session.scalar( - select(deal_relations.c.deal_id) - .where(deal_relations.c.group_id == request.group_id) - ) - group_deal_status = await self.session.scalar( - select(Deal.current_status_id) - .where(Deal.id == deal_id) - ) - request_deal = await self.session.scalar( - select(Deal).where(Deal.id == request.deal_id) - ) - if group_deal_status != request_deal.current_status_id: - await DealService(self.session).change_status(request_deal, group_deal_status, user) - insert_stmt = insert(deal_relations).values({ - 'deal_id': request.deal_id, - 'group_id': request.group_id - }) - await self.session.execute(insert_stmt) - await self.session.commit() - - return DealAddToGroupResponse(ok=True, message="Сделка успешно добавлена в группу") - except Exception as e: - await self.session.rollback() - return DealAddToGroupResponse(ok=False, message=str(e)) - - async def remove_deal(self, request: DealRemoveFromGroupRequest) -> DealRemoveFromGroupResponse: - try: - delete_stmt = ( - delete(deal_relations) - .where( - deal_relations.c.deal_id == request.deal_id, - ) - ) - await self.session.execute(delete_stmt) - await self.session.commit() - return DealRemoveFromGroupResponse(ok=True, message="Сделка успешно удалена из группы") - except Exception as e: - await self.session.rollback() - return DealRemoveFromGroupResponse(ok=False, message=str(e)) diff --git a/services/project.py b/services/project.py index ee95100..7c77aaa 100644 --- a/services/project.py +++ b/services/project.py @@ -1,6 +1,7 @@ from datetime import datetime -from sqlalchemy import select, update, func, delete, and_ +from sqlalchemy import select, update, func, delete +from sqlalchemy.orm import selectinload from models import Project, Board from schemas.project import * @@ -9,28 +10,37 @@ from services.base import BaseService class ProjectService(BaseService): async def get_projects(self) -> GetProjectsResponse: - boards_sub = ( - select(Board) - .where(Board.is_deleted == False) + board_count_sub = ( + select( + Board.project_id, + func.count(Board.id).label('boards_count'), + ) + .group_by(Board.project_id) .subquery() ) stmt = ( select( - Project.id, - Project.name, - func.count(boards_sub.c.id) + Project, + func.coalesce(board_count_sub.c.boards_count, 0), + ) + .outerjoin(board_count_sub, Project.id == board_count_sub.c.project_id) + .options( + selectinload(Project.attributes), + selectinload(Project.modules), ) - .join(boards_sub, Project.id == boards_sub.c.project_id, isouter=True) - .where(Project.is_deleted == False) - .group_by(Project.id, Project.name) - .order_by(Project.name) ) project_data = (await self.session.execute(stmt)).all() projects = [] - for project_id, name, boards_count in project_data: - project = ProjectSchemaWithCount(id=project_id, name=name, boards_count=boards_count) - projects.append(project) + for project, boards_count in project_data: + project_schema = FullProjectSchema( + id=project.id, + name=project.name, + boards_count=boards_count, + attributes=project.attributes, + modules=project.modules, + ) + projects.append(project_schema) return GetProjectsResponse(projects=projects) diff --git a/services/service.py b/services/service.py index cc7d9d8..592702a 100644 --- a/services/service.py +++ b/services/service.py @@ -192,8 +192,8 @@ class ServiceService(BaseService): category_dict = raw_category.model_dump() del category_dict['id'] last_deal_service_rank = await self.session.scalar( - select(ServiceCategory.deal_service_rank) - .order_by(ServiceCategory.deal_service_rank.desc()) + select(ServiceCategory.card_service_rank) + .order_by(ServiceCategory.card_service_rank.desc()) .limit(1) ) last_product_service_rank = await self.session.scalar( @@ -338,7 +338,7 @@ class ServiceService(BaseService): if not request.move_down and not request.move_up: return ServiceCategoryReorderResponse(ok=False, message="Не указано действие") if request.service_type == ServiceType.DEAL_SERVICE: - order_by = ServiceCategory.deal_service_rank + order_by = ServiceCategory.card_service_rank rank_field = 'deal_service_rank' else: order_by = ServiceCategory.product_service_rank diff --git a/services/shipping.py b/services/shipping.py index 543666c..c02fd71 100644 --- a/services/shipping.py +++ b/services/shipping.py @@ -1,18 +1,18 @@ from sqlalchemy import select, and_ -from models import Deal, Pallet, Box +from models import Card, Pallet, Box from models.shipping import ShippingProduct from schemas.shipping import * from services.base import BaseService class ShippingService(BaseService): - async def create_pallet(self, deal_id: int) -> CreatePalletResponse: - deal = await self.session.get(Deal, deal_id) + async def create_pallet(self, card_id: int) -> CreatePalletResponse: + deal = await self.session.get(Card, card_id) if not deal: return CreatePalletResponse(ok=False, message="Сделка не найдена") - pallet = Pallet(deal_id=deal_id) + pallet = Pallet(card_id=card_id) self.session.add(pallet) await self.session.commit() return CreatePalletResponse(ok=True, message="Паллет успешно создан") @@ -36,19 +36,19 @@ class ShippingService(BaseService): await self.session.commit() return True, "Короб обновлен" - async def _create_box(self, data: CreateBoxInDealSchema | CreateBoxInPalletSchema): + async def _create_box(self, data: CreateBoxInCardSchema | CreateBoxInPalletSchema): box = Box(**data.model_dump()) self.session.add(box) await self.session.commit() - async def _create_box_in_deal(self, data: CreateBoxInDealSchema) -> tuple[bool, str]: - deal = await self.session.get(Deal, data.deal_id) - if not deal: - return False, f"Сделка с ID:{data.deal_id} не найдена" + async def _create_box_in_card(self, data: CreateBoxInCardSchema) -> tuple[bool, str]: + card = await self.session.get(Card, data.card_id) + if not card: + return False, f"Сделка с ID:{data.card_id} не найдена" await self._create_box(data) - return True, f"Короб для сделки ID:{data.deal_id} добавлен" + return True, f"Короб для сделки ID:{data.card_id} добавлен" async def _create_box_in_pallet(self, data: CreateBoxInPalletSchema) -> tuple[bool, str]: pallet = await self.session.get(Pallet, data.pallet_id) @@ -66,7 +66,7 @@ class ShippingService(BaseService): elif "pallet_id" in data_keys: ok, message = await self._create_box_in_pallet(CreateBoxInPalletSchema.model_validate(request.data)) else: - ok, message = await self._create_box_in_deal(CreateBoxInDealSchema.model_validate(request.data)) + ok, message = await self._create_box_in_card(CreateBoxInCardSchema.model_validate(request.data)) return UpdateBoxResponse(ok=ok, message=message) diff --git a/services/statistics/profit_statistics.py b/services/statistics/profit_statistics.py index 8955445..3a22ebc 100644 --- a/services/statistics/profit_statistics.py +++ b/services/statistics/profit_statistics.py @@ -4,7 +4,7 @@ from fastapi import HTTPException from sqlalchemy import select, and_, union_all, func, Subquery, literal from enums.profit_table_group_by import ProfitTableGroupBy -from models import DealService, Deal, DealStatusHistory, DealProductService, DealProduct, Service, Client, \ +from models import CardService, Card, CardStatusHistory, CardProductService, CardProduct, Service, Client, \ ShippingWarehouse, BaseMarketplace, User, Project, Board from schemas.statistics import GetProfitChartDataResponse, GetProfitChartDataRequest, ProfitChartDataItem, \ GetProfitTableDataResponse, GetProfitTableDataRequest, ProfitTableDataItem, CommonProfitFilters @@ -19,12 +19,12 @@ class ProfitStatisticsService(BaseService): def _get_sub_deals_created_at(date_from: datetime.date, date_to: datetime.date): deals_created_at = ( select( - Deal.id.label('deal_id'), + Card.id.label('deal_id'), func.date_trunc( 'day', - Deal.created_at, + Card.created_at, ).label('date'), - Deal.current_status_id, + Card.current_status_id, ) .subquery() ) @@ -38,22 +38,22 @@ class ProfitStatisticsService(BaseService): def _get_sub_status_history(): last_statuses = ( select( - DealStatusHistory.deal_id, - func.max(DealStatusHistory.changed_at).label('changed_at') + CardStatusHistory.card_id, + func.max(CardStatusHistory.changed_at).label('changed_at') ) - .group_by(DealStatusHistory.deal_id) + .group_by(CardStatusHistory.card_id) .subquery() ) return ( select( - Deal.id.label('deal_id'), + Card.id.label('deal_id'), func.date_trunc( 'day', last_statuses.c.changed_at, ).label('date'), - Deal.current_status_id, + Card.current_status_id, ) - .join(last_statuses, last_statuses.c.deal_id == Deal.id) + .join(last_statuses, last_statuses.c.deal_id == Card.id) .subquery() ) @@ -80,7 +80,7 @@ class ProfitStatisticsService(BaseService): if is_chart: data_item = ProfitChartDataItem( date=row.date.date(), - deals_count=row.deals_count, + deals_count=row.cards_count, profit=row.profit, revenue=row.revenue, expenses=row.expenses, @@ -88,7 +88,7 @@ class ProfitStatisticsService(BaseService): else: data_item = ProfitTableDataItem( grouped_value=row.date.date(), - deals_count=row.deals_count, + deals_count=row.cards_count, profit=row.profit, revenue=row.revenue, expenses=row.expenses, @@ -100,25 +100,25 @@ class ProfitStatisticsService(BaseService): def _get_stmt_deal_services(self, sub_filtered_status_history: Subquery): return ( select( - Deal.id.label("deal_id"), + Card.id.label("deal_id"), func.date_trunc( "day", sub_filtered_status_history.c.date, ).label("date"), - func.sum(DealService.price * DealService.quantity).label("revenue"), - func.sum(DealService.price * DealService.quantity).label("profit"), + func.sum(CardService.price * CardService.quantity).label("revenue"), + func.sum(CardService.price * CardService.quantity).label("profit"), ) - .join(DealService, Deal.id == DealService.deal_id) - .join(Service, DealService.service_id == Service.id) - .join(sub_filtered_status_history, Deal.id == sub_filtered_status_history.c.deal_id) + .join(CardService, Card.id == CardService.card_id) + .join(Service, CardService.service_id == Service.id) + .join(sub_filtered_status_history, Card.id == sub_filtered_status_history.c.deal_id) .where( and_( - Deal.is_deleted == False, - Deal.is_accounted == True, - Deal.is_completed == True if self.is_completed_only else True + Card.is_deleted == False, + Card.is_accounted == True, + Card.is_completed == True if self.is_completed_only else True ) ) - .group_by(Deal.id, "date") + .group_by(Card.id, "date") ) @staticmethod @@ -127,18 +127,18 @@ class ProfitStatisticsService(BaseService): select(Board.id) .where(Board.project_id == project_id) ) - return stmt.where(Deal.board_id.in_(board_ids_stmt)) + return stmt.where(Card.board_id.in_(board_ids_stmt)) @staticmethod def _apply_filters(request: CommonProfitFilters, stmt_deal_services, stmt_deal_product_services): if request.client_id != -1: - stmt_deal_services = stmt_deal_services.where(Deal.client_id == request.client_id) - stmt_deal_product_services = stmt_deal_product_services.where(Deal.client_id == request.client_id) + stmt_deal_services = stmt_deal_services.where(Card.client_id == request.client_id) + stmt_deal_product_services = stmt_deal_product_services.where(Card.client_id == request.client_id) if request.base_marketplace_key != "all": - stmt_deal_services = stmt_deal_services.where(Deal.base_marketplace_key == request.base_marketplace_key) + stmt_deal_services = stmt_deal_services.where(Card.base_marketplace_key == request.base_marketplace_key) stmt_deal_product_services = stmt_deal_product_services.where( - Deal.base_marketplace_key == request.base_marketplace_key) + Card.base_marketplace_key == request.base_marketplace_key) if request.project_id != -1: stmt_deal_services = ProfitStatisticsService._board_ids_for_project(request.project_id, stmt_deal_services) @@ -148,44 +148,44 @@ class ProfitStatisticsService(BaseService): ) if request.board_id != -1: - stmt_deal_services = stmt_deal_services.where(Deal.board_id == request.board_id) - stmt_deal_product_services = stmt_deal_product_services.where(Deal.board_id == request.board_id) + stmt_deal_services = stmt_deal_services.where(Card.board_id == request.board_id) + stmt_deal_product_services = stmt_deal_product_services.where(Card.board_id == request.board_id) - if request.deal_status_id != -1: - stmt_deal_services = stmt_deal_services.where(Deal.current_status_id == request.deal_status_id) + if request.card_status_id != -1: + stmt_deal_services = stmt_deal_services.where(Card.current_status_id == request.card_status_id) stmt_deal_product_services = stmt_deal_product_services.where( - Deal.current_status_id == request.deal_status_id) + Card.current_status_id == request.card_status_id) if request.manager_id != -1: - stmt_deal_services = stmt_deal_services.where(Deal.manager_id == request.manager_id) - stmt_deal_product_services = stmt_deal_product_services.where(Deal.manager_id == request.manager_id) + stmt_deal_services = stmt_deal_services.where(Card.manager_id == request.manager_id) + stmt_deal_product_services = stmt_deal_product_services.where(Card.manager_id == request.manager_id) return stmt_deal_services, stmt_deal_product_services def _get_stmt_product_services(self): return ( select( - Deal.id.label("deal_id"), - func.sum(DealProductService.price * DealProduct.quantity).label("revenue"), - func.sum(DealProductService.price * DealProduct.quantity).label("profit"), + Card.id.label("deal_id"), + func.sum(CardProductService.price * CardProduct.quantity).label("revenue"), + func.sum(CardProductService.price * CardProduct.quantity).label("profit"), ) - .join(DealProduct, Deal.id == DealProduct.deal_id) + .join(CardProduct, Card.id == CardProduct.card_id) .join( - DealProductService, + CardProductService, and_( - DealProductService.deal_id == Deal.id, - DealProductService.product_id == DealProduct.product_id, + CardProductService.card_id == Card.id, + CardProductService.product_id == CardProduct.product_id, ) ) - .join(Service, DealProductService.service_id == Service.id) + .join(Service, CardProductService.service_id == Service.id) .where( and_( - Deal.is_deleted == False, - Deal.is_accounted == True, - Deal.is_completed == True if self.is_completed_only else True, + Card.is_deleted == False, + Card.is_accounted == True, + Card.is_completed == True if self.is_completed_only else True, ) ) - .group_by(Deal.id) + .group_by(Card.id) ) @staticmethod @@ -208,7 +208,7 @@ class ProfitStatisticsService(BaseService): deals = ( select( stmt.c.date, - func.count(stmt.c.deal_id).label("deals_count"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) @@ -246,12 +246,12 @@ class ProfitStatisticsService(BaseService): select( Client.id, Client.name.label("grouped_value"), - func.count(stmt.c.deal_id).label("deals_count"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) - .join(Deal, Deal.id == stmt.c.deal_id) - .join(Client, Client.id == Deal.client_id) + .join(Card, Card.id == stmt.c.card_id) + .join(Client, Client.id == Card.client_id) .group_by(Client.id, Client.name) ) @@ -261,12 +261,12 @@ class ProfitStatisticsService(BaseService): select( Project.id, Project.name.label("grouped_value"), - func.count(stmt.c.deal_id).label("deals_count"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) - .join(Deal, Deal.id == stmt.c.deal_id) - .join(Board, Board.id == Deal.board_id) + .join(Card, Card.id == stmt.c.card_id) + .join(Board, Board.id == Card.board_id) .join(Project, Project.id == Board.project_id) .group_by(Project.id, Project.name) ) @@ -277,12 +277,12 @@ class ProfitStatisticsService(BaseService): select( Board.id, Board.name.label("grouped_value"), - func.count(stmt.c.deal_id).label("deals_count"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) - .join(Deal, Deal.id == stmt.c.deal_id) - .join(Board, Board.id == Deal.board_id) + .join(Card, Card.id == stmt.c.card_id) + .join(Board, Board.id == Card.board_id) .group_by(Board.id, Board.name) ) @@ -290,13 +290,13 @@ class ProfitStatisticsService(BaseService): def _join_and_group_by_statuses(stmt): return ( select( - Deal.current_status_id.label("grouped_value"), - func.count(stmt.c.deal_id).label("deals_count"), + Card.current_status_id.label("grouped_value"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) - .join(Deal, Deal.id == stmt.c.deal_id) - .group_by(Deal.current_status_id) + .join(Card, Card.id == stmt.c.card_id) + .group_by(Card.current_status_id) ) @staticmethod @@ -306,12 +306,12 @@ class ProfitStatisticsService(BaseService): ShippingWarehouse.id, ShippingWarehouse.name.label("grouped_value"), ShippingWarehouse.is_deleted, - func.count(stmt.c.deal_id).label("deals_count"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) - .join(Deal, Deal.id == stmt.c.deal_id) - .join(ShippingWarehouse, Deal.shipping_warehouse_id == ShippingWarehouse.id) + .join(Card, Card.id == stmt.c.card_id) + .join(ShippingWarehouse, Card.shipping_warehouse_id == ShippingWarehouse.id) .where(ShippingWarehouse.is_deleted == False) .group_by(ShippingWarehouse.is_deleted, ShippingWarehouse.id, ShippingWarehouse.name) ) @@ -321,12 +321,12 @@ class ProfitStatisticsService(BaseService): return ( select( BaseMarketplace.name.label("grouped_value"), - func.count(stmt.c.deal_id).label("deals_count"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) - .join(Deal, Deal.id == stmt.c.deal_id) - .join(BaseMarketplace, Deal.base_marketplace_key == BaseMarketplace.key) + .join(Card, Card.id == stmt.c.card_id) + .join(BaseMarketplace, Card.base_marketplace_key == BaseMarketplace.key) .group_by(BaseMarketplace.name) ) @@ -341,12 +341,12 @@ class ProfitStatisticsService(BaseService): select( managers.c.id, (managers.c.first_name + " " + managers.c.second_name).label("grouped_value"), - func.count(stmt.c.deal_id).label("deals_count"), + func.count(stmt.c.card_id).label("deals_count"), func.sum(stmt.c.revenue).label("revenue"), func.sum(stmt.c.profit).label("profit"), ) - .join(Deal, Deal.id == stmt.c.deal_id) - .join(managers, managers.c.id == Deal.manager_id) + .join(Card, Card.id == stmt.c.card_id) + .join(managers, managers.c.id == Card.manager_id) .group_by(managers.c.id, "grouped_value") ) @@ -384,7 +384,7 @@ class ProfitStatisticsService(BaseService): self.is_completed_only = request.is_completed_only self.filters = request - sub_deals_dates = self._get_deals_dates(request.deal_status_id) + sub_deals_dates = self._get_deals_dates(request.card_status_id) stmt_deal_services = self._get_stmt_deal_services(sub_deals_dates) stmt_deal_product_services = self._get_stmt_product_services() @@ -425,7 +425,7 @@ class ProfitStatisticsService(BaseService): self.date_from, self.date_to = request.date_range self.filters = request - sub_deals_dates = self._get_deals_dates(request.deal_status_id) + sub_deals_dates = self._get_deals_dates(request.card_status_id) stmt_deal_services = self._get_stmt_deal_services(sub_deals_dates) @@ -436,12 +436,12 @@ class ProfitStatisticsService(BaseService): stmt_deal_product_services.c.deal_id, func.date_trunc( "day", - Deal.created_at + Card.created_at ).label("date"), stmt_deal_product_services.c.revenue.label("revenue"), stmt_deal_product_services.c.profit.label("profit"), ) - .join(Deal, Deal.id == stmt_deal_product_services.c.deal_id) + .join(Card, Card.id == stmt_deal_product_services.c.deal_id) ) stmt_deal_services, stmt_deal_product_services = self._apply_filters( diff --git a/services/status.py b/services/status.py index 760b484..c5db979 100644 --- a/services/status.py +++ b/services/status.py @@ -2,30 +2,30 @@ from typing import Optional from sqlalchemy import select, and_, func -from models import DealStatus, Deal +from models import CardStatus, Card from schemas.status import * from services.base import BaseService class StatusService(BaseService): - async def _get_statuses_for_board(self, board_id: int) -> list[DealStatus]: + async def _get_statuses_for_board(self, board_id: int) -> list[CardStatus]: stmt = ( - select(DealStatus) + select(CardStatus) .where( and_( - DealStatus.board_id == board_id, - DealStatus.is_deleted == False, + CardStatus.board_id == board_id, + CardStatus.is_deleted == False, ) ) - .order_by(DealStatus.ordinal_number) + .order_by(CardStatus.ordinal_number) ) statuses = (await self.session.scalars(stmt)).all() return list(statuses) - async def _get_status_by_id(self, status_id: int) -> Optional[DealStatus]: + async def _get_status_by_id(self, status_id: int) -> Optional[CardStatus]: stmt = ( - select(DealStatus) - .where(DealStatus.id == status_id) + select(CardStatus) + .where(CardStatus.id == status_id) ) status = await self.session.scalar(stmt) return status @@ -38,7 +38,7 @@ class StatusService(BaseService): statuses[-1].is_finishing = False ordinal_number = statuses[-1].ordinal_number + 1 - status = DealStatus( + status = CardStatus( **request.status.model_dump(), ordinal_number=ordinal_number, is_finishing=True, @@ -81,12 +81,12 @@ class StatusService(BaseService): async def _count_deals_in_progress(self, status_id: int) -> int: stmt = ( - select(func.count(Deal.id)) + select(func.count(Card.id)) .where( and_( - Deal.current_status_id == status_id, - Deal.is_deleted == False, - Deal.is_completed == False, + Card.current_status_id == status_id, + Card.is_deleted == False, + Card.is_completed == False, ) ) ) @@ -94,12 +94,12 @@ class StatusService(BaseService): async def _count_deals(self, status_id: int) -> int: stmt = ( - select(func.count(Deal.id)) - .where(Deal.current_status_id == status_id) + select(func.count(Card.id)) + .where(Card.current_status_id == status_id) ) return (await self.session.scalars(stmt)).first() - async def _set_finishing_flag_to_prev_status(self, status: DealStatus): + async def _set_finishing_flag_to_prev_status(self, status: CardStatus): statuses = await self._get_statuses_for_board(status.board_id) if len(statuses) < 2: return