Files
shopship/apps/bizapps/api/serializers.py
T
2026-09-04 13:05:54 +03:30

256 lines
12 KiB
Python

from rest_framework import serializers
from apps.bizapps.models import (
Account,
Customer,
Invoice,
InvoiceItem,
LedgerBook,
LedgerEntry,
LedgerInstallment,
Product,
StockMovement,
Transaction,
)
class AccountSerializer(serializers.ModelSerializer):
full_path = serializers.CharField(read_only=True)
level_display = serializers.CharField(source="get_level_display", read_only=True)
account_type_display = serializers.CharField(source="get_account_type_display", read_only=True)
class Meta:
model = Account
fields = [
"id", "code", "name", "level", "level_display", "account_type",
"account_type_display", "parent", "full_path", "is_system", "is_active",
]
read_only_fields = ["is_system"]
class CustomerSerializer(serializers.ModelSerializer):
account_code = serializers.CharField(source="account.code", read_only=True, default="")
class Meta:
model = Customer
fields = [
"id", "name", "phone", "social_platform", "social_handle",
"email", "address", "notes",
"total_purchases_count", "total_purchase_amount", "last_purchase_date",
"outstanding_balance", "account_code", "updated_at",
]
# این‌ها را سرویس‌ها محاسبه می‌کنند، نه کلاینت — وگرنه مانده‌ی بدهی
# از سمت اپ قابل‌دستکاری می‌شد.
read_only_fields = [
"total_purchases_count", "total_purchase_amount", "last_purchase_date",
"outstanding_balance", "account_code", "updated_at",
]
class LedgerInstallmentSerializer(serializers.ModelSerializer):
class Meta:
model = LedgerInstallment
fields = ["id", "amount", "paid_date", "reference", "created_at"]
# برخلافِ بقیه‌ی مدل‌ها (که UUID را خودِ اپ فلاتر می‌سازد و می‌فرستد)،
# id قسط را ویو تولید می‌کند — پس نباید از کلاینت الزامی باشد.
read_only_fields = ["id", "created_at"]
def validate_amount(self, value):
if value <= 0:
raise serializers.ValidationError("مبلغ قسط باید بزرگ‌تر از صفر باشد.")
return value
def validate(self, attrs):
entry: LedgerEntry = self.context["ledger_entry"]
if not entry.is_debt:
raise serializers.ValidationError("فقط برای ردیف‌های بدهی‌دار می‌توان قسط ثبت کرد.")
amount = attrs.get("amount", 0)
# epsilon کوچک تا خطای گرد‌کردنِ اعشارِ شناور باعثِ ردِ یک قسطِ
# تسویه‌ی دقیق نشود.
if amount > entry.remaining_amount + 1:
raise serializers.ValidationError("مبلغ قسط از مانده‌ی بدهی بیشتر است.")
return attrs
class LedgerEntrySerializer(serializers.ModelSerializer):
installments = LedgerInstallmentSerializer(many=True, read_only=True)
is_debt = serializers.BooleanField(read_only=True)
professional_type = serializers.CharField(read_only=True)
paid_amount = serializers.FloatField(read_only=True)
remaining_amount = serializers.FloatField(read_only=True)
category_display = serializers.CharField(source="get_category_display", read_only=True)
account_name = serializers.CharField(source="account.name", read_only=True, default="")
account_code = serializers.CharField(source="account.code", read_only=True, default="")
class Meta:
model = LedgerEntry
fields = [
"id", "entry_type", "title", "amount", "category", "category_display",
"entry_date", "note", "total_amount", "counterparty_name", "counterparty_phone",
"linked_invoice", "account_name", "account_code",
"is_debt", "professional_type", "paid_amount", "remaining_amount",
"installments", "updated_at",
]
read_only_fields = ["linked_invoice", "account_name", "account_code", "updated_at"]
def validate(self, attrs):
category = attrs.get("category", getattr(self.instance, "category", ""))
if category in LedgerEntry.DEBT_CATEGORIES:
total = attrs.get("total_amount", getattr(self.instance, "total_amount", 0))
if not total or total <= 0:
raise serializers.ValidationError(
{"total_amount": "برای فروش قسطی/خرید نسیه، مبلغ کل تعهد الزامی است."}
)
return attrs
class ProductSerializer(serializers.ModelSerializer):
is_low_stock = serializers.BooleanField(read_only=True)
class Meta:
model = Product
fields = [
"id", "name", "sku", "category", "unit", "quantity",
"unit_price", "purchase_price", "total_purchase_value",
"low_stock_threshold", "is_low_stock", "notes", "updated_at",
]
read_only_fields = ["total_purchase_value", "is_low_stock", "updated_at"]
class StockMovementSerializer(serializers.ModelSerializer):
class Meta:
model = StockMovement
fields = ["id", "delta", "quantity_after", "note", "created_at"]
read_only_fields = fields
class AdjustStockSerializer(serializers.Serializer):
delta = serializers.FloatField()
note = serializers.CharField(max_length=255, required=False, allow_blank=True, default="")
def validate_delta(self, value):
if value == 0:
raise serializers.ValidationError("مقدار تغییر نمی‌تواند صفر باشد.")
return value
class InvoiceItemSerializer(serializers.ModelSerializer):
class Meta:
model = InvoiceItem
fields = ["id", "product", "description", "quantity", "unit_price", "discount_percent", "total_price"]
read_only_fields = ["total_price"]
class InvoiceSerializer(serializers.ModelSerializer):
items = InvoiceItemSerializer(many=True, read_only=True)
payment_type_display = serializers.CharField(source="get_payment_type_display", read_only=True)
payment_status_display = serializers.CharField(source="get_payment_status_display", read_only=True)
order_status_display = serializers.CharField(source="get_order_status_display", read_only=True)
class Meta:
model = Invoice
fields = [
"id", "invoice_number", "invoice_date", "customer",
"buyer_name", "buyer_phone", "buyer_address",
"seller_name", "seller_phone", "seller_address",
"total_amount", "discount_amount", "tax_lines", "final_amount",
"payment_type", "payment_type_display",
"payment_status", "payment_status_display",
"order_status", "order_status_display",
"shipping_method", "tracking_code", "notes", "paid_at",
"items", "created_at", "updated_at",
]
read_only_fields = [
"invoice_number", "total_amount", "final_amount", "paid_at", "created_at", "updated_at",
]
class CreateInvoiceItemSerializer(serializers.Serializer):
product = serializers.PrimaryKeyRelatedField(queryset=Product.objects.all(), required=False, allow_null=True)
description = serializers.CharField(max_length=255, required=False, allow_blank=True, default="")
quantity = serializers.FloatField(min_value=0.0001)
unit_price = serializers.FloatField(required=False, default=0)
discount_percent = serializers.FloatField(required=False, default=0, min_value=0, max_value=100)
def validate(self, attrs):
if attrs.get("product") is None and not attrs.get("description"):
raise serializers.ValidationError("برای ردیف بدون کالا، شرح الزامی است.")
return attrs
class CreateInvoiceSerializer(serializers.Serializer):
items = CreateInvoiceItemSerializer(many=True)
customer = serializers.PrimaryKeyRelatedField(queryset=Customer.objects.all(), required=False, allow_null=True)
invoice_date = serializers.DateTimeField(required=False)
buyer_name = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
buyer_phone = serializers.CharField(max_length=20, required=False, allow_blank=True, default="")
buyer_address = serializers.CharField(max_length=255, required=False, allow_blank=True, default="")
seller_name = serializers.CharField(max_length=150, required=False, allow_blank=True, default="")
seller_phone = serializers.CharField(max_length=20, required=False, allow_blank=True, default="")
seller_address = serializers.CharField(max_length=255, required=False, allow_blank=True, default="")
discount_amount = serializers.FloatField(required=False, default=0, min_value=0)
tax_lines = serializers.ListField(child=serializers.DictField(), required=False, default=list)
payment_type = serializers.ChoiceField(choices=Invoice.PaymentType.choices, required=False)
shipping_method = serializers.ChoiceField(choices=Invoice.ShippingMethod.choices, required=False)
tracking_code = serializers.CharField(max_length=100, required=False, allow_blank=True, default="")
notes = serializers.CharField(required=False, allow_blank=True, default="")
send_sms = serializers.BooleanField(required=False, default=False)
def validate_items(self, value):
if not value:
raise serializers.ValidationError("فاکتور باید حداقل یک قلم کالا داشته باشد.")
return value
class UpdateInvoiceStatusSerializer(serializers.Serializer):
order_status = serializers.ChoiceField(choices=Invoice.OrderStatus.choices, required=False)
payment_status = serializers.ChoiceField(choices=Invoice.PaymentStatus.choices, required=False)
tracking_code = serializers.CharField(max_length=100, required=False, allow_blank=True)
def validate(self, attrs):
if not attrs:
raise serializers.ValidationError("حداقل یکی از فیلدهای وضعیت باید ارسال شود.")
return attrs
class TransactionSerializer(serializers.ModelSerializer):
transaction_type_display = serializers.CharField(source="get_transaction_type_display", read_only=True)
debit_account_name = serializers.CharField(source="debit_account_ref.name", read_only=True, default="")
credit_account_name = serializers.CharField(source="credit_account_ref.name", read_only=True, default="")
class Meta:
model = Transaction
fields = [
"id", "transaction_number", "transaction_date", "reference_invoice",
"transaction_type", "transaction_type_display",
"debit_account", "credit_account", "debit_account_name", "credit_account_name",
"amount", "description", "created_at",
]
read_only_fields = fields
class LedgerBookSerializer(serializers.ModelSerializer):
class Meta:
model = LedgerBook
fields = [
"id", "name", "start_date", "end_date", "opening_balance",
"is_active", "is_closed", "closed_at", "created_at",
]
read_only_fields = ["id", "is_active", "is_closed", "closed_at", "created_at"]
class OpenLedgerBookSerializer(serializers.Serializer):
name = serializers.CharField(max_length=120)
start_date = serializers.DateField()
end_date = serializers.DateField(required=False, allow_null=True)
# پیش‌فرض true: انتقالِ مانده سرِ سال مالی کارِ استاندارد است.
carry_balance = serializers.BooleanField(required=False, default=True)
def validate(self, attrs):
end = attrs.get("end_date")
if end and end <= attrs["start_date"]:
raise serializers.ValidationError(
{"end_date": "تاریخ پایان باید بعد از تاریخ شروع باشد."}
)
return attrs