234 lines
11 KiB
Python
234 lines
11 KiB
Python
from __future__ import annotations
|
||
|
||
from typing import Dict, Any, List, Optional
|
||
from decimal import Decimal
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import and_, or_
|
||
|
||
from adapters.db.models.product import Product
|
||
from adapters.db.models.price_list import PriceItem
|
||
from adapters.db.models.category import BusinessCategory
|
||
from adapters.db.models.currency import Currency
|
||
from adapters.api.v1.schema_models.product import (
|
||
BulkPriceUpdateRequest,
|
||
BulkPriceUpdatePreview,
|
||
BulkPriceUpdatePreviewResponse,
|
||
BulkPriceUpdateType,
|
||
BulkPriceUpdateTarget,
|
||
BulkPriceUpdateDirection,
|
||
ProductItemType
|
||
)
|
||
|
||
|
||
def _quantize_non_negative_integer(value: Decimal) -> Decimal:
|
||
"""رُند کردن به عدد صحیح غیرمنفی (بدون اعشار)."""
|
||
# حذف اعشار: round-half-up به نزدیکترین عدد صحیح
|
||
quantized = value.quantize(Decimal('1'))
|
||
if quantized < 0:
|
||
return Decimal('0')
|
||
return quantized
|
||
|
||
def _quantize_integer_keep_sign(value: Decimal) -> Decimal:
|
||
"""رُند کردن به عدد صحیح با حفظ علامت (بدون اعشار)."""
|
||
return value.quantize(Decimal('1'))
|
||
|
||
|
||
def calculate_new_price(current_price: Optional[Decimal], update_type: BulkPriceUpdateType, direction: BulkPriceUpdateDirection, value: Decimal) -> Optional[Decimal]:
|
||
"""محاسبه قیمت جدید بر اساس نوع تغییر با جهت، سپس رُند و کلَمپ به صفر"""
|
||
if current_price is None:
|
||
return None
|
||
|
||
delta = Decimal('0')
|
||
if update_type == BulkPriceUpdateType.PERCENTAGE:
|
||
sign = Decimal('1') if direction == BulkPriceUpdateDirection.INCREASE else Decimal('-1')
|
||
multiplier = Decimal('1') + (sign * (value / Decimal('100')))
|
||
new_value = current_price * multiplier
|
||
else:
|
||
sign = Decimal('1') if direction == BulkPriceUpdateDirection.INCREASE else Decimal('-1')
|
||
delta = sign * value
|
||
new_value = current_price + delta
|
||
|
||
# رُند به عدد صحیح و کلَمپ به صفر
|
||
return _quantize_non_negative_integer(new_value)
|
||
|
||
|
||
def get_filtered_products(db: Session, business_id: int, request: BulkPriceUpdateRequest) -> List[Product]:
|
||
"""دریافت کالاهای فیلتر شده بر اساس معیارهای درخواست"""
|
||
query = db.query(Product).filter(Product.business_id == business_id)
|
||
|
||
# فیلتر بر اساس دستهبندی
|
||
if request.category_ids:
|
||
query = query.filter(Product.category_id.in_(request.category_ids))
|
||
|
||
# فیلتر بر اساس نوع آیتم
|
||
if request.item_types:
|
||
query = query.filter(Product.item_type.in_([t.value for t in request.item_types]))
|
||
|
||
# فیلتر بر اساس ارز: محصولی که قیمتهای لیست مرتبط با ارزهای انتخابی دارد
|
||
if request.currency_ids:
|
||
query = query.filter(
|
||
db.query(PriceItem.id)
|
||
.filter(
|
||
PriceItem.product_id == Product.id,
|
||
PriceItem.currency_id.in_(request.currency_ids)
|
||
).exists()
|
||
)
|
||
|
||
# فیلتر بر اساس لیست قیمت: محصولی که در هر یک از لیستهای انتخابی آیتم قیمت دارد
|
||
if request.price_list_ids:
|
||
query = query.filter(
|
||
db.query(PriceItem.id)
|
||
.filter(
|
||
PriceItem.product_id == Product.id,
|
||
PriceItem.price_list_id.in_(request.price_list_ids)
|
||
).exists()
|
||
)
|
||
|
||
# فیلتر بر اساس شناسههای کالاهای خاص
|
||
if request.product_ids:
|
||
query = query.filter(Product.id.in_(request.product_ids))
|
||
|
||
# فیلتر بر اساس موجودی
|
||
if request.only_products_with_inventory is not None:
|
||
if request.only_products_with_inventory:
|
||
query = query.filter(Product.track_inventory == True)
|
||
else:
|
||
query = query.filter(Product.track_inventory == False)
|
||
|
||
# فیلتر بر اساس وجود قیمت پایه
|
||
if request.only_products_with_base_price:
|
||
if request.target == BulkPriceUpdateTarget.SALES_PRICE:
|
||
query = query.filter(Product.base_sales_price.isnot(None))
|
||
elif request.target == BulkPriceUpdateTarget.PURCHASE_PRICE:
|
||
query = query.filter(Product.base_purchase_price.isnot(None))
|
||
else:
|
||
# در حالت هر دو، حداقل یکی موجود باشد
|
||
query = query.filter(or_(Product.base_sales_price.isnot(None), Product.base_purchase_price.isnot(None)))
|
||
|
||
return query.all()
|
||
|
||
|
||
def preview_bulk_price_update(db: Session, business_id: int, request: BulkPriceUpdateRequest) -> BulkPriceUpdatePreviewResponse:
|
||
"""پیشنمایش تغییرات قیمت گروهی"""
|
||
products = get_filtered_products(db, business_id, request)
|
||
|
||
# کش نام دستهها برای کاهش کوئری
|
||
category_titles: Dict[int, str] = {}
|
||
def _resolve_category_name(cid: Optional[int]) -> Optional[str]:
|
||
if cid is None:
|
||
return None
|
||
if cid in category_titles:
|
||
return category_titles[cid]
|
||
try:
|
||
cat = db.query(BusinessCategory).filter(BusinessCategory.id == cid, BusinessCategory.business_id == business_id).first()
|
||
if cat and isinstance(cat.title_translations, dict):
|
||
title = cat.title_translations.get('fa') or cat.title_translations.get('default') or ''
|
||
category_titles[cid] = title
|
||
return title
|
||
except Exception:
|
||
return None
|
||
return None
|
||
|
||
affected_products = []
|
||
total_sales_change = Decimal('0')
|
||
total_purchase_change = Decimal('0')
|
||
products_with_sales_change = 0
|
||
products_with_purchase_change = 0
|
||
|
||
for product in products:
|
||
preview = BulkPriceUpdatePreview(
|
||
product_id=product.id,
|
||
product_name=product.name or "بدون نام",
|
||
product_code=product.code or "بدون کد",
|
||
category_name=_resolve_category_name(product.category_id),
|
||
current_sales_price=product.base_sales_price,
|
||
current_purchase_price=product.base_purchase_price,
|
||
new_sales_price=None,
|
||
new_purchase_price=None,
|
||
sales_price_change=None,
|
||
purchase_price_change=None
|
||
)
|
||
|
||
# محاسبه تغییرات قیمت فروش
|
||
if request.target in [BulkPriceUpdateTarget.SALES_PRICE, BulkPriceUpdateTarget.BOTH] and product.base_sales_price is not None:
|
||
new_sales_price = calculate_new_price(product.base_sales_price, request.update_type, request.direction, request.value)
|
||
preview.new_sales_price = new_sales_price
|
||
preview.sales_price_change = (new_sales_price - product.base_sales_price) if new_sales_price is not None else None
|
||
total_sales_change += (preview.sales_price_change or Decimal('0'))
|
||
products_with_sales_change += 1
|
||
|
||
# محاسبه تغییرات قیمت خرید
|
||
if request.target in [BulkPriceUpdateTarget.PURCHASE_PRICE, BulkPriceUpdateTarget.BOTH] and product.base_purchase_price is not None:
|
||
new_purchase_price = calculate_new_price(product.base_purchase_price, request.update_type, request.direction, request.value)
|
||
preview.new_purchase_price = new_purchase_price
|
||
preview.purchase_price_change = (new_purchase_price - product.base_purchase_price) if new_purchase_price is not None else None
|
||
total_purchase_change += (preview.purchase_price_change or Decimal('0'))
|
||
products_with_purchase_change += 1
|
||
|
||
affected_products.append(preview)
|
||
|
||
summary = {
|
||
"total_products": len(products),
|
||
"affected_products": len(affected_products),
|
||
"products_with_sales_change": products_with_sales_change,
|
||
"products_with_purchase_change": products_with_purchase_change,
|
||
"total_sales_change": float(_quantize_integer_keep_sign(total_sales_change)),
|
||
"total_purchase_change": float(_quantize_integer_keep_sign(total_purchase_change)),
|
||
"update_type": request.update_type.value,
|
||
"direction": request.direction.value,
|
||
"target": request.target.value,
|
||
"value": float(_quantize_non_negative_integer(request.value)) if request.update_type == BulkPriceUpdateType.AMOUNT else float(request.value)
|
||
}
|
||
|
||
return BulkPriceUpdatePreviewResponse(
|
||
total_products=len(products),
|
||
affected_products=affected_products,
|
||
summary=summary
|
||
)
|
||
|
||
|
||
def apply_bulk_price_update(db: Session, business_id: int, request: BulkPriceUpdateRequest) -> Dict[str, Any]:
|
||
"""اعمال تغییرات قیمت گروهی"""
|
||
products = get_filtered_products(db, business_id, request)
|
||
|
||
updated_count = 0
|
||
errors = []
|
||
|
||
# اگر price_list_ids مشخص شده باشد، هم قیمت پایه و هم PriceItemها باید بهروزرسانی شوند
|
||
for product in products:
|
||
try:
|
||
# بروزرسانی قیمت فروش
|
||
if request.target in [BulkPriceUpdateTarget.SALES_PRICE, BulkPriceUpdateTarget.BOTH] and product.base_sales_price is not None:
|
||
new_sales_price = calculate_new_price(product.base_sales_price, request.update_type, request.direction, request.value)
|
||
product.base_sales_price = new_sales_price
|
||
|
||
# بروزرسانی قیمت خرید
|
||
if request.target in [BulkPriceUpdateTarget.PURCHASE_PRICE, BulkPriceUpdateTarget.BOTH] and product.base_purchase_price is not None:
|
||
new_purchase_price = calculate_new_price(product.base_purchase_price, request.update_type, request.direction, request.value)
|
||
product.base_purchase_price = new_purchase_price
|
||
|
||
# بروزرسانی آیتمهای لیست قیمت مرتبط (در صورت مشخص بودن فیلترها)
|
||
q = db.query(PriceItem).filter(PriceItem.product_id == product.id)
|
||
if request.currency_ids:
|
||
q = q.filter(PriceItem.currency_id.in_(request.currency_ids))
|
||
if request.price_list_ids:
|
||
q = q.filter(PriceItem.price_list_id.in_(request.price_list_ids))
|
||
# اگر هدف فقط فروش/خرید نیست چون PriceItem فقط یک فیلد price دارد، همان price را تغییر میدهیم
|
||
for pi in q.all():
|
||
new_pi_price = calculate_new_price(Decimal(pi.price), request.update_type, request.direction, request.value)
|
||
pi.price = new_pi_price
|
||
|
||
updated_count += 1
|
||
|
||
except Exception as e:
|
||
errors.append(f"خطا در بروزرسانی کالای {product.name}: {str(e)}")
|
||
|
||
db.commit()
|
||
|
||
return {
|
||
"message": f"تغییرات قیمت برای {updated_count} کالا اعمال شد",
|
||
"updated_count": updated_count,
|
||
"total_products": len(products),
|
||
"errors": errors
|
||
}
|