feat: pallets and boxes for deals
This commit is contained in:
		@@ -12,6 +12,7 @@ ENV.globals['now'] = datetime.now
 | 
			
		||||
ENV.globals['encode128'] = encode128
 | 
			
		||||
ENV.globals['format_number'] = lambda x: '{:,}'.format(x).replace(',', ' ')
 | 
			
		||||
 | 
			
		||||
DOMAIN_NAME = "crm.denco.store"
 | 
			
		||||
API_ROOT = "/api"
 | 
			
		||||
 | 
			
		||||
APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,65 @@
 | 
			
		||||
import os
 | 
			
		||||
 | 
			
		||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
 | 
			
		||||
from reportlab.lib.units import mm
 | 
			
		||||
from reportlab.pdfbase import pdfmetrics
 | 
			
		||||
from reportlab.pdfbase.ttfonts import TTFont
 | 
			
		||||
from reportlab.platypus import SimpleDocTemplate
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncSession
 | 
			
		||||
 | 
			
		||||
from constants import APP_PATH
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BasePdfCardGenerator:
 | 
			
		||||
    def __init__(self, session: AsyncSession):
 | 
			
		||||
        self._session = session
 | 
			
		||||
        assets_folder = os.path.join(APP_PATH, 'assets')
 | 
			
		||||
        fonts_folder = os.path.join(assets_folder, 'fonts')
 | 
			
		||||
        font_file_path = os.path.join(fonts_folder, 'DejaVuSans.ttf')
 | 
			
		||||
        self.page_width = 58 * mm
 | 
			
		||||
        self.page_height = 40 * mm
 | 
			
		||||
        pdfmetrics.registerFont(TTFont('DejaVuSans', font_file_path))
 | 
			
		||||
 | 
			
		||||
        self.styles = getSampleStyleSheet()
 | 
			
		||||
        self._set_small_paragraph_styles()
 | 
			
		||||
        self._set_medium_paragraph_styles()
 | 
			
		||||
 | 
			
		||||
    def _set_small_paragraph_styles(self):
 | 
			
		||||
        common_paragraph_style = {
 | 
			
		||||
            "parent": self.styles['Normal'],
 | 
			
		||||
            "fontName": "DejaVuSans",
 | 
			
		||||
            "spaceAfter": 4,
 | 
			
		||||
            "fontSize": 9,
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        self.small_style = ParagraphStyle(
 | 
			
		||||
            'Small',
 | 
			
		||||
            alignment=0,
 | 
			
		||||
            **common_paragraph_style,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        self.small_centered_style = ParagraphStyle(
 | 
			
		||||
            'SmallCentered',
 | 
			
		||||
            alignment=1,
 | 
			
		||||
            **common_paragraph_style,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _set_medium_paragraph_styles(self):
 | 
			
		||||
        self.medium_style = ParagraphStyle(
 | 
			
		||||
            'Medium',
 | 
			
		||||
            parent=self.styles['Normal'],
 | 
			
		||||
            fontName="DejaVuSans",
 | 
			
		||||
            spaceAfter=6,
 | 
			
		||||
            fontSize=12,
 | 
			
		||||
            alignment=0,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _create_doc(self, buffer):
 | 
			
		||||
        return SimpleDocTemplate(
 | 
			
		||||
            buffer,
 | 
			
		||||
            pagesize=(self.page_width, self.page_height),
 | 
			
		||||
            rightMargin=1,
 | 
			
		||||
            leftMargin=1,
 | 
			
		||||
            topMargin=1,
 | 
			
		||||
            bottomMargin=1
 | 
			
		||||
        )
 | 
			
		||||
							
								
								
									
										1
									
								
								generators/shipping_qr_code_generator/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								generators/shipping_qr_code_generator/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
from .generator import ShippingQRCodeGenerator
 | 
			
		||||
							
								
								
									
										139
									
								
								generators/shipping_qr_code_generator/generator.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										139
									
								
								generators/shipping_qr_code_generator/generator.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,139 @@
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from fastapi import HTTPException
 | 
			
		||||
from reportlab.lib.units import mm
 | 
			
		||||
from reportlab.pdfgen.canvas import Canvas
 | 
			
		||||
from reportlab.platypus import Paragraph, SimpleDocTemplate, Frame, PageBreak
 | 
			
		||||
from reportlab_qrcode import QRCodeImage
 | 
			
		||||
from sqlalchemy import select, func
 | 
			
		||||
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.shipping import Box
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShippingQRCodeGenerator(BasePdfCardGenerator):
 | 
			
		||||
    async def _get_deal_by_id(self, deal_id: int) -> Optional[Deal]:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(Deal)
 | 
			
		||||
            .where(Deal.id == deal_id)
 | 
			
		||||
            .options(
 | 
			
		||||
                joinedload(Deal.shipping_warehouse),
 | 
			
		||||
                selectinload(Deal.pallets),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        deal = (await self._session.execute(stmt)).one_or_none()
 | 
			
		||||
        return deal[0] if deal else None
 | 
			
		||||
 | 
			
		||||
    async def generate_deal(self, deal_id: int) -> BytesIO:
 | 
			
		||||
        deal = await self._get_deal_by_id(deal_id)
 | 
			
		||||
        if not deal:
 | 
			
		||||
            raise HTTPException(status_code=404, detail=f"Сделка с ID {deal_id}a не найдена")
 | 
			
		||||
 | 
			
		||||
        buffer = BytesIO()
 | 
			
		||||
        doc: SimpleDocTemplate = self._create_doc(buffer)
 | 
			
		||||
 | 
			
		||||
        deal_link = f"{DOMAIN_NAME}/deals/{deal_id}"
 | 
			
		||||
        shipping_warehouse = await self._session.get(ShippingWarehouse, deal.shipping_warehouse_id)
 | 
			
		||||
        warehouse_name = shipping_warehouse.name if shipping_warehouse else ""
 | 
			
		||||
 | 
			
		||||
        def on_first_page(canvas: Canvas, doc):
 | 
			
		||||
            qr = QRCodeImage(deal_link, size=30 * mm)
 | 
			
		||||
            qr.drawOn(canvas, 0, 30)
 | 
			
		||||
 | 
			
		||||
            deal_id_paragraph = Paragraph(f"ID: {deal_id}", self.small_centered_style)
 | 
			
		||||
            deal_name_paragraph = Paragraph(str(deal.name), self.small_centered_style)
 | 
			
		||||
 | 
			
		||||
            frame = Frame(x1=28 * mm, y1=5 * mm, width=30 * mm, height=30 * mm)
 | 
			
		||||
            frame.addFromList([deal_id_paragraph, deal_name_paragraph], canvas)
 | 
			
		||||
 | 
			
		||||
            warehouse_paragraph = Paragraph(warehouse_name, self.small_centered_style)
 | 
			
		||||
            frame = Frame(x1=0 * mm, y1=-7 * mm, width=58 * mm, height=20 * mm)
 | 
			
		||||
            frame.addFromList([warehouse_paragraph], canvas)
 | 
			
		||||
 | 
			
		||||
        empty_paragraph = Paragraph("", self.small_centered_style)
 | 
			
		||||
        elements = [empty_paragraph]
 | 
			
		||||
        doc.build(elements, on_first_page)
 | 
			
		||||
 | 
			
		||||
        buffer.seek(0)
 | 
			
		||||
        return buffer
 | 
			
		||||
 | 
			
		||||
    async def generate_pallets(self, deal_id: int):
 | 
			
		||||
        deal = await self._get_deal_by_id(deal_id)
 | 
			
		||||
        if not deal:
 | 
			
		||||
            raise HTTPException(status_code=404, detail=f"Сделка с ID {deal_id}a не найдена")
 | 
			
		||||
 | 
			
		||||
        buffer = BytesIO()
 | 
			
		||||
        doc: SimpleDocTemplate = self._create_doc(buffer)
 | 
			
		||||
 | 
			
		||||
        shipping_warehouse = await self._session.get(ShippingWarehouse, deal.shipping_warehouse_id)
 | 
			
		||||
        warehouse_name = shipping_warehouse.name if shipping_warehouse else ""
 | 
			
		||||
 | 
			
		||||
        total_pallets = len(deal.pallets)
 | 
			
		||||
        elements = []
 | 
			
		||||
 | 
			
		||||
        for pallet_counter in range(total_pallets):
 | 
			
		||||
            elements.append(Paragraph(f"ID: {deal_id}", self.medium_style))
 | 
			
		||||
            elements.append(Paragraph(str(deal.name), self.medium_style))
 | 
			
		||||
            elements.append(Paragraph(f"Паллет {pallet_counter + 1}/{total_pallets}", self.medium_style))
 | 
			
		||||
            elements.append(Paragraph(warehouse_name, self.medium_style))
 | 
			
		||||
            elements.append(PageBreak())
 | 
			
		||||
 | 
			
		||||
        doc.build(elements)
 | 
			
		||||
 | 
			
		||||
        buffer.seek(0)
 | 
			
		||||
        return buffer
 | 
			
		||||
 | 
			
		||||
    async def _get_boxes_on_pallets_count(self, deal_id):
 | 
			
		||||
        stmt_boxes_on_pallets = (
 | 
			
		||||
            select(
 | 
			
		||||
                Pallet.id,
 | 
			
		||||
                func.count(Box.id).label("box_count"),
 | 
			
		||||
            )
 | 
			
		||||
            .join(Box, isouter=True)
 | 
			
		||||
            .where(Pallet.deal_id == deal_id)
 | 
			
		||||
            .group_by(Pallet.id)
 | 
			
		||||
        )
 | 
			
		||||
        pallets = (await self._session.execute(stmt_boxes_on_pallets)).all()
 | 
			
		||||
        return pallets
 | 
			
		||||
 | 
			
		||||
    async def generate_boxes(self, deal_id: int) -> BytesIO:
 | 
			
		||||
        deal = await self._get_deal_by_id(deal_id)
 | 
			
		||||
        if not deal:
 | 
			
		||||
            raise HTTPException(status_code=404, detail=f"Сделка с ID {deal_id}a не найдена")
 | 
			
		||||
 | 
			
		||||
        shipping_warehouse = await self._session.get(ShippingWarehouse, deal.shipping_warehouse_id)
 | 
			
		||||
        warehouse_name = shipping_warehouse.name if shipping_warehouse else ""
 | 
			
		||||
 | 
			
		||||
        buffer = BytesIO()
 | 
			
		||||
        doc: SimpleDocTemplate = self._create_doc(buffer)
 | 
			
		||||
 | 
			
		||||
        elements = []
 | 
			
		||||
 | 
			
		||||
        total_pallets = len(deal.pallets)
 | 
			
		||||
        boxes_on_pallets = await self._get_boxes_on_pallets_count(deal_id)
 | 
			
		||||
        boxes_without_pallets = len(deal.boxes)
 | 
			
		||||
 | 
			
		||||
        for box_on_pallet in range(boxes_without_pallets):
 | 
			
		||||
            elements.append(Paragraph(f"ID: {deal_id}", self.medium_style))
 | 
			
		||||
            elements.append(Paragraph(str(deal.name), self.medium_style))
 | 
			
		||||
            elements.append(Paragraph(f"Короб {box_on_pallet + 1}/{boxes_without_pallets}", self.medium_style))
 | 
			
		||||
            elements.append(Paragraph(warehouse_name, self.medium_style))
 | 
			
		||||
            elements.append(PageBreak())
 | 
			
		||||
 | 
			
		||||
        for pallet_idx, [_, box_count] in enumerate(boxes_on_pallets):
 | 
			
		||||
            for box_on_pallet in range(box_count):
 | 
			
		||||
                elements.append(Paragraph(f"ID: {deal_id}", self.medium_style))
 | 
			
		||||
                elements.append(Paragraph(str(deal.name), self.medium_style))
 | 
			
		||||
                box_label = f"Паллет {pallet_idx + 1}/{total_pallets}, Короб {box_on_pallet + 1}/{box_count}"
 | 
			
		||||
                elements.append(Paragraph(box_label, self.medium_style))
 | 
			
		||||
                elements.append(Paragraph(warehouse_name, self.medium_style))
 | 
			
		||||
                elements.append(PageBreak())
 | 
			
		||||
 | 
			
		||||
        doc.build(elements)
 | 
			
		||||
 | 
			
		||||
        buffer.seek(0)
 | 
			
		||||
        return buffer
 | 
			
		||||
@@ -1,52 +1,16 @@
 | 
			
		||||
import os
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
 | 
			
		||||
from reportlab.pdfgen.canvas import Canvas
 | 
			
		||||
from reportlab_qrcode import QRCodeImage
 | 
			
		||||
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet
 | 
			
		||||
from reportlab.lib.units import mm
 | 
			
		||||
from reportlab.pdfbase import pdfmetrics
 | 
			
		||||
from reportlab.pdfbase.ttfonts import TTFont
 | 
			
		||||
from reportlab.platypus import SimpleDocTemplate, Paragraph
 | 
			
		||||
from sqlalchemy.ext.asyncio import AsyncSession
 | 
			
		||||
from reportlab.pdfgen.canvas import Canvas
 | 
			
		||||
from reportlab.platypus import Paragraph
 | 
			
		||||
from reportlab_qrcode import QRCodeImage
 | 
			
		||||
 | 
			
		||||
from constants import APP_PATH
 | 
			
		||||
from generators.base_pdf_card_generator.base_pdf_card_generator import BasePdfCardGenerator
 | 
			
		||||
from models import User
 | 
			
		||||
from services.user import UserService
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class WorkShiftsQRCodeGenerator:
 | 
			
		||||
    def __init__(self, session: AsyncSession):
 | 
			
		||||
        self._session = session
 | 
			
		||||
        assets_folder = os.path.join(APP_PATH, 'assets')
 | 
			
		||||
        fonts_folder = os.path.join(assets_folder, 'fonts')
 | 
			
		||||
        font_file_path = os.path.join(fonts_folder, 'DejaVuSans.ttf')
 | 
			
		||||
        self.page_width = 58 * mm
 | 
			
		||||
        self.page_height = 40 * mm
 | 
			
		||||
        pdfmetrics.registerFont(TTFont('DejaVuSans', font_file_path))
 | 
			
		||||
 | 
			
		||||
        styles = getSampleStyleSheet()
 | 
			
		||||
        self.small_style = ParagraphStyle(
 | 
			
		||||
            'Small',
 | 
			
		||||
            parent=styles['Normal'],
 | 
			
		||||
            alignment=1,
 | 
			
		||||
            fontName='DejaVuSans',  # Specify the new font
 | 
			
		||||
            fontSize=9,
 | 
			
		||||
            leading=12,
 | 
			
		||||
            spaceAfter=2,
 | 
			
		||||
            rightIndent=2,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _create_doc(self, buffer):
 | 
			
		||||
        return SimpleDocTemplate(
 | 
			
		||||
            buffer,
 | 
			
		||||
            pagesize=(self.page_width, self.page_height),
 | 
			
		||||
            rightMargin=1,
 | 
			
		||||
            leftMargin=1,
 | 
			
		||||
            topMargin=1,
 | 
			
		||||
            bottomMargin=1
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
class WorkShiftsQRCodeGenerator(BasePdfCardGenerator):
 | 
			
		||||
    async def generate(self, user_id: int) -> BytesIO:
 | 
			
		||||
        buffer = BytesIO()
 | 
			
		||||
        doc = self._create_doc(buffer)
 | 
			
		||||
@@ -59,7 +23,7 @@ class WorkShiftsQRCodeGenerator:
 | 
			
		||||
        position = user.position.name if user.position else ""
 | 
			
		||||
        user_info = Paragraph(
 | 
			
		||||
            f"{user.first_name} {user.second_name}\n{position}",
 | 
			
		||||
            self.small_style
 | 
			
		||||
            self.small_centered_style
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        doc.build([user_info], on_first_page)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								main.py
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								main.py
									
									
									
									
									
								
							@@ -48,6 +48,7 @@ routers_list = [
 | 
			
		||||
    routers.statistics_router,
 | 
			
		||||
    routers.work_shifts_router,
 | 
			
		||||
    routers.expense_router,
 | 
			
		||||
    routers.shipping_router,
 | 
			
		||||
]
 | 
			
		||||
for router in routers_list:
 | 
			
		||||
    app.include_router(router)
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@ 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:
 | 
			
		||||
@@ -104,6 +105,9 @@ class Deal(BaseModel):
 | 
			
		||||
    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')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DealStatusHistory(BaseModel):
 | 
			
		||||
    __tablename__ = 'deals_status_history'
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								models/shipping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								models/shipping.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
from typing import TYPE_CHECKING, Optional
 | 
			
		||||
 | 
			
		||||
from sqlalchemy import ForeignKey
 | 
			
		||||
from sqlalchemy.orm import Mapped, mapped_column, relationship
 | 
			
		||||
from models import BaseModel
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from models import Deal, Product
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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')
 | 
			
		||||
 | 
			
		||||
    boxes: Mapped[list['Box']] = relationship(
 | 
			
		||||
        back_populates='pallet',
 | 
			
		||||
        uselist=True,
 | 
			
		||||
        lazy='joined',
 | 
			
		||||
        cascade='all, delete-orphan',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    shipping_products: Mapped[list['ShippingProduct']] = relationship(
 | 
			
		||||
        back_populates='pallet',
 | 
			
		||||
        uselist=True,
 | 
			
		||||
        lazy='joined',
 | 
			
		||||
        cascade='all, delete-orphan',
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShippingProduct(BaseModel):
 | 
			
		||||
    __tablename__ = 'shipping_products'
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
    quantity: Mapped[int] = mapped_column()
 | 
			
		||||
 | 
			
		||||
    product_id: Mapped[int] = mapped_column(ForeignKey('products.id'))
 | 
			
		||||
    product: Mapped['Product'] = relationship(lazy='joined')
 | 
			
		||||
 | 
			
		||||
    pallet_id: Mapped[int] = mapped_column(ForeignKey('pallets.id'))
 | 
			
		||||
    pallet: Mapped['Pallet'] = relationship(lazy='joined')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Box(BaseModel):
 | 
			
		||||
    __tablename__ = 'boxes'
 | 
			
		||||
    id: Mapped[int] = mapped_column(primary_key=True)
 | 
			
		||||
 | 
			
		||||
    quantity: Mapped[int] = mapped_column()
 | 
			
		||||
 | 
			
		||||
    product_id: Mapped[int] = mapped_column(ForeignKey('products.id'))
 | 
			
		||||
    product: Mapped['Product'] = relationship(lazy='joined')
 | 
			
		||||
 | 
			
		||||
    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')
 | 
			
		||||
@@ -16,3 +16,4 @@ from .task import task_router
 | 
			
		||||
from .work_shifts import work_shifts_router
 | 
			
		||||
from .statistics import statistics_router
 | 
			
		||||
from .expense import expense_router
 | 
			
		||||
from .shipping import shipping_router
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										128
									
								
								routers/shipping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										128
									
								
								routers/shipping.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,128 @@
 | 
			
		||||
from io import BytesIO
 | 
			
		||||
 | 
			
		||||
from fastapi import APIRouter, Depends, Response
 | 
			
		||||
 | 
			
		||||
from backend.dependecies import SessionDependency
 | 
			
		||||
from generators.shipping_qr_code_generator import ShippingQRCodeGenerator
 | 
			
		||||
from schemas.shipping import *
 | 
			
		||||
from services.auth import authorized_user
 | 
			
		||||
from services.shipping import ShippingService
 | 
			
		||||
 | 
			
		||||
shipping_router = APIRouter(
 | 
			
		||||
    prefix="/shipping",
 | 
			
		||||
    tags=["shipping"],
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.post(
 | 
			
		||||
    '/pallet/{deal_id}',
 | 
			
		||||
    response_model=CreatePalletResponse,
 | 
			
		||||
    operation_id='create_pallet',
 | 
			
		||||
    dependencies=[Depends(authorized_user)],
 | 
			
		||||
)
 | 
			
		||||
async def create_pallet(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        deal_id: int,
 | 
			
		||||
):
 | 
			
		||||
    return await ShippingService(session).create_pallet(deal_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.delete(
 | 
			
		||||
    '/pallet/{pallet_id}',
 | 
			
		||||
    response_model=DeletePalletResponse,
 | 
			
		||||
    operation_id='delete_pallet',
 | 
			
		||||
    dependencies=[Depends(authorized_user)],
 | 
			
		||||
)
 | 
			
		||||
async def delete_pallet(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        pallet_id: int,
 | 
			
		||||
):
 | 
			
		||||
    return await ShippingService(session).delete_pallet(pallet_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.post(
 | 
			
		||||
    '/product',
 | 
			
		||||
    response_model=UpdateShippingProductResponse,
 | 
			
		||||
    operation_id='update_shipping_product',
 | 
			
		||||
    dependencies=[Depends(authorized_user)],
 | 
			
		||||
)
 | 
			
		||||
async def update_shipping_product(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        request: UpdateShippingProductRequest,
 | 
			
		||||
):
 | 
			
		||||
    return await ShippingService(session).update_shipping_product(request)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.delete(
 | 
			
		||||
    '/product/{shipping_product_id}',
 | 
			
		||||
    response_model=DeleteShippingProductResponse,
 | 
			
		||||
    operation_id='delete_shipping_product',
 | 
			
		||||
    dependencies=[Depends(authorized_user)],
 | 
			
		||||
)
 | 
			
		||||
async def delete_shipping_product(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        shipping_product_id: int,
 | 
			
		||||
):
 | 
			
		||||
    return await ShippingService(session).delete_shipping_product(shipping_product_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.post(
 | 
			
		||||
    '/box',
 | 
			
		||||
    response_model=UpdateBoxResponse,
 | 
			
		||||
    operation_id='update_box',
 | 
			
		||||
    dependencies=[Depends(authorized_user)],
 | 
			
		||||
)
 | 
			
		||||
async def update_box(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        request: UpdateBoxRequest,
 | 
			
		||||
):
 | 
			
		||||
    return await ShippingService(session).update_box(request)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.delete(
 | 
			
		||||
    '/box/{box_id}',
 | 
			
		||||
    response_model=DeleteBoxResponse,
 | 
			
		||||
    operation_id='delete_box',
 | 
			
		||||
    dependencies=[Depends(authorized_user)],
 | 
			
		||||
)
 | 
			
		||||
async def delete_box(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        box_id: int,
 | 
			
		||||
):
 | 
			
		||||
    return await ShippingService(session).delete_box(box_id)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.get(
 | 
			
		||||
    "/pdf/deal/{deal_id}",
 | 
			
		||||
    operation_id="get_deal_qr_code_pdf",
 | 
			
		||||
)
 | 
			
		||||
async def generate_deal_qr_code_pdf(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        deal_id: int,
 | 
			
		||||
):
 | 
			
		||||
    pdf_file: BytesIO = await ShippingQRCodeGenerator(session).generate_deal(deal_id)
 | 
			
		||||
    return Response(pdf_file.getvalue(), media_type="application/pdf")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.get(
 | 
			
		||||
    "/pdf/pallets/{deal_id}",
 | 
			
		||||
    operation_id="get_pallets_pdf",
 | 
			
		||||
)
 | 
			
		||||
async def generate_pallets_pdf(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        deal_id: int,
 | 
			
		||||
):
 | 
			
		||||
    pdf_file: BytesIO = await ShippingQRCodeGenerator(session).generate_pallets(deal_id)
 | 
			
		||||
    return Response(pdf_file.getvalue(), media_type="application/pdf")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@shipping_router.get(
 | 
			
		||||
    "/pdf/boxes/{deal_id}",
 | 
			
		||||
    operation_id="get_boxes_pdf",
 | 
			
		||||
)
 | 
			
		||||
async def generate_boxes_pdf(
 | 
			
		||||
        session: SessionDependency,
 | 
			
		||||
        deal_id: int,
 | 
			
		||||
):
 | 
			
		||||
    pdf_file: BytesIO = await ShippingQRCodeGenerator(session).generate_boxes(deal_id)
 | 
			
		||||
    return Response(pdf_file.getvalue(), media_type="application/pdf")
 | 
			
		||||
@@ -11,6 +11,7 @@ from schemas.client import ClientSchema
 | 
			
		||||
from schemas.marketplace import BaseMarketplaceSchema
 | 
			
		||||
from schemas.product import ProductSchema
 | 
			
		||||
from schemas.service import ServiceSchema, ServicePriceCategorySchema
 | 
			
		||||
from schemas.shipping import PalletSchema, BoxSchema
 | 
			
		||||
from schemas.shipping_warehouse import ShippingWarehouseSchema
 | 
			
		||||
from schemas.user import UserSchema
 | 
			
		||||
 | 
			
		||||
@@ -102,6 +103,8 @@ class DealSchema(BaseSchema):
 | 
			
		||||
    category: Optional[ServicePriceCategorySchema] = None
 | 
			
		||||
    group: Optional[DealGroupSchema] = None
 | 
			
		||||
    manager: Optional[UserSchema] = None
 | 
			
		||||
    pallets: List[PalletSchema] = []
 | 
			
		||||
    boxes: List[BoxSchema] = []
 | 
			
		||||
 | 
			
		||||
    delivery_date: Optional[datetime.datetime] = None
 | 
			
		||||
    receiving_slot_date: Optional[datetime.datetime] = None
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										94
									
								
								schemas/shipping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								schemas/shipping.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,94 @@
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
from schemas.base import BaseSchema, OkMessageSchema
 | 
			
		||||
from schemas.product import ProductSchema
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# region Entities
 | 
			
		||||
 | 
			
		||||
class ProductAndQuantitySchema(BaseSchema):
 | 
			
		||||
    product_id: Optional[int]
 | 
			
		||||
    quantity: Optional[int]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BoxSchema(BaseSchema):
 | 
			
		||||
    id: int
 | 
			
		||||
    quantity: int
 | 
			
		||||
    product: ProductSchema
 | 
			
		||||
    pallet_id: Optional[int]
 | 
			
		||||
    deal_id: Optional[int]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ShippingProductSchema(BaseSchema):
 | 
			
		||||
    id: int
 | 
			
		||||
    quantity: int
 | 
			
		||||
    product: ProductSchema
 | 
			
		||||
    pallet_id: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class PalletSchema(BaseSchema):
 | 
			
		||||
    id: int
 | 
			
		||||
    boxes: list[BoxSchema]
 | 
			
		||||
    shipping_products: list[ShippingProductSchema]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateShippingProductSchema(ProductAndQuantitySchema):
 | 
			
		||||
    pallet_id: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateShippingProductSchema(ProductAndQuantitySchema):
 | 
			
		||||
    shipping_product_id: int
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateBoxInPalletSchema(ProductAndQuantitySchema):
 | 
			
		||||
    pallet_id: Optional[int]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class CreateBoxInDealSchema(ProductAndQuantitySchema):
 | 
			
		||||
    deal_id: Optional[int]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateBoxSchema(ProductAndQuantitySchema):
 | 
			
		||||
    box_id: Optional[int]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# endregion
 | 
			
		||||
 | 
			
		||||
# region Requests
 | 
			
		||||
 | 
			
		||||
class UpdateShippingProductRequest(BaseSchema):
 | 
			
		||||
    data: CreateShippingProductSchema | UpdateShippingProductSchema
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateBoxRequest(BaseSchema):
 | 
			
		||||
    data: CreateBoxInDealSchema | CreateBoxInPalletSchema | UpdateBoxSchema
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# endregion
 | 
			
		||||
 | 
			
		||||
# region Responses
 | 
			
		||||
 | 
			
		||||
class CreatePalletResponse(OkMessageSchema):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeletePalletResponse(OkMessageSchema):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateShippingProductResponse(OkMessageSchema):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class UpdateBoxResponse(OkMessageSchema):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteBoxResponse(OkMessageSchema):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class DeleteShippingProductResponse(OkMessageSchema):
 | 
			
		||||
    pass
 | 
			
		||||
 | 
			
		||||
# endregion
 | 
			
		||||
@@ -10,6 +10,7 @@ import models.secondary
 | 
			
		||||
from models import User, Service, Client, DealProductService, deal_relations, GroupBillRequest
 | 
			
		||||
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
 | 
			
		||||
@@ -331,7 +332,13 @@ class DealService(BaseService):
 | 
			
		||||
                .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),
 | 
			
		||||
            )
 | 
			
		||||
            .where(Deal.id == deal_id)
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										121
									
								
								services/shipping.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								services/shipping.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
from sqlalchemy import select, and_
 | 
			
		||||
 | 
			
		||||
from models import Deal, 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)
 | 
			
		||||
        if not deal:
 | 
			
		||||
            return CreatePalletResponse(ok=False, message="Сделка не найдена")
 | 
			
		||||
 | 
			
		||||
        pallet = Pallet(deal_id=deal_id)
 | 
			
		||||
        self.session.add(pallet)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return CreatePalletResponse(ok=True, message="Паллет успешно создан")
 | 
			
		||||
 | 
			
		||||
    async def delete_pallet(self, pallet_id: int) -> DeletePalletResponse:
 | 
			
		||||
        pallet = await self.session.get(Pallet, pallet_id)
 | 
			
		||||
        if not pallet:
 | 
			
		||||
            return DeletePalletResponse(ok=False, message="Паллет не найден")
 | 
			
		||||
 | 
			
		||||
        await self.session.delete(pallet)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return DeletePalletResponse(ok=True, message="Паллет успешно удален")
 | 
			
		||||
 | 
			
		||||
    async def _update_box(self, data: UpdateBoxSchema) -> tuple[bool, str]:
 | 
			
		||||
        box = await self.session.get(Box, data.box_id)
 | 
			
		||||
        if not box:
 | 
			
		||||
            return False, f"Короб с ID:{data.box_id} не найден"
 | 
			
		||||
 | 
			
		||||
        box.quantity = data.quantity
 | 
			
		||||
        box.product_id = data.product_id
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return True, "Короб обновлен"
 | 
			
		||||
 | 
			
		||||
    async def _create_box(self, data: CreateBoxInDealSchema | 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} не найдена"
 | 
			
		||||
 | 
			
		||||
        await self._create_box(data)
 | 
			
		||||
 | 
			
		||||
        return True, f"Короб для сделки ID:{data.deal_id} добавлен"
 | 
			
		||||
 | 
			
		||||
    async def _create_box_in_pallet(self, data: CreateBoxInPalletSchema) -> tuple[bool, str]:
 | 
			
		||||
        pallet = await self.session.get(Pallet, data.pallet_id)
 | 
			
		||||
        if not pallet:
 | 
			
		||||
            return False, f"Паллет с ID:{data.pallet_id} не найден"
 | 
			
		||||
 | 
			
		||||
        await self._create_box(data)
 | 
			
		||||
 | 
			
		||||
        return True, f"Короб добавлен в паллет"
 | 
			
		||||
 | 
			
		||||
    async def update_box(self, request: UpdateBoxRequest) -> UpdateBoxResponse:
 | 
			
		||||
        data_keys = request.data.model_dump().keys()
 | 
			
		||||
        if "box_id" in data_keys:
 | 
			
		||||
            ok, message = await self._update_box(request.data)
 | 
			
		||||
        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))
 | 
			
		||||
 | 
			
		||||
        return UpdateBoxResponse(ok=ok, message=message)
 | 
			
		||||
 | 
			
		||||
    async def delete_box(self, deal_id: int) -> DeleteBoxResponse:
 | 
			
		||||
        box = await self.session.get(Box, deal_id)
 | 
			
		||||
        if not box:
 | 
			
		||||
            return DeleteBoxResponse(ok=False, message=f"Короб с ID:{deal_id} не найден")
 | 
			
		||||
 | 
			
		||||
        await self.session.delete(box)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return DeleteBoxResponse(ok=True, message="Короб успешно удален")
 | 
			
		||||
 | 
			
		||||
    async def _get_shipping_product(self, pallet_id: int, product_id: int) -> Optional[ShippingProduct]:
 | 
			
		||||
        stmt = (
 | 
			
		||||
            select(ShippingProduct)
 | 
			
		||||
            .where(
 | 
			
		||||
                and_(ShippingProduct.pallet_id == pallet_id, ShippingProduct.product_id == product_id)
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
        shipping_product = (await self.session.execute(stmt)).unique().one_or_none()
 | 
			
		||||
        return shipping_product[0] if shipping_product else None
 | 
			
		||||
 | 
			
		||||
    async def _update_shipping_product(self, data: UpdateShippingProductSchema) -> tuple[bool, str]:
 | 
			
		||||
        shipping_product = await self.session.get(ShippingProduct, data.shipping_product_id)
 | 
			
		||||
        shipping_product.product_id = data.product_id
 | 
			
		||||
        shipping_product.quantity = data.quantity
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return True, "Запись о товаре на паллете успешно изменена"
 | 
			
		||||
 | 
			
		||||
    async def _create_shipping_product(self, data: CreateShippingProductSchema) -> tuple[bool, str]:
 | 
			
		||||
        shipping_product = ShippingProduct(**data.model_dump())
 | 
			
		||||
        self.session.add(shipping_product)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return True, "Запись о товаре на паллете успешно добавлена"
 | 
			
		||||
 | 
			
		||||
    async def update_shipping_product(self, request: UpdateShippingProductRequest) -> UpdateShippingProductResponse:
 | 
			
		||||
        data_keys = request.data.model_dump().keys()
 | 
			
		||||
        if "shipping_product_id" in data_keys:
 | 
			
		||||
            ok, message = await self._update_shipping_product(request.data)
 | 
			
		||||
        else:
 | 
			
		||||
            ok, message = await self._create_shipping_product(request.data)
 | 
			
		||||
 | 
			
		||||
        return UpdateShippingProductResponse(ok=ok, message=message)
 | 
			
		||||
 | 
			
		||||
    async def delete_shipping_product(self, shipping_product_id: int) -> DeleteShippingProductResponse:
 | 
			
		||||
        shipping_product = await self.session.get(ShippingProduct, shipping_product_id)
 | 
			
		||||
        if not shipping_product:
 | 
			
		||||
            return DeleteShippingProductResponse(ok=False, message=f"Запись для данного паллета и товара не найдена")
 | 
			
		||||
 | 
			
		||||
        await self.session.delete(shipping_product)
 | 
			
		||||
        await self.session.commit()
 | 
			
		||||
        return DeleteShippingProductResponse(ok=True, message="Запись о товаре на паллете успешно удалена")
 | 
			
		||||
		Reference in New Issue
	
	Block a user