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['encode128'] = encode128
|
||||||
ENV.globals['format_number'] = lambda x: '{:,}'.format(x).replace(',', ' ')
|
ENV.globals['format_number'] = lambda x: '{:,}'.format(x).replace(',', ' ')
|
||||||
|
|
||||||
|
DOMAIN_NAME = "crm.denco.store"
|
||||||
API_ROOT = "/api"
|
API_ROOT = "/api"
|
||||||
|
|
||||||
APP_PATH = os.path.dirname(sys.executable) if getattr(sys, 'frozen', False) else os.path.dirname(__file__)
|
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 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.lib.units import mm
|
||||||
from reportlab.pdfbase import pdfmetrics
|
from reportlab.pdfgen.canvas import Canvas
|
||||||
from reportlab.pdfbase.ttfonts import TTFont
|
from reportlab.platypus import Paragraph
|
||||||
from reportlab.platypus import SimpleDocTemplate, Paragraph
|
from reportlab_qrcode import QRCodeImage
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
|
||||||
|
|
||||||
from constants import APP_PATH
|
from generators.base_pdf_card_generator.base_pdf_card_generator import BasePdfCardGenerator
|
||||||
from models import User
|
from models import User
|
||||||
from services.user import UserService
|
from services.user import UserService
|
||||||
|
|
||||||
|
|
||||||
class WorkShiftsQRCodeGenerator:
|
class WorkShiftsQRCodeGenerator(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))
|
|
||||||
|
|
||||||
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
|
|
||||||
)
|
|
||||||
|
|
||||||
async def generate(self, user_id: int) -> BytesIO:
|
async def generate(self, user_id: int) -> BytesIO:
|
||||||
buffer = BytesIO()
|
buffer = BytesIO()
|
||||||
doc = self._create_doc(buffer)
|
doc = self._create_doc(buffer)
|
||||||
@@ -59,7 +23,7 @@ class WorkShiftsQRCodeGenerator:
|
|||||||
position = user.position.name if user.position else ""
|
position = user.position.name if user.position else ""
|
||||||
user_info = Paragraph(
|
user_info = Paragraph(
|
||||||
f"{user.first_name} {user.second_name}\n{position}",
|
f"{user.first_name} {user.second_name}\n{position}",
|
||||||
self.small_style
|
self.small_centered_style
|
||||||
)
|
)
|
||||||
|
|
||||||
doc.build([user_info], on_first_page)
|
doc.build([user_info], on_first_page)
|
||||||
|
|||||||
1
main.py
1
main.py
@@ -48,6 +48,7 @@ routers_list = [
|
|||||||
routers.statistics_router,
|
routers.statistics_router,
|
||||||
routers.work_shifts_router,
|
routers.work_shifts_router,
|
||||||
routers.expense_router,
|
routers.expense_router,
|
||||||
|
routers.shipping_router,
|
||||||
]
|
]
|
||||||
for router in routers_list:
|
for router in routers_list:
|
||||||
app.include_router(router)
|
app.include_router(router)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from sqlalchemy.orm import relationship, backref, Mapped, mapped_column
|
|||||||
|
|
||||||
from models.base import BaseModel
|
from models.base import BaseModel
|
||||||
from .marketplace import BaseMarketplace
|
from .marketplace import BaseMarketplace
|
||||||
|
from .shipping import Pallet, Box
|
||||||
from .shipping_warehouse import ShippingWarehouse
|
from .shipping_warehouse import ShippingWarehouse
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -104,6 +105,9 @@ class Deal(BaseModel):
|
|||||||
manager_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=True)
|
manager_id: Mapped[int] = mapped_column(ForeignKey('users.id'), nullable=True)
|
||||||
manager: Mapped[Optional["User"]] = relationship(back_populates='managed_deals', lazy='joined')
|
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):
|
class DealStatusHistory(BaseModel):
|
||||||
__tablename__ = 'deals_status_history'
|
__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')
|
||||||
@@ -15,4 +15,5 @@ from .billing import billing_router
|
|||||||
from .task import task_router
|
from .task import task_router
|
||||||
from .work_shifts import work_shifts_router
|
from .work_shifts import work_shifts_router
|
||||||
from .statistics import statistics_router
|
from .statistics import statistics_router
|
||||||
from .expense import expense_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.marketplace import BaseMarketplaceSchema
|
||||||
from schemas.product import ProductSchema
|
from schemas.product import ProductSchema
|
||||||
from schemas.service import ServiceSchema, ServicePriceCategorySchema
|
from schemas.service import ServiceSchema, ServicePriceCategorySchema
|
||||||
|
from schemas.shipping import PalletSchema, BoxSchema
|
||||||
from schemas.shipping_warehouse import ShippingWarehouseSchema
|
from schemas.shipping_warehouse import ShippingWarehouseSchema
|
||||||
from schemas.user import UserSchema
|
from schemas.user import UserSchema
|
||||||
|
|
||||||
@@ -102,6 +103,8 @@ class DealSchema(BaseSchema):
|
|||||||
category: Optional[ServicePriceCategorySchema] = None
|
category: Optional[ServicePriceCategorySchema] = None
|
||||||
group: Optional[DealGroupSchema] = None
|
group: Optional[DealGroupSchema] = None
|
||||||
manager: Optional[UserSchema] = None
|
manager: Optional[UserSchema] = None
|
||||||
|
pallets: List[PalletSchema] = []
|
||||||
|
boxes: List[BoxSchema] = []
|
||||||
|
|
||||||
delivery_date: Optional[datetime.datetime] = None
|
delivery_date: Optional[datetime.datetime] = None
|
||||||
receiving_slot_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 import User, Service, Client, DealProductService, deal_relations, GroupBillRequest
|
||||||
from models.deal import *
|
from models.deal import *
|
||||||
from models.deal_group import DealGroup
|
from models.deal_group import DealGroup
|
||||||
|
from models.shipping import ShippingProduct
|
||||||
from schemas.client import ClientDetailsSchema
|
from schemas.client import ClientDetailsSchema
|
||||||
from schemas.deal import *
|
from schemas.deal import *
|
||||||
from services.auth import AuthService
|
from services.auth import AuthService
|
||||||
@@ -331,7 +332,13 @@ class DealService(BaseService):
|
|||||||
.joinedload(DealStatusHistory.user),
|
.joinedload(DealStatusHistory.user),
|
||||||
selectinload(Deal.status_history)
|
selectinload(Deal.status_history)
|
||||||
.noload(DealStatusHistory.deal),
|
.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)
|
.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