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