forked from hesabix/arc
421 lines
18 KiB
Python
Executable file
421 lines
18 KiB
Python
Executable file
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List
|
|
import json
|
|
import logging
|
|
from sqlalchemy.orm import Session
|
|
from sqlalchemy import and_, func
|
|
|
|
from adapters.db.models.quick_sales_settings import QuickSalesSetting
|
|
from adapters.db.models.person import Person, PersonType
|
|
from adapters.db.models.business import Business
|
|
from adapters.api.v1.schema_models.person import PersonCreateRequest
|
|
from app.core.responses import ApiError
|
|
from app.services.person_service import create_person
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_SHARE_CHANNELS: List[str] = ["sms", "native"]
|
|
VALID_SHARE_CHANNEL_CODES = frozenset({"sms", "email", "native"})
|
|
DEFAULT_SHARE_EXPIRY_HOURS = 168
|
|
MIN_SHARE_EXPIRY_HOURS = 1
|
|
MAX_SHARE_EXPIRY_HOURS = 720
|
|
|
|
|
|
def normalize_default_share_channels(value: Any) -> List[str]:
|
|
"""نرمالسازی لیست کانالهای اشتراکگذاری."""
|
|
if value is None:
|
|
return list(DEFAULT_SHARE_CHANNELS)
|
|
parsed = value
|
|
if isinstance(value, str):
|
|
try:
|
|
parsed = json.loads(value)
|
|
except json.JSONDecodeError:
|
|
return list(DEFAULT_SHARE_CHANNELS)
|
|
if not isinstance(parsed, list):
|
|
return list(DEFAULT_SHARE_CHANNELS)
|
|
channels = [str(c).strip().lower() for c in parsed if str(c).strip()]
|
|
filtered = [c for c in channels if c in VALID_SHARE_CHANNEL_CODES]
|
|
return filtered or list(DEFAULT_SHARE_CHANNELS)
|
|
|
|
|
|
def normalize_share_expiry_hours(value: Any) -> int:
|
|
try:
|
|
hours = int(value)
|
|
except (TypeError, ValueError):
|
|
return DEFAULT_SHARE_EXPIRY_HOURS
|
|
return max(MIN_SHARE_EXPIRY_HOURS, min(MAX_SHARE_EXPIRY_HOURS, hours))
|
|
|
|
|
|
def _share_settings_from_obj(obj: QuickSalesSetting | None) -> Dict[str, Any]:
|
|
if obj is None:
|
|
return {
|
|
"default_share_online_payment": True,
|
|
"default_share_gateway_id": None,
|
|
"default_share_channels": list(DEFAULT_SHARE_CHANNELS),
|
|
"default_share_expiry_hours": DEFAULT_SHARE_EXPIRY_HOURS,
|
|
}
|
|
return {
|
|
"default_share_online_payment": bool(getattr(obj, "default_share_online_payment", True)),
|
|
"default_share_gateway_id": (
|
|
int(obj.default_share_gateway_id) if obj.default_share_gateway_id else None
|
|
),
|
|
"default_share_channels": normalize_default_share_channels(obj.default_share_channels),
|
|
"default_share_expiry_hours": normalize_share_expiry_hours(
|
|
getattr(obj, "default_share_expiry_hours", DEFAULT_SHARE_EXPIRY_HOURS)
|
|
),
|
|
}
|
|
|
|
|
|
def _validate_share_gateway_id(db: Session, business_id: int, gateway_id: int) -> None:
|
|
from adapters.db.models.payment_gateway import BusinessPaymentGateway, PaymentGateway
|
|
|
|
gw = (
|
|
db.query(PaymentGateway)
|
|
.filter(PaymentGateway.id == int(gateway_id), PaymentGateway.is_active == True) # noqa: E712
|
|
.first()
|
|
)
|
|
if not gw:
|
|
raise ApiError("GATEWAY_NOT_FOUND", "درگاه پرداخت یافت نشد یا غیرفعال است", http_status=404)
|
|
links = (
|
|
db.query(BusinessPaymentGateway)
|
|
.filter(
|
|
BusinessPaymentGateway.business_id == int(business_id),
|
|
BusinessPaymentGateway.is_active == True, # noqa: E712
|
|
)
|
|
.all()
|
|
)
|
|
if links:
|
|
allowed = {int(lg.gateway_id) for lg in links}
|
|
if int(gateway_id) not in allowed:
|
|
raise ApiError(
|
|
"GATEWAY_NOT_LINKED",
|
|
"این درگاه برای این کسبوکار فعال نیست",
|
|
http_status=400,
|
|
)
|
|
|
|
|
|
def get_quick_sales_settings(db: Session, business_id: int) -> Dict[str, Any]:
|
|
"""دریافت تنظیمات فروش سریع یک کسبوکار"""
|
|
obj = (
|
|
db.query(QuickSalesSetting)
|
|
.filter(QuickSalesSetting.business_id == int(business_id))
|
|
.first()
|
|
)
|
|
|
|
# دریافت کسبوکار برای دسترسی به ارز پیشفرض
|
|
business = db.query(Business).filter(Business.id == int(business_id)).first()
|
|
business_default_currency_id = business.default_currency_id if business else None
|
|
|
|
if not obj:
|
|
# بازگشت مقادیر پیشفرض
|
|
return {
|
|
"business_id": int(business_id),
|
|
"default_anonymous_customer_id": None,
|
|
"auto_create_anonymous_customer": True,
|
|
"anonymous_customer_name": "مشتری ناشناس",
|
|
"default_warehouse_id": None,
|
|
"default_cash_register_id": None,
|
|
"default_currency_id": business_default_currency_id,
|
|
"default_price_list_id": None,
|
|
"auto_print": False,
|
|
"print_template_id": None,
|
|
"print_paper_size": "80mm",
|
|
"enable_warehouse_document": True,
|
|
"warehouse_document_type": "posted",
|
|
"auto_post_warehouse": True,
|
|
"show_inventory": True,
|
|
"auto_create_payment_document": True,
|
|
"show_purchase_price": False,
|
|
**_share_settings_from_obj(None),
|
|
}
|
|
|
|
# اگر ارز پیشفرض در تنظیمات تنظیم نشده باشد، از ارز پیشفرض کسبوکار استفاده میکنیم
|
|
currency_id = int(obj.default_currency_id) if obj.default_currency_id else business_default_currency_id
|
|
|
|
return {
|
|
"business_id": int(business_id),
|
|
"default_anonymous_customer_id": int(obj.default_anonymous_customer_id) if obj.default_anonymous_customer_id else None,
|
|
"auto_create_anonymous_customer": bool(obj.auto_create_anonymous_customer),
|
|
"anonymous_customer_name": obj.anonymous_customer_name if obj.anonymous_customer_name else "مشتری ناشناس",
|
|
"default_warehouse_id": int(obj.default_warehouse_id) if obj.default_warehouse_id else None,
|
|
"default_cash_register_id": int(obj.default_cash_register_id) if obj.default_cash_register_id else None,
|
|
"default_currency_id": currency_id,
|
|
"default_price_list_id": int(obj.default_price_list_id) if obj.default_price_list_id else None,
|
|
"auto_print": bool(obj.auto_print),
|
|
"print_template_id": int(obj.print_template_id) if obj.print_template_id else None,
|
|
"print_paper_size": (getattr(obj, "print_paper_size", None) or "80mm"),
|
|
"enable_warehouse_document": bool(getattr(obj, 'enable_warehouse_document', True)),
|
|
"warehouse_document_type": str(getattr(obj, 'warehouse_document_type', 'posted')),
|
|
"auto_post_warehouse": bool(obj.auto_post_warehouse),
|
|
"show_inventory": bool(obj.show_inventory),
|
|
"auto_create_payment_document": bool(obj.auto_create_payment_document),
|
|
"show_purchase_price": bool(obj.show_purchase_price),
|
|
**_share_settings_from_obj(obj),
|
|
}
|
|
|
|
|
|
def update_quick_sales_settings(db: Session, business_id: int, payload: Dict[str, Any], user_id: int | None = None) -> Dict[str, Any]:
|
|
"""بهروزرسانی تنظیمات فروش سریع یک کسبوکار"""
|
|
obj = (
|
|
db.query(QuickSalesSetting)
|
|
.filter(QuickSalesSetting.business_id == int(business_id))
|
|
.first()
|
|
)
|
|
|
|
if not obj:
|
|
obj = QuickSalesSetting(business_id=int(business_id))
|
|
db.add(obj)
|
|
|
|
# بهروزرسانی فیلدها
|
|
if "default_anonymous_customer_id" in payload:
|
|
customer_id = payload.get("default_anonymous_customer_id")
|
|
if customer_id is not None:
|
|
# بررسی وجود شخص
|
|
person = db.query(Person).filter(
|
|
and_(
|
|
Person.id == int(customer_id),
|
|
Person.business_id == int(business_id),
|
|
Person.person_types.like(f'%{PersonType.CUSTOMER.value}%'),
|
|
)
|
|
).first()
|
|
if not person:
|
|
raise ApiError("PERSON_NOT_FOUND", "مشتری یافت نشد", http_status=404)
|
|
obj.default_anonymous_customer_id = int(customer_id) if customer_id is not None else None
|
|
|
|
if "auto_create_anonymous_customer" in payload:
|
|
obj.auto_create_anonymous_customer = bool(payload.get("auto_create_anonymous_customer"))
|
|
|
|
if "anonymous_customer_name" in payload:
|
|
name_data = payload.get("anonymous_customer_name")
|
|
if name_data is not None:
|
|
obj.anonymous_customer_name = str(name_data).strip()
|
|
else:
|
|
obj.anonymous_customer_name = None
|
|
|
|
if "default_warehouse_id" in payload:
|
|
warehouse_id = payload.get("default_warehouse_id")
|
|
if warehouse_id is not None:
|
|
from adapters.db.models.warehouse import Warehouse
|
|
warehouse = db.query(Warehouse).filter(
|
|
and_(
|
|
Warehouse.id == int(warehouse_id),
|
|
Warehouse.business_id == int(business_id),
|
|
)
|
|
).first()
|
|
if not warehouse:
|
|
raise ApiError("WAREHOUSE_NOT_FOUND", "انبار یافت نشد", http_status=404)
|
|
obj.default_warehouse_id = int(warehouse_id) if warehouse_id is not None else None
|
|
|
|
if "default_cash_register_id" in payload:
|
|
cash_register_id = payload.get("default_cash_register_id")
|
|
if cash_register_id is not None:
|
|
from adapters.db.models.cash_register import CashRegister
|
|
cash_register = db.query(CashRegister).filter(
|
|
and_(
|
|
CashRegister.id == int(cash_register_id),
|
|
CashRegister.business_id == int(business_id),
|
|
)
|
|
).first()
|
|
if not cash_register:
|
|
raise ApiError("CASH_REGISTER_NOT_FOUND", "صندوق یافت نشد", http_status=404)
|
|
obj.default_cash_register_id = int(cash_register_id) if cash_register_id is not None else None
|
|
|
|
if "default_currency_id" in payload:
|
|
currency_id = payload.get("default_currency_id")
|
|
if currency_id is not None:
|
|
from adapters.db.models.currency import Currency
|
|
currency = db.query(Currency).filter(Currency.id == int(currency_id)).first()
|
|
if not currency:
|
|
raise ApiError("CURRENCY_NOT_FOUND", "ارز یافت نشد", http_status=404)
|
|
obj.default_currency_id = int(currency_id) if currency_id is not None else None
|
|
|
|
if "default_price_list_id" in payload:
|
|
price_list_id = payload.get("default_price_list_id")
|
|
if price_list_id is not None:
|
|
from adapters.db.models.price_list import PriceList
|
|
price_list = db.query(PriceList).filter(
|
|
and_(
|
|
PriceList.id == int(price_list_id),
|
|
PriceList.business_id == int(business_id),
|
|
)
|
|
).first()
|
|
if not price_list:
|
|
raise ApiError("PRICE_LIST_NOT_FOUND", "لیست قیمت یافت نشد", http_status=404)
|
|
obj.default_price_list_id = int(price_list_id) if price_list_id is not None else None
|
|
|
|
if "auto_print" in payload:
|
|
obj.auto_print = bool(payload.get("auto_print"))
|
|
|
|
if "print_template_id" in payload:
|
|
template_id = payload.get("print_template_id")
|
|
obj.print_template_id = int(template_id) if template_id is not None else None
|
|
|
|
if "print_paper_size" in payload:
|
|
from app.services.pdf.page_size import is_receipt_paper
|
|
|
|
raw = payload.get("print_paper_size")
|
|
size = str(raw).strip() if raw not in (None, "") else None
|
|
if size and size not in ("A4", "A5", "A6") and not is_receipt_paper(size):
|
|
raise ApiError("INVALID_PAPER_SIZE", "سایز کاغذ چاپ نامعتبر است", http_status=400)
|
|
obj.print_paper_size = size
|
|
|
|
if "enable_warehouse_document" in payload:
|
|
obj.enable_warehouse_document = bool(payload.get("enable_warehouse_document"))
|
|
|
|
if "warehouse_document_type" in payload:
|
|
doc_type = payload.get("warehouse_document_type")
|
|
if doc_type not in ("draft", "posted"):
|
|
raise ApiError("INVALID_WAREHOUSE_DOCUMENT_TYPE", "نوع سند حواله باید draft یا posted باشد", http_status=400)
|
|
obj.warehouse_document_type = str(doc_type)
|
|
|
|
if "auto_post_warehouse" in payload:
|
|
obj.auto_post_warehouse = bool(payload.get("auto_post_warehouse"))
|
|
|
|
if "show_inventory" in payload:
|
|
obj.show_inventory = bool(payload.get("show_inventory"))
|
|
|
|
if "auto_create_payment_document" in payload:
|
|
obj.auto_create_payment_document = bool(payload.get("auto_create_payment_document"))
|
|
|
|
if "show_purchase_price" in payload:
|
|
obj.show_purchase_price = bool(payload.get("show_purchase_price"))
|
|
|
|
if "default_share_online_payment" in payload:
|
|
obj.default_share_online_payment = bool(payload.get("default_share_online_payment"))
|
|
|
|
if "default_share_gateway_id" in payload:
|
|
gateway_id = payload.get("default_share_gateway_id")
|
|
if gateway_id is not None:
|
|
_validate_share_gateway_id(db, business_id, int(gateway_id))
|
|
obj.default_share_gateway_id = int(gateway_id) if gateway_id is not None else None
|
|
|
|
if "default_share_channels" in payload:
|
|
obj.default_share_channels = normalize_default_share_channels(payload.get("default_share_channels"))
|
|
|
|
if "default_share_expiry_hours" in payload:
|
|
obj.default_share_expiry_hours = normalize_share_expiry_hours(payload.get("default_share_expiry_hours"))
|
|
|
|
db.flush()
|
|
db.commit()
|
|
return get_quick_sales_settings(db, business_id)
|
|
|
|
|
|
def get_or_create_anonymous_customer(
|
|
db: Session,
|
|
business_id: int,
|
|
settings: Dict[str, Any] | None = None
|
|
) -> Person:
|
|
"""دریافت یا ایجاد مشتری ناشناس"""
|
|
if settings is None:
|
|
settings = get_quick_sales_settings(db, business_id)
|
|
|
|
# بررسی مشتری پیشفرض
|
|
customer_id = settings.get("default_anonymous_customer_id")
|
|
if customer_id:
|
|
customer = db.query(Person).filter(
|
|
and_(
|
|
Person.id == int(customer_id),
|
|
Person.business_id == int(business_id),
|
|
Person.person_types.like(f'%{PersonType.CUSTOMER.value}%'),
|
|
)
|
|
).first()
|
|
if customer:
|
|
return customer
|
|
|
|
# جستجوی مشتری ناشناس موجود
|
|
customer_name = settings.get("anonymous_customer_name", "مشتری ناشناس")
|
|
name_fa = str(customer_name) if customer_name else "مشتری ناشناس"
|
|
|
|
# جستجوی مشتری با نام و نوع مشتری
|
|
# اولویت با مشتریهایی که کد دارند (کد None نباشد)
|
|
customer = db.query(Person).filter(
|
|
and_(
|
|
Person.business_id == int(business_id),
|
|
Person.alias_name == name_fa,
|
|
Person.person_types.like(f'%{PersonType.CUSTOMER.value}%'),
|
|
Person.code.isnot(None), # اولویت با مشتریهایی که کد دارند
|
|
)
|
|
).first()
|
|
|
|
# اگر مشتری با کد پیدا نشد، مشتری بدون کد را جستجو کن
|
|
if not customer:
|
|
customer = db.query(Person).filter(
|
|
and_(
|
|
Person.business_id == int(business_id),
|
|
Person.alias_name == name_fa,
|
|
Person.person_types.like(f'%{PersonType.CUSTOMER.value}%'),
|
|
)
|
|
).first()
|
|
|
|
if customer:
|
|
# بهروزرسانی تنظیمات (مستقیماً بدون بررسی مجدد)
|
|
obj = (
|
|
db.query(QuickSalesSetting)
|
|
.filter(QuickSalesSetting.business_id == int(business_id))
|
|
.first()
|
|
)
|
|
if not obj:
|
|
obj = QuickSalesSetting(business_id=int(business_id))
|
|
db.add(obj)
|
|
obj.default_anonymous_customer_id = customer.id
|
|
db.flush()
|
|
db.commit()
|
|
return customer
|
|
|
|
# ایجاد جدید با استفاده از create_person برای تولید صحیح کد
|
|
if settings.get("auto_create_anonymous_customer", True):
|
|
# استفاده از create_person برای تولید صحیح کد
|
|
person_data = PersonCreateRequest(
|
|
alias_name=name_fa,
|
|
person_types=[PersonType.CUSTOMER],
|
|
code=None, # None = تولید خودکار کد
|
|
)
|
|
|
|
result = create_person(db, int(business_id), person_data)
|
|
logger.debug(f"create_person result: {result}")
|
|
customer_id = result.get("data", {}).get("id")
|
|
|
|
if not customer_id:
|
|
logger.error(f"Failed to get customer_id from create_person result: {result}")
|
|
raise ApiError(
|
|
"FAILED_TO_CREATE_ANONYMOUS_CUSTOMER",
|
|
"خطا در ایجاد مشتری ناشناس",
|
|
http_status=500
|
|
)
|
|
|
|
# دریافت مشتری ایجاد شده
|
|
customer = db.query(Person).filter(
|
|
and_(
|
|
Person.id == int(customer_id),
|
|
Person.business_id == int(business_id),
|
|
)
|
|
).first()
|
|
|
|
if not customer:
|
|
raise ApiError(
|
|
"ANONYMOUS_CUSTOMER_NOT_FOUND",
|
|
"مشتری ناشناس ایجاد شد اما یافت نشد",
|
|
http_status=500
|
|
)
|
|
|
|
# بهروزرسانی تنظیمات (مستقیماً بدون بررسی مجدد)
|
|
obj = (
|
|
db.query(QuickSalesSetting)
|
|
.filter(QuickSalesSetting.business_id == int(business_id))
|
|
.first()
|
|
)
|
|
if not obj:
|
|
obj = QuickSalesSetting(business_id=int(business_id))
|
|
db.add(obj)
|
|
obj.default_anonymous_customer_id = customer.id
|
|
db.flush()
|
|
db.commit()
|
|
return customer
|
|
|
|
raise ApiError(
|
|
"ANONYMOUS_CUSTOMER_NOT_SET",
|
|
"مشتری ناشناس تعریف نشده است. لطفاً در تنظیمات فروش سریع یک مشتری پیشفرض انتخاب کنید.",
|
|
http_status=400
|
|
)
|
|
|