adding
This commit is contained in:
+430
-71
@@ -1,130 +1,489 @@
|
||||
from django.db import transaction
|
||||
import uuid
|
||||
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpResponse
|
||||
from django.utils.dateparse import parse_date
|
||||
from rest_framework import generics, status
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.exceptions import NotFound, ValidationError
|
||||
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,
|
||||
from apps.bizapps.models import (
|
||||
Account,
|
||||
Customer,
|
||||
Invoice,
|
||||
LedgerBook,
|
||||
LedgerEntry,
|
||||
LedgerInstallment,
|
||||
Product,
|
||||
StockMovement,
|
||||
Transaction,
|
||||
)
|
||||
|
||||
# همهی این ریزاپها با جم فعال میشوند (یا دورهی آزمایشی) — `feature_key`
|
||||
# روی هر ویو مشخص میکند HasActiveFeature باید چه چیزی را چک کند.
|
||||
# permission_classes اینجا Base است تا زیرکلاسها فراموش نکنند.
|
||||
from .accounting import (
|
||||
ChartOfAccountsService,
|
||||
CustomerStatementService,
|
||||
LedgerBookService,
|
||||
SubsidiaryLedgerService,
|
||||
)
|
||||
from .serializers import (
|
||||
AccountSerializer,
|
||||
AdjustStockSerializer,
|
||||
CreateInvoiceSerializer,
|
||||
CustomerSerializer,
|
||||
InvoiceSerializer,
|
||||
LedgerBookSerializer,
|
||||
LedgerEntrySerializer,
|
||||
LedgerInstallmentSerializer,
|
||||
OpenLedgerBookSerializer,
|
||||
ProductSerializer,
|
||||
StockMovementSerializer,
|
||||
TransactionSerializer,
|
||||
UpdateInvoiceStatusSerializer,
|
||||
)
|
||||
from .services import InsufficientStockError, InvoiceService, LedgerService
|
||||
|
||||
|
||||
class _UserScopedListCreateView(generics.ListCreateAPIView):
|
||||
permission_classes = [IsAuthenticated, HasActiveFeature]
|
||||
class _OwnedMixin:
|
||||
"""همهی منابع این اپ فقط متعلق به کاربرِ درخواستدهندهاند."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
# ---------------- مشتریان ----------------
|
||||
|
||||
|
||||
class CustomerListCreateView(_OwnedMixin, generics.ListCreateAPIView):
|
||||
model = Customer
|
||||
serializer_class = CustomerSerializer
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(user=self.request.user)
|
||||
|
||||
|
||||
class _UserScopedDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
permission_classes = [IsAuthenticated, HasActiveFeature]
|
||||
class CustomerDetailView(_OwnedMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
model = Customer
|
||||
serializer_class = CustomerSerializer
|
||||
|
||||
|
||||
class CustomerStatementView(APIView):
|
||||
"""صورتحسابِ کاملِ یک مشتری — فاکتورها + بدهیهای باز + اقساط، یکجا.
|
||||
همان اتصالِ «مشتری ↔ فاکتور ↔ حسابداری» بهشکلِ یک پاسخِ واحد."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
customer = Customer.objects.filter(user=request.user, pk=pk).first()
|
||||
if customer is None:
|
||||
raise NotFound("مشتری یافت نشد.")
|
||||
return Response(CustomerStatementService.build(request.user, customer))
|
||||
|
||||
|
||||
# ---------------- دفتر درآمد و هزینه ----------------
|
||||
|
||||
|
||||
class LedgerEntryListCreateView(_OwnedMixin, generics.ListCreateAPIView):
|
||||
model = LedgerEntry
|
||||
serializer_class = LedgerEntrySerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return self.model.objects.filter(user=self.request.user)
|
||||
qs = super().get_queryset().prefetch_related("installments").select_related("account")
|
||||
# پیشفرض فقط فایلِ جاری؟ نه — book=<id> صریح، وگرنه همه؛ تا
|
||||
# گزارشهای چنددورهای هم ممکن بماند.
|
||||
book_id = self.request.query_params.get("book")
|
||||
if book_id:
|
||||
qs = qs.filter(book_id=book_id)
|
||||
start = parse_date(self.request.query_params.get("start", "") or "")
|
||||
end = parse_date(self.request.query_params.get("end", "") or "")
|
||||
if start:
|
||||
qs = qs.filter(entry_date__gte=start)
|
||||
if end:
|
||||
qs = qs.filter(entry_date__lte=end)
|
||||
return qs
|
||||
|
||||
def perform_create(self, serializer):
|
||||
book = LedgerBookService.ensure_active_book(self.request.user)
|
||||
entry = serializer.save(user=self.request.user, book=book)
|
||||
LedgerService.sync_entry_account(self.request.user, entry)
|
||||
if entry.counterparty_phone:
|
||||
LedgerService.recompute_customer_balance(self.request.user, entry.counterparty_phone)
|
||||
|
||||
|
||||
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):
|
||||
class LedgerEntryDetailView(_OwnedMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
model = LedgerEntry
|
||||
serializer_class = LedgerEntrySerializer
|
||||
feature_key = "ledger"
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related("installments").select_related("account")
|
||||
|
||||
def perform_update(self, serializer):
|
||||
entry = serializer.save()
|
||||
LedgerService.sync_entry_account(self.request.user, entry)
|
||||
if entry.counterparty_phone:
|
||||
LedgerService.recompute_customer_balance(self.request.user, entry.counterparty_phone)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
phone = instance.counterparty_phone
|
||||
instance.delete()
|
||||
if phone:
|
||||
LedgerService.recompute_customer_balance(self.request.user, phone)
|
||||
|
||||
|
||||
class LedgerEntryDetailView(_UserScopedDetailView):
|
||||
model = LedgerEntry
|
||||
serializer_class = LedgerEntrySerializer
|
||||
feature_key = "ledger"
|
||||
class LedgerInstallmentListCreateView(generics.ListCreateAPIView):
|
||||
serializer_class = LedgerInstallmentSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def _entry(self) -> LedgerEntry:
|
||||
entry = LedgerEntry.objects.filter(
|
||||
user=self.request.user, pk=self.kwargs["entry_id"]
|
||||
).prefetch_related("installments").first()
|
||||
if entry is None:
|
||||
raise NotFound("ردیف دفتر یافت نشد.")
|
||||
return entry
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["ledger_entry"] = self._entry()
|
||||
return context
|
||||
|
||||
def get_queryset(self):
|
||||
return self._entry().installments.all()
|
||||
|
||||
def perform_create(self, serializer):
|
||||
entry = self._entry()
|
||||
installment = serializer.save(id=str(uuid.uuid4()), ledger_entry=entry)
|
||||
# هر قسط، هم ماندهی مشتری را بهروز میکند هم یک سندِ حسابداری
|
||||
# دوطرفه میسازد — همان اتصالِ دفتر به درختِ حسابها.
|
||||
LedgerService.post_installment_transaction(self.request.user, installment)
|
||||
if entry.counterparty_phone:
|
||||
LedgerService.recompute_customer_balance(self.request.user, entry.counterparty_phone)
|
||||
|
||||
|
||||
class ProductListCreateView(_UserScopedListCreateView):
|
||||
class LedgerInstallmentDetailView(generics.DestroyAPIView):
|
||||
serializer_class = LedgerInstallmentSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return LedgerInstallment.objects.filter(ledger_entry__user=self.request.user)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
phone = instance.ledger_entry.counterparty_phone
|
||||
instance.delete()
|
||||
if phone:
|
||||
LedgerService.recompute_customer_balance(self.request.user, phone)
|
||||
|
||||
|
||||
class LedgerExportView(APIView):
|
||||
"""خروجیِ دفتر — پیشفرض اکسلِ چندبرگی (?type=csv برای نسخهی ساده).
|
||||
|
||||
عمداً `type` و نه `format`: پارامترِ `format` را خودِ DRF برای مذاکرهی
|
||||
محتوا رزرو کرده و `?format=csv` را بهعنوان درخواستِ رندررِ ثبتنشدهی
|
||||
csv تفسیر و ۴۰۴ میدهد."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
start = parse_date(request.query_params.get("start", "") or "")
|
||||
end = parse_date(request.query_params.get("end", "") or "")
|
||||
|
||||
if request.query_params.get("type") == "csv":
|
||||
content = LedgerService.export_csv(request.user, start, end)
|
||||
response = HttpResponse(content, content_type="text/csv; charset=utf-8-sig")
|
||||
response["Content-Disposition"] = 'attachment; filename="ledger.csv"'
|
||||
return response
|
||||
|
||||
content = LedgerService.export_xlsx(request.user, start, end)
|
||||
response = HttpResponse(
|
||||
content,
|
||||
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
)
|
||||
response["Content-Disposition"] = 'attachment; filename="shopship-report.xlsx"'
|
||||
return response
|
||||
|
||||
|
||||
# ---------------- درخت حسابها ----------------
|
||||
|
||||
|
||||
class AccountListView(generics.ListCreateAPIView):
|
||||
serializer_class = AccountSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
ChartOfAccountsService.ensure_chart(self.request.user)
|
||||
qs = Account.objects.filter(user=self.request.user).select_related("parent")
|
||||
level = self.request.query_params.get("level")
|
||||
if level:
|
||||
qs = qs.filter(level=level)
|
||||
return qs
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save(id=str(uuid.uuid4()), user=self.request.user, is_system=False)
|
||||
|
||||
|
||||
class AccountDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
serializer_class = AccountSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Account.objects.filter(user=self.request.user)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
# حسابهای سیستمی پایهی سندهای خودکارند؛ حذفشان گزارشها را میشکند.
|
||||
if instance.is_system:
|
||||
raise ValidationError({"detail": "حسابهای سیستمی قابل حذف نیستند."})
|
||||
instance.delete()
|
||||
|
||||
|
||||
class TrialBalanceView(APIView):
|
||||
"""ترازِ حسابها — جمعِ بدهکار/بستانکار و ماندهی هر حسابِ معین."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
start = parse_date(request.query_params.get("start", "") or "")
|
||||
end = parse_date(request.query_params.get("end", "") or "")
|
||||
return Response(ChartOfAccountsService.trial_balance(request.user, start, end))
|
||||
|
||||
|
||||
class TransactionListView(generics.ListAPIView):
|
||||
serializer_class = TransactionSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return Transaction.objects.filter(user=self.request.user).select_related(
|
||||
"debit_account_ref", "credit_account_ref"
|
||||
)
|
||||
|
||||
|
||||
# ---------------- انبار ----------------
|
||||
|
||||
|
||||
class ProductListCreateView(_OwnedMixin, generics.ListCreateAPIView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
feature_key = "inventory"
|
||||
|
||||
def perform_create(self, serializer):
|
||||
product = serializer.save(user=self.request.user)
|
||||
product.recompute_purchase_value()
|
||||
product.save(update_fields=["total_purchase_value", "updated_at"])
|
||||
|
||||
|
||||
class ProductDetailView(_UserScopedDetailView):
|
||||
class ProductDetailView(_OwnedMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
model = Product
|
||||
serializer_class = ProductSerializer
|
||||
feature_key = "inventory"
|
||||
|
||||
def perform_update(self, serializer):
|
||||
product = serializer.save()
|
||||
product.recompute_purchase_value()
|
||||
product.save(update_fields=["total_purchase_value", "updated_at"])
|
||||
|
||||
|
||||
class ProductAdjustStockView(APIView):
|
||||
"""تعدیل اتمیک موجودی (+/-) + یک ردیف تاریخچه — بهجای اینکه کلاینت
|
||||
مستقیم quantity جدید را PATCH کند (که هیچ تاریخچهای نمیسازد و اگر دو
|
||||
درخواست همزمان بیایند میتواند یکی را گم کند)."""
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
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:
|
||||
product = Product.objects.filter(user=request.user, pk=pk).first()
|
||||
if product is None:
|
||||
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"])
|
||||
product.quantity += serializer.validated_data["delta"]
|
||||
product.recompute_purchase_value()
|
||||
product.save(update_fields=["quantity", "total_purchase_value", "updated_at"])
|
||||
StockMovement.objects.create(
|
||||
product=product,
|
||||
delta=delta,
|
||||
delta=serializer.validated_data["delta"],
|
||||
quantity_after=product.quantity,
|
||||
note=serializer.validated_data.get("note", ""),
|
||||
)
|
||||
return Response(ProductSerializer(product).data, status=status.HTTP_200_OK)
|
||||
return Response(ProductSerializer(product).data)
|
||||
|
||||
|
||||
class ProductMovementsView(generics.ListAPIView):
|
||||
"""تاریخچهی تغییرات موجودی یک کالا — جدیدترین اول."""
|
||||
|
||||
serializer_class = StockMovementSerializer
|
||||
permission_classes = [IsAuthenticated, HasActiveFeature]
|
||||
feature_key = "inventory"
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return StockMovement.objects.filter(
|
||||
product_id=self.kwargs["pk"], product__user=self.request.user
|
||||
product__user=self.request.user, product_id=self.kwargs["pk"]
|
||||
)
|
||||
|
||||
|
||||
class AppointmentListCreateView(_UserScopedListCreateView):
|
||||
model = Appointment
|
||||
serializer_class = AppointmentSerializer
|
||||
feature_key = "booking"
|
||||
class InventorySummaryView(APIView):
|
||||
"""خلاصهی انبار — ارزشِ کل، تعداد اقلام، و کالاهای روی آستانهی کمبود."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
products = Product.objects.filter(user=request.user)
|
||||
low = [p for p in products if p.is_low_stock]
|
||||
return Response(
|
||||
{
|
||||
"total_products": products.count(),
|
||||
"total_value": products.aggregate(v=Sum("total_purchase_value"))["v"] or 0,
|
||||
"low_stock_count": len(low),
|
||||
"low_stock": ProductSerializer(low, many=True).data,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class AppointmentDetailView(_UserScopedDetailView):
|
||||
model = Appointment
|
||||
serializer_class = AppointmentSerializer
|
||||
feature_key = "booking"
|
||||
# ---------------- فاکتور ----------------
|
||||
|
||||
|
||||
class InvoiceListCreateView(_OwnedMixin, generics.ListCreateAPIView):
|
||||
model = Invoice
|
||||
serializer_class = InvoiceSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related("items")
|
||||
|
||||
def create(self, request, *args, **kwargs):
|
||||
serializer = CreateInvoiceSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
data = dict(serializer.validated_data)
|
||||
send_sms = data.pop("send_sms", False)
|
||||
try:
|
||||
invoice = InvoiceService.create_invoice(request.user, data, send_sms=send_sms)
|
||||
except InsufficientStockError as exc:
|
||||
raise ValidationError({"detail": str(exc)})
|
||||
return Response(InvoiceSerializer(invoice).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class InvoiceDetailView(_OwnedMixin, generics.RetrieveAPIView):
|
||||
model = Invoice
|
||||
serializer_class = InvoiceSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().prefetch_related("items")
|
||||
|
||||
|
||||
class InvoiceStatusView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk):
|
||||
serializer = UpdateInvoiceStatusSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
try:
|
||||
invoice = InvoiceService.update_status(request.user, pk, **serializer.validated_data)
|
||||
except Invoice.DoesNotExist:
|
||||
raise NotFound("فاکتور یافت نشد.")
|
||||
return Response(InvoiceSerializer(invoice).data)
|
||||
|
||||
|
||||
class InvoiceCancelView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk):
|
||||
try:
|
||||
invoice = InvoiceService.cancel_invoice(request.user, pk)
|
||||
except Invoice.DoesNotExist:
|
||||
raise NotFound("فاکتور یافت نشد.")
|
||||
except ValueError as exc:
|
||||
raise ValidationError({"detail": str(exc)})
|
||||
return Response(InvoiceSerializer(invoice).data)
|
||||
|
||||
|
||||
class BusinessDashboardView(APIView):
|
||||
"""داشبوردِ یکپارچه — همان «اتصالِ بخشها» بهشکلِ یک پاسخ: فروش، بدهی،
|
||||
انبار و فاکتورها کنار هم، بهجای چهار درخواستِ جدا از چهار صفحه."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request):
|
||||
user = request.user
|
||||
entries = LedgerEntry.objects.filter(user=user).prefetch_related("installments")
|
||||
products = Product.objects.filter(user=user)
|
||||
invoices = Invoice.objects.filter(user=user)
|
||||
|
||||
income = sum(e.amount for e in entries if e.entry_type == LedgerEntry.EntryType.INCOME)
|
||||
expense = sum(e.amount for e in entries if e.entry_type == LedgerEntry.EntryType.EXPENSE)
|
||||
receivable = sum(
|
||||
e.remaining_amount for e in entries if e.category == LedgerEntry.Category.SALE_CREDIT
|
||||
)
|
||||
payable = sum(
|
||||
e.remaining_amount for e in entries if e.category == LedgerEntry.Category.PURCHASE_CREDIT
|
||||
)
|
||||
|
||||
return Response(
|
||||
{
|
||||
"total_income": income,
|
||||
"total_expense": expense,
|
||||
"net_profit": income - expense,
|
||||
"open_receivable": receivable,
|
||||
"open_payable": payable,
|
||||
"inventory_value": products.aggregate(v=Sum("total_purchase_value"))["v"] or 0,
|
||||
"low_stock_count": sum(1 for p in products if p.is_low_stock),
|
||||
"invoice_count": invoices.count(),
|
||||
"unpaid_invoice_count": invoices.filter(
|
||||
payment_status=Invoice.PaymentStatus.UNPAID
|
||||
).exclude(order_status=Invoice.OrderStatus.CANCELLED).count(),
|
||||
"customer_count": Customer.objects.filter(user=user).count(),
|
||||
"debtor_count": Customer.objects.filter(user=user, outstanding_balance__gt=0).count(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
# ---------------- فایلهای دفتر و دفتر معین ----------------
|
||||
|
||||
|
||||
class LedgerBookListView(generics.ListAPIView):
|
||||
"""فهرست فایلهای دفتر — همیشه حداقل یک فایلِ جاری وجود دارد."""
|
||||
|
||||
serializer_class = LedgerBookSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
LedgerBookService.ensure_active_book(self.request.user)
|
||||
return LedgerBook.objects.filter(user=self.request.user)
|
||||
|
||||
|
||||
class OpenLedgerBookView(APIView):
|
||||
"""باز کردن فایل تازه — فایلِ جاری بسته میشود و (در صورت انتخاب) مانده
|
||||
به فایل جدید منتقل میگردد."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request):
|
||||
serializer = OpenLedgerBookSerializer(data=request.data)
|
||||
serializer.is_valid(raise_exception=True)
|
||||
book = LedgerBookService.open_new_book(
|
||||
request.user,
|
||||
name=serializer.validated_data["name"],
|
||||
start_date=serializer.validated_data["start_date"],
|
||||
end_date=serializer.validated_data.get("end_date"),
|
||||
carry_balance=serializer.validated_data.get("carry_balance", True),
|
||||
)
|
||||
return Response(LedgerBookSerializer(book).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class LedgerBookSummaryView(APIView):
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
book = LedgerBook.objects.filter(user=request.user, pk=pk).first()
|
||||
if book is None:
|
||||
raise NotFound("فایل دفتر یافت نشد.")
|
||||
return Response(
|
||||
{"book": LedgerBookSerializer(book).data, **LedgerBookService.summary(book)}
|
||||
)
|
||||
|
||||
|
||||
class SubsidiaryLedgerView(APIView):
|
||||
"""دفتر معین یک حساب — گردشِ حساب با ماندهی در جریان."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get(self, request, pk):
|
||||
account = Account.objects.filter(user=request.user, pk=pk).first()
|
||||
if account is None:
|
||||
raise NotFound("حساب یافت نشد.")
|
||||
start = parse_date(request.query_params.get("start", "") or "")
|
||||
end = parse_date(request.query_params.get("end", "") or "")
|
||||
return Response(
|
||||
SubsidiaryLedgerService.statement(request.user, account, start, end)
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user