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="") discount_amount = serializers.FloatField(read_only=True) tax_amount = serializers.FloatField(read_only=True) computed_amount = serializers.FloatField(read_only=True) 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", "base_amount", "discount_percent", "tax_percent", "discount_amount", "tax_amount", "computed_amount", "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", "is_active", "production_date", "expiry_date", "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_percent", "discount_amount", "extra_discount_label", "extra_discount_percent", "extra_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_percent مثبت باشد، discount_amount از # روی همان محاسبه می‌شود (هم‌الگو با تخفیفِ درصدیِ هر قلم فاکتور)؛ # discount_amount خامْ فقط برای سازگاری با کلاینت‌های قدیمی نگه داشته # شده — اگر درصد صفر باشد همان مقدار مستقیم استفاده می‌شود. discount_percent = serializers.FloatField(required=False, default=0, min_value=0, max_value=100) discount_amount = serializers.FloatField(required=False, default=0, min_value=0) # تخفیفِ ویژه/دوره‌ای — جدا از تخفیفِ بالا، روی مبلغِ بعد از آن اعمال # می‌شود. یا با درصد یا با مبلغِ ثابت پر می‌شود. extra_discount_label = serializers.CharField(max_length=100, required=False, allow_blank=True, default="") extra_discount_percent = serializers.FloatField(required=False, default=0, min_value=0, max_value=100) extra_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