adding
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import Appointment, Customer, LedgerEntry, Product, StockMovement
|
||||
|
||||
|
||||
@admin.register(Customer)
|
||||
class CustomerAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "phone", "user", "updated_at")
|
||||
search_fields = ("name", "phone", "email", "user__phone")
|
||||
|
||||
|
||||
@admin.register(LedgerEntry)
|
||||
class LedgerEntryAdmin(admin.ModelAdmin):
|
||||
list_display = ("title", "entry_type", "amount", "entry_date", "user")
|
||||
list_filter = ("entry_type", "category")
|
||||
search_fields = ("title", "user__phone")
|
||||
|
||||
|
||||
@admin.register(Product)
|
||||
class ProductAdmin(admin.ModelAdmin):
|
||||
list_display = ("name", "sku", "category", "quantity", "unit_price", "user")
|
||||
list_filter = ("category",)
|
||||
search_fields = ("name", "sku", "user__phone")
|
||||
|
||||
|
||||
@admin.register(StockMovement)
|
||||
class StockMovementAdmin(admin.ModelAdmin):
|
||||
list_display = ("product", "delta", "quantity_after", "created_at")
|
||||
search_fields = ("product__name", "product__user__phone")
|
||||
readonly_fields = ("product", "delta", "quantity_after", "note", "created_at")
|
||||
|
||||
def has_add_permission(self, request):
|
||||
return False
|
||||
|
||||
|
||||
@admin.register(Appointment)
|
||||
class AppointmentAdmin(admin.ModelAdmin):
|
||||
list_display = ("customer_name", "title", "start_at", "status", "user")
|
||||
list_filter = ("status",)
|
||||
search_fields = ("customer_name", "customer_phone", "user__phone")
|
||||
@@ -0,0 +1,58 @@
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.bizapps.models import Appointment, Customer, LedgerEntry, Product, StockMovement
|
||||
|
||||
|
||||
class CustomerSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Customer
|
||||
fields = ["id", "name", "phone", "email", "address", "notes", "updated_at"]
|
||||
read_only_fields = ["updated_at"]
|
||||
|
||||
|
||||
class LedgerEntrySerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = LedgerEntry
|
||||
fields = ["id", "entry_type", "title", "amount", "category", "entry_date", "note", "updated_at"]
|
||||
read_only_fields = ["updated_at"]
|
||||
|
||||
|
||||
class ProductSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Product
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"sku",
|
||||
"category",
|
||||
"quantity",
|
||||
"unit_price",
|
||||
"low_stock_threshold",
|
||||
"notes",
|
||||
"updated_at",
|
||||
]
|
||||
read_only_fields = ["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 AppointmentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Appointment
|
||||
fields = ["id", "customer_name", "customer_phone", "title", "start_at", "status", "notes", "updated_at"]
|
||||
read_only_fields = ["updated_at"]
|
||||
@@ -0,0 +1,130 @@
|
||||
from django.db import transaction
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.views import APIView
|
||||
|
||||
from apps.billing.permissions import HasActiveFeature
|
||||
from apps.bizapps.models import Appointment, Customer, LedgerEntry, Product, StockMovement
|
||||
|
||||
from .serializers import (
|
||||
AdjustStockSerializer,
|
||||
AppointmentSerializer,
|
||||
CustomerSerializer,
|
||||
LedgerEntrySerializer,
|
||||
ProductSerializer,
|
||||
StockMovementSerializer,
|
||||
)
|
||||
|
||||
# همهی این ریزاپها با جم فعال میشوند (یا دورهی آزمایشی) — `feature_key`
|
||||
# روی هر ویو مشخص میکند HasActiveFeature باید چه چیزی را چک کند.
|
||||
# permission_classes اینجا Base است تا زیرکلاسها فراموش نکنند.
|
||||
|
||||
|
||||
class _UserScopedListCreateView(generics.ListCreateAPIView):
|
||||
permission_classes = [IsAuthenticated, HasActiveFeature]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(user=self.request.user)
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class _UserScopedDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [IsAuthenticated, HasActiveFeature]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
class CustomerListCreateView(_UserScopedListCreateView):
|
||||
model = Customer
|
||||
serializer_class = CustomerSerializer
|
||||
feature_key = "customers"
|
||||
|
||||
|
||||
class CustomerDetailView(_UserScopedDetailView):
|
||||
model = Customer
|
||||
serializer_class = CustomerSerializer
|
||||
feature_key = "customers"
|
||||
|
||||
|
||||
class LedgerEntryListCreateView(_UserScopedListCreateView):
|
||||
model = LedgerEntry
|
||||
serializer_class = LedgerEntrySerializer
|
||||
feature_key = "ledger"
|
||||
|
||||
|
||||
class LedgerEntryDetailView(_UserScopedDetailView):
|
||||
model = LedgerEntry
|
||||
serializer_class = LedgerEntrySerializer
|
||||
feature_key = "ledger"
|
||||
|
||||
|
||||
class ProductListCreateView(_UserScopedListCreateView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
feature_key = "inventory"
|
||||
|
||||
|
||||
class ProductDetailView(_UserScopedDetailView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
feature_key = "inventory"
|
||||
|
||||
|
||||
class ProductAdjustStockView(APIView):
|
||||
"""تعدیل اتمیک موجودی (+/-) + یک ردیف تاریخچه — بهجای اینکه کلاینت
|
||||
مستقیم quantity جدید را PATCH کند (که هیچ تاریخچهای نمیسازد و اگر دو
|
||||
درخواست همزمان بیایند میتواند یکی را گم کند)."""
|
||||
|
||||
permission_classes = [IsAuthenticated, HasActiveFeature]
|
||||
feature_key = "inventory"
|
||||
|
||||
@transaction.atomic
|
||||
def post(self, request, pk):
|
||||
try:
|
||||
product = Product.objects.select_for_update().get(pk=pk, user=request.user)
|
||||
except Product.DoesNotExist:
|
||||
raise NotFound("کالا یافت نشد.")
|
||||
|
||||
serializer = AdjustStockSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
delta = serializer.validated_data["delta"]
|
||||
|
||||
product.quantity += delta
|
||||
product.save(update_fields=["quantity", "updated_at"])
|
||||
StockMovement.objects.create(
|
||||
product=product,
|
||||
delta=delta,
|
||||
quantity_after=product.quantity,
|
||||
note=serializer.validated_data.get("note", ""),
|
||||
)
|
||||
return Response(ProductSerializer(product).data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class ProductMovementsView(generics.ListAPIView):
|
||||
"""تاریخچهی تغییرات موجودی یک کالا — جدیدترین اول."""
|
||||
|
||||
serializer_class = StockMovementSerializer
|
||||
permission_classes = [IsAuthenticated, HasActiveFeature]
|
||||
feature_key = "inventory"
|
||||
|
||||
def get_queryset(self):
|
||||
return StockMovement.objects.filter(
|
||||
product_id=self.kwargs["pk"], product__user=self.request.user
|
||||
)
|
||||
|
||||
|
||||
class AppointmentListCreateView(_UserScopedListCreateView):
|
||||
model = Appointment
|
||||
serializer_class = AppointmentSerializer
|
||||
feature_key = "booking"
|
||||
|
||||
|
||||
class AppointmentDetailView(_UserScopedDetailView):
|
||||
model = Appointment
|
||||
serializer_class = AppointmentSerializer
|
||||
feature_key = "booking"
|
||||
@@ -0,0 +1,7 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BizappsConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "apps.bizapps"
|
||||
verbose_name = "ابزارهای کسبوکار"
|
||||
@@ -0,0 +1,92 @@
|
||||
# Generated by Django 5.1.15 on 2026-08-23 11:25
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Appointment',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('customer_name', models.CharField(max_length=150, verbose_name='نام مشتری')),
|
||||
('customer_phone', models.CharField(blank=True, max_length=20, verbose_name='شماره مشتری')),
|
||||
('title', models.CharField(max_length=150, verbose_name='عنوان/خدمت')),
|
||||
('start_at', models.DateTimeField(verbose_name='زمان نوبت')),
|
||||
('status', models.CharField(choices=[('scheduled', 'زمان\u200cبندی\u200cشده'), ('done', 'انجام\u200cشده'), ('cancelled', 'لغوشده')], default='scheduled', max_length=10)),
|
||||
('notes', models.TextField(blank=True, verbose_name='یادداشت')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appointments', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'نوبت',
|
||||
'verbose_name_plural': 'نوبت\u200cدهی',
|
||||
'ordering': ['start_at'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Customer',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=150, verbose_name='نام')),
|
||||
('phone', models.CharField(blank=True, max_length=20, verbose_name='شماره تماس')),
|
||||
('email', models.CharField(blank=True, max_length=150, verbose_name='ایمیل')),
|
||||
('address', models.CharField(blank=True, max_length=255, verbose_name='آدرس')),
|
||||
('notes', models.TextField(blank=True, verbose_name='یادداشت')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'مشتری',
|
||||
'verbose_name_plural': 'مشتریان',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='LedgerEntry',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('entry_type', models.CharField(choices=[('income', 'درآمد'), ('expense', 'هزینه')], max_length=10, verbose_name='نوع')),
|
||||
('title', models.CharField(max_length=150, verbose_name='عنوان')),
|
||||
('amount', models.FloatField(verbose_name='مبلغ')),
|
||||
('category', models.CharField(blank=True, max_length=100, verbose_name='دسته')),
|
||||
('entry_date', models.DateField(verbose_name='تاریخ')),
|
||||
('note', models.TextField(blank=True, verbose_name='یادداشت')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ledger_entries', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ردیف دفتر درآمد/هزینه',
|
||||
'verbose_name_plural': 'دفتر درآمد و هزینه',
|
||||
'ordering': ['-entry_date'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Product',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('name', models.CharField(max_length=150, verbose_name='نام کالا')),
|
||||
('sku', models.CharField(blank=True, max_length=100, verbose_name='کد کالا')),
|
||||
('quantity', models.FloatField(default=0, verbose_name='موجودی')),
|
||||
('unit_price', models.FloatField(default=0, verbose_name='قیمت واحد')),
|
||||
('low_stock_threshold', models.FloatField(default=0, verbose_name='آستانه\u200cی هشدار موجودی کم')),
|
||||
('notes', models.TextField(blank=True, verbose_name='یادداشت')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'کالا',
|
||||
'verbose_name_plural': 'انبار',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,38 @@
|
||||
# Generated by Django 5.1.15 on 2026-08-23 12:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bizapps', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='BrokerageTransaction',
|
||||
fields=[
|
||||
('id', models.CharField(max_length=36, primary_key=True, serialize=False)),
|
||||
('client_name', models.CharField(max_length=150, verbose_name='نام مشتری')),
|
||||
('client_phone', models.CharField(blank=True, max_length=20, verbose_name='شماره مشتری')),
|
||||
('symbol', models.CharField(max_length=50, verbose_name='نماد')),
|
||||
('side', models.CharField(choices=[('buy', 'خرید'), ('sell', 'فروش')], max_length=4, verbose_name='نوع معامله')),
|
||||
('quantity', models.FloatField(verbose_name='تعداد')),
|
||||
('price', models.FloatField(verbose_name='قیمت واحد')),
|
||||
('commission_percent', models.FloatField(default=0, verbose_name='درصد کارمزد')),
|
||||
('transaction_date', models.DateField(verbose_name='تاریخ')),
|
||||
('notes', models.TextField(blank=True, verbose_name='یادداشت')),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='brokerage_transactions', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'معامله\u200cی بورسی',
|
||||
'verbose_name_plural': 'کارگزاری',
|
||||
'ordering': ['-transaction_date'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,16 @@
|
||||
# Generated by Django 5.1.15 on 2026-08-23 14:51
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bizapps', '0002_brokeragetransaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='BrokerageTransaction',
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 5.1.15 on 2026-08-23 20:24
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bizapps', '0003_delete_brokeragetransaction'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='category',
|
||||
field=models.CharField(blank=True, max_length=100, verbose_name='دسته'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='StockMovement',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('delta', models.FloatField(verbose_name='تغییر')),
|
||||
('quantity_after', models.FloatField(verbose_name='موجودی بعد از تغییر')),
|
||||
('note', models.CharField(blank=True, max_length=255, verbose_name='یادداشت')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movements', to='bizapps.product')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'تغییر موجودی',
|
||||
'verbose_name_plural': 'تاریخچه\u200cی موجودی',
|
||||
'ordering': ['-created_at'],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,116 @@
|
||||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
# ریزاپهای سادهی کسبوکار کوچک — مشتریان، دفتر درآمد/هزینه، انبار،
|
||||
# نوبتدهی. دقیقا همالگو با apps.banker: id همان UUID تولیدشده در خودِ اپ
|
||||
# فلاتر است (نه auto-increment)، هر رکورد به user صاحبش محدود میشود.
|
||||
|
||||
|
||||
class Customer(models.Model):
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="customers")
|
||||
name = models.CharField(max_length=150, verbose_name=_("نام"))
|
||||
phone = models.CharField(max_length=20, blank=True, verbose_name=_("شماره تماس"))
|
||||
email = models.CharField(max_length=150, blank=True, verbose_name=_("ایمیل"))
|
||||
address = models.CharField(max_length=255, blank=True, verbose_name=_("آدرس"))
|
||||
notes = models.TextField(blank=True, verbose_name=_("یادداشت"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("مشتری")
|
||||
verbose_name_plural = _("مشتریان")
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class LedgerEntry(models.Model):
|
||||
class EntryType(models.TextChoices):
|
||||
INCOME = "income", _("درآمد")
|
||||
EXPENSE = "expense", _("هزینه")
|
||||
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="ledger_entries")
|
||||
entry_type = models.CharField(max_length=10, choices=EntryType.choices, verbose_name=_("نوع"))
|
||||
title = models.CharField(max_length=150, verbose_name=_("عنوان"))
|
||||
amount = models.FloatField(verbose_name=_("مبلغ"))
|
||||
category = models.CharField(max_length=100, blank=True, verbose_name=_("دسته"))
|
||||
entry_date = models.DateField(verbose_name=_("تاریخ"))
|
||||
note = models.TextField(blank=True, verbose_name=_("یادداشت"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("ردیف دفتر درآمد/هزینه")
|
||||
verbose_name_plural = _("دفتر درآمد و هزینه")
|
||||
ordering = ["-entry_date"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.title} ({self.get_entry_type_display()})"
|
||||
|
||||
|
||||
class Product(models.Model):
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="products")
|
||||
name = models.CharField(max_length=150, verbose_name=_("نام کالا"))
|
||||
sku = models.CharField(max_length=100, blank=True, verbose_name=_("کد کالا"))
|
||||
category = models.CharField(max_length=100, blank=True, verbose_name=_("دسته"))
|
||||
quantity = models.FloatField(default=0, verbose_name=_("موجودی"))
|
||||
unit_price = models.FloatField(default=0, verbose_name=_("قیمت واحد"))
|
||||
low_stock_threshold = models.FloatField(default=0, verbose_name=_("آستانهی هشدار موجودی کم"))
|
||||
notes = models.TextField(blank=True, verbose_name=_("یادداشت"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("کالا")
|
||||
verbose_name_plural = _("انبار")
|
||||
ordering = ["name"]
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class StockMovement(models.Model):
|
||||
"""تاریخچهی تغییرات موجودی — هر تعدیل (دستیِ +/- یا ویرایش مستقیم فرم)
|
||||
یک ردیف اینجا ثبت میکند تا معلوم باشد موجودی کِی و چقدر تغییر کرده."""
|
||||
|
||||
id = models.AutoField(primary_key=True)
|
||||
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="movements")
|
||||
delta = models.FloatField(verbose_name=_("تغییر"))
|
||||
quantity_after = models.FloatField(verbose_name=_("موجودی بعد از تغییر"))
|
||||
note = models.CharField(max_length=255, blank=True, verbose_name=_("یادداشت"))
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("تغییر موجودی")
|
||||
verbose_name_plural = _("تاریخچهی موجودی")
|
||||
ordering = ["-created_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.product.name} — {self.delta:+g} — {self.created_at:%Y-%m-%d %H:%M}"
|
||||
|
||||
|
||||
class Appointment(models.Model):
|
||||
class Status(models.TextChoices):
|
||||
SCHEDULED = "scheduled", _("زمانبندیشده")
|
||||
DONE = "done", _("انجامشده")
|
||||
CANCELLED = "cancelled", _("لغوشده")
|
||||
|
||||
id = models.CharField(max_length=36, primary_key=True)
|
||||
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="appointments")
|
||||
customer_name = models.CharField(max_length=150, verbose_name=_("نام مشتری"))
|
||||
customer_phone = models.CharField(max_length=20, blank=True, verbose_name=_("شماره مشتری"))
|
||||
title = models.CharField(max_length=150, verbose_name=_("عنوان/خدمت"))
|
||||
start_at = models.DateTimeField(verbose_name=_("زمان نوبت"))
|
||||
status = models.CharField(max_length=10, choices=Status.choices, default=Status.SCHEDULED)
|
||||
notes = models.TextField(blank=True, verbose_name=_("یادداشت"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("نوبت")
|
||||
verbose_name_plural = _("نوبتدهی")
|
||||
ordering = ["start_at"]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.customer_name} — {self.title}"
|
||||
@@ -0,0 +1,28 @@
|
||||
from django.urls import path
|
||||
|
||||
from .api.views import (
|
||||
AppointmentDetailView,
|
||||
AppointmentListCreateView,
|
||||
CustomerDetailView,
|
||||
CustomerListCreateView,
|
||||
LedgerEntryDetailView,
|
||||
LedgerEntryListCreateView,
|
||||
ProductAdjustStockView,
|
||||
ProductDetailView,
|
||||
ProductListCreateView,
|
||||
ProductMovementsView,
|
||||
)
|
||||
|
||||
app_name = "bizapps"
|
||||
urlpatterns = [
|
||||
path("customers/", CustomerListCreateView.as_view(), name="customer-list"),
|
||||
path("customers/<str:pk>/", CustomerDetailView.as_view(), name="customer-detail"),
|
||||
path("ledger/", LedgerEntryListCreateView.as_view(), name="ledger-list"),
|
||||
path("ledger/<str:pk>/", LedgerEntryDetailView.as_view(), name="ledger-detail"),
|
||||
path("products/", ProductListCreateView.as_view(), name="product-list"),
|
||||
path("products/<str:pk>/", ProductDetailView.as_view(), name="product-detail"),
|
||||
path("products/<str:pk>/adjust/", ProductAdjustStockView.as_view(), name="product-adjust"),
|
||||
path("products/<str:pk>/movements/", ProductMovementsView.as_view(), name="product-movements"),
|
||||
path("appointments/", AppointmentListCreateView.as_view(), name="appointment-list"),
|
||||
path("appointments/<str:pk>/", AppointmentDetailView.as_view(), name="appointment-detail"),
|
||||
]
|
||||
Reference in New Issue
Block a user