Fix OTP/SMS, ledger accounting, order tracking, and add reviews app
- Add MessageTemplate.runtime_field_labels so mobile's per-field execution-time labels are actually persisted (were silently dropped) - Fix AdvanceOrderStageView reading the wrong extra-input key, and be honest about SMS send failures instead of always claiming success - Add admin gift-gem SMS notification via Kavenegar - Post real double-entry Transactions for plain (non-invoice) ledger entries so trial balance, the معین screen, and CSV/XLSX exports actually include everyday income/expense entries; backfill existing historical entries via data migration - Rewrite CSV ledger export to produce an actual دفتر معین structure (per-account running balance) instead of a flat row dump - Add invoice percent/promotional discounts, tax presets, ledger discount-percent handling, product SKU/category/expiry fields, and manual ledger-book archiving (replacing auto-archive on new book) - Add reviews app (models, API, admin) for the comments/reviews feature - Update tests for the above; fix two stale assertions left over from earlier work in this branch (معین sheet name, book auto-close)
This commit is contained in:
@@ -426,7 +426,40 @@ class ExcelExportService:
|
||||
money_columns={4, 5, 6},
|
||||
)
|
||||
|
||||
# --- برگه ۶: انبار ---
|
||||
# --- برگه ۶: دفتر معین ---
|
||||
# همان چیزی که «معین» یعنی: بهازای هر حسابِ معینِ دارای گردش، تکِ
|
||||
# تکِ اسناد بهترتیبِ تاریخ با ماندهی در جریان — دقیقا فرمتی که
|
||||
# حسابدار وقتی میگوید «دفتر معین رو بفرست» انتظارش را دارد.
|
||||
ws = wb.create_sheet("دفتر معین")
|
||||
subsidiary_rows: list[list] = []
|
||||
for account in Account.objects.filter(user=user, level=Account.Level.MOIN).order_by("code"):
|
||||
statement = SubsidiaryLedgerService.statement(user, account, start, end)
|
||||
if not statement["rows"]:
|
||||
continue
|
||||
subsidiary_rows.append([f"{account.code} — {account.name}", "", "", "", "", "", ""])
|
||||
subsidiary_rows.append(["ماندهی ابتدای دوره", "", "", "", "", "", statement["opening_balance"]])
|
||||
for row in statement["rows"]:
|
||||
subsidiary_rows.append(
|
||||
[
|
||||
row["date"].date().isoformat() if hasattr(row["date"], "date") else row["date"],
|
||||
row["number"],
|
||||
row["type"],
|
||||
row["description"],
|
||||
row["debit"],
|
||||
row["credit"],
|
||||
row["balance"],
|
||||
]
|
||||
)
|
||||
subsidiary_rows.append(["ماندهی پایانِ دوره", "", "", "", "", "", statement["closing_balance"]])
|
||||
subsidiary_rows.append([])
|
||||
ExcelExportService._write(
|
||||
ws,
|
||||
["تاریخ", "شماره سند", "نوع", "شرح", "بدهکار", "بستانکار", "مانده"],
|
||||
subsidiary_rows,
|
||||
money_columns={4, 5, 6},
|
||||
)
|
||||
|
||||
# --- برگه ۷: انبار ---
|
||||
ws = wb.create_sheet("انبار")
|
||||
ExcelExportService._write(
|
||||
ws,
|
||||
@@ -489,9 +522,14 @@ class LedgerBookService:
|
||||
end_date: date | None = None,
|
||||
carry_balance: bool = True,
|
||||
) -> "LedgerBook":
|
||||
"""فایل تازه باز میکند: فایلِ جاریِ قبلی بسته و فقط-خواندنی میشود،
|
||||
و اگر خواسته شود ماندهی پایانیِ آن بهعنوان ماندهی ابتدای فایلِ
|
||||
جدید منتقل میگردد (کارِ استانداردِ «انتقال مانده» سرِ سال مالی)."""
|
||||
"""فایل تازه باز میکند و آن را فایلِ جاری میکند؛ اگر خواسته شود
|
||||
ماندهی پایانیِ فایلِ قبلی بهعنوان ماندهی ابتدای فایلِ جدید منتقل
|
||||
میشود (کارِ استانداردِ «انتقال مانده» سرِ سال مالی).
|
||||
|
||||
برخلافِ قبل، فایلِ قبلی خودکار بایگانی (بسته) نمیشود — فقط از حالتِ
|
||||
«جاری» خارج میشود و همچنان در فهرست و قابلویرایش باقی میماند؛
|
||||
بایگانیکردنش یک اقدامِ جداگانه و دستی است (به LedgerBookService.
|
||||
archive_book مراجعه کن)."""
|
||||
from apps.bizapps.models import LedgerBook
|
||||
|
||||
previous = LedgerBook.objects.select_for_update().filter(user=user, is_active=True).first()
|
||||
@@ -500,9 +538,7 @@ class LedgerBookService:
|
||||
if carry_balance:
|
||||
opening = LedgerBookService.closing_balance(previous)
|
||||
previous.is_active = False
|
||||
previous.is_closed = True
|
||||
previous.closed_at = timezone.now()
|
||||
previous.save(update_fields=["is_active", "is_closed", "closed_at", "updated_at"])
|
||||
previous.save(update_fields=["is_active", "updated_at"])
|
||||
|
||||
return LedgerBook.objects.create(
|
||||
id=str(uuid.uuid4()),
|
||||
@@ -514,6 +550,18 @@ class LedgerBookService:
|
||||
is_active=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def archive_book(book: "LedgerBook") -> "LedgerBook":
|
||||
"""بایگانیکردنِ دستیِ یک فایل — از این پس فقط-خواندنی میشود.
|
||||
فایلِ جاری هم قابلبایگانی است (کاربر آگاهانه انتخاب کرده)، فقط بعد
|
||||
از آن یک فایلِ جاریِ تازه لازم است (اولین فراخوانیِ بعدیِ
|
||||
ensure_active_book خودش میسازد)."""
|
||||
book.is_active = False
|
||||
book.is_closed = True
|
||||
book.closed_at = timezone.now()
|
||||
book.save(update_fields=["is_active", "is_closed", "closed_at", "updated_at"])
|
||||
return book
|
||||
|
||||
@staticmethod
|
||||
def closing_balance(book: "LedgerBook") -> float:
|
||||
"""ماندهی پایانیِ یک فایل = ماندهی ابتدا + درآمدها − هزینهها."""
|
||||
|
||||
@@ -81,6 +81,9 @@ class LedgerEntrySerializer(serializers.ModelSerializer):
|
||||
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
|
||||
@@ -88,6 +91,8 @@ class LedgerEntrySerializer(serializers.ModelSerializer):
|
||||
"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",
|
||||
]
|
||||
@@ -112,7 +117,8 @@ class ProductSerializer(serializers.ModelSerializer):
|
||||
fields = [
|
||||
"id", "name", "sku", "category", "unit", "quantity",
|
||||
"unit_price", "purchase_price", "total_purchase_value",
|
||||
"low_stock_threshold", "is_low_stock", "notes", "updated_at",
|
||||
"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"]
|
||||
|
||||
@@ -153,7 +159,9 @@ class InvoiceSerializer(serializers.ModelSerializer):
|
||||
"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",
|
||||
"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",
|
||||
@@ -188,7 +196,17 @@ class CreateInvoiceSerializer(serializers.Serializer):
|
||||
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)
|
||||
|
||||
+127
-40
@@ -9,6 +9,7 @@ from django.utils import timezone
|
||||
from apps.account.models import User
|
||||
from apps.banker.models import Contact
|
||||
from apps.bizapps.models import (
|
||||
Account,
|
||||
Customer,
|
||||
Invoice,
|
||||
InvoiceItem,
|
||||
@@ -26,6 +27,7 @@ from apps.bizapps.api.accounting import (
|
||||
ACCOUNT_SALES,
|
||||
ChartOfAccountsService,
|
||||
ExcelExportService,
|
||||
SubsidiaryLedgerService,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -92,7 +94,9 @@ class InvoiceService:
|
||||
seller_name=data.get("seller_name", ""),
|
||||
seller_phone=data.get("seller_phone", ""),
|
||||
seller_address=data.get("seller_address", ""),
|
||||
discount_amount=data.get("discount_amount", 0),
|
||||
discount_percent=data.get("discount_percent", 0),
|
||||
extra_discount_label=data.get("extra_discount_label", ""),
|
||||
extra_discount_percent=data.get("extra_discount_percent", 0),
|
||||
tax_lines=data.get("tax_lines", []),
|
||||
payment_type=payment_type,
|
||||
shipping_method=data.get("shipping_method", Invoice.ShippingMethod.PICKUP),
|
||||
@@ -146,10 +150,27 @@ class InvoiceService:
|
||||
note=f"فروش فاکتور {invoice_number}",
|
||||
)
|
||||
|
||||
if invoice.discount_percent:
|
||||
invoice.discount_amount = subtotal * invoice.discount_percent / 100
|
||||
else:
|
||||
invoice.discount_amount = data.get("discount_amount", 0)
|
||||
|
||||
after_discount = subtotal - invoice.discount_amount
|
||||
if invoice.extra_discount_percent:
|
||||
invoice.extra_discount_amount = after_discount * invoice.extra_discount_percent / 100
|
||||
else:
|
||||
invoice.extra_discount_amount = data.get("extra_discount_amount", 0)
|
||||
|
||||
tax_amount = sum(subtotal * float(line.get("percent", 0)) / 100 for line in invoice.tax_lines)
|
||||
invoice.total_amount = subtotal
|
||||
invoice.final_amount = subtotal - invoice.discount_amount + tax_amount
|
||||
invoice.save(update_fields=["total_amount", "final_amount"])
|
||||
invoice.final_amount = (
|
||||
subtotal - invoice.discount_amount - invoice.extra_discount_amount + tax_amount
|
||||
)
|
||||
invoice.save(
|
||||
update_fields=[
|
||||
"total_amount", "discount_amount", "extra_discount_amount", "final_amount",
|
||||
]
|
||||
)
|
||||
|
||||
transaction_number = InvoiceService._next_sequenced_number(user, Transaction, "transaction_number", "TRX")
|
||||
is_credit = payment_type == Invoice.PaymentType.CREDIT
|
||||
@@ -396,6 +417,70 @@ class LedgerService:
|
||||
entry.account = account
|
||||
entry.save(update_fields=["account", "updated_at"])
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def post_entry_transaction(user: User, entry: LedgerEntry) -> None:
|
||||
"""سندِ دوطرفهی یک ثبتِ ساده (غیرِ فاکتوری) — قبلا فقط فاکتورها و
|
||||
اقساط سند میخوردند، پس ثبتِ مستقیمِ درآمد/هزینه (که کارِ اصلیِ
|
||||
بیشترِ کاربرهاست) هیچوقت وارد تراز حسابها/دفتر معین نمیشد. اینجا
|
||||
سندِ قبلیِ همین ردیف (اگر بود) پاک و دوباره ساخته میشود — سادهتر و
|
||||
کمخطاتر از آپدیتِ جزئی.
|
||||
|
||||
فاکتورهای فروش (linked_invoice) را عمدا رد میکند — سندشان از قبل،
|
||||
موقعِ ساختِ فاکتور، ساخته شده؛ دوبارهسندزدن یعنی دوبار حساب کردن.
|
||||
"""
|
||||
Transaction.objects.filter(reference_ledger_entry=entry).delete()
|
||||
if entry.linked_invoice_id:
|
||||
return
|
||||
|
||||
account = entry.account or LedgerService.account_for_category(user, entry.category)
|
||||
if account is None:
|
||||
return # دستهی ناشناخته یا خالی — چیزی برای سند زدن نیست.
|
||||
|
||||
is_debt = entry.category in LedgerEntry.DEBT_CATEGORIES
|
||||
amount = (entry.total_amount or entry.amount) if is_debt else entry.amount
|
||||
if not amount:
|
||||
return
|
||||
|
||||
if entry.category == LedgerEntry.Category.SALE_CREDIT:
|
||||
debit_ref = ChartOfAccountsService.get(user, ACCOUNT_RECEIVABLE)
|
||||
credit_ref = account
|
||||
elif entry.category == LedgerEntry.Category.PURCHASE_CREDIT:
|
||||
debit_ref = account
|
||||
credit_ref = ChartOfAccountsService.get(user, ACCOUNT_PAYABLE)
|
||||
elif entry.entry_type == LedgerEntry.EntryType.INCOME:
|
||||
debit_ref = ChartOfAccountsService.get(user, ACCOUNT_CASH)
|
||||
credit_ref = account
|
||||
else:
|
||||
debit_ref = account
|
||||
credit_ref = ChartOfAccountsService.get(user, ACCOUNT_CASH)
|
||||
|
||||
transaction_type = (
|
||||
Transaction.TransactionType.CREDIT_SALE
|
||||
if entry.category == LedgerEntry.Category.SALE_CREDIT
|
||||
else Transaction.TransactionType.PURCHASE
|
||||
if entry.category == LedgerEntry.Category.PURCHASE_CREDIT
|
||||
else Transaction.TransactionType.CASH_SALE
|
||||
if entry.entry_type == LedgerEntry.EntryType.INCOME
|
||||
else Transaction.TransactionType.EXPENSE
|
||||
)
|
||||
number = InvoiceService._next_sequenced_number(user, Transaction, "transaction_number", "LED")
|
||||
naive_dt = timezone.datetime.combine(entry.entry_date, timezone.datetime.min.time())
|
||||
Transaction.objects.create(
|
||||
id=str(uuid.uuid4()),
|
||||
user=user,
|
||||
transaction_number=number,
|
||||
transaction_date=timezone.make_aware(naive_dt) if timezone.is_naive(naive_dt) else naive_dt,
|
||||
reference_ledger_entry=entry,
|
||||
transaction_type=transaction_type,
|
||||
debit_account=debit_ref.code if debit_ref else "",
|
||||
credit_account=credit_ref.code if credit_ref else "",
|
||||
debit_account_ref=debit_ref,
|
||||
credit_account_ref=credit_ref,
|
||||
amount=amount,
|
||||
description=entry.title,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def post_installment_transaction(user: User, installment: LedgerInstallment) -> None:
|
||||
@@ -448,46 +533,48 @@ class LedgerService:
|
||||
|
||||
@staticmethod
|
||||
def export_csv(user: User, start=None, end=None) -> str:
|
||||
"""خروجیِ CSV ساده — برای وقتی کاربر فقط یک فایلِ سبکِ تکبرگی
|
||||
میخواهد. نسخهی کاملِ چندبرگی در export_xlsx است."""
|
||||
qs = LedgerEntry.objects.filter(user=user).order_by("entry_date").prefetch_related("installments")
|
||||
if start:
|
||||
qs = qs.filter(entry_date__gte=start)
|
||||
if end:
|
||||
qs = qs.filter(entry_date__lte=end)
|
||||
|
||||
"""خروجیِ CSV بهشکلِ واقعیِ دفتر معین — بهازای هر حسابِ معینِ
|
||||
دارای گردش: سربرگِ حساب، ماندهی ابتدای دوره، سندهای آن حساب
|
||||
بهترتیبِ تاریخ (با بدهکار/بستانکار/ماندهی در جریان)، و ماندهی
|
||||
پایانِ دوره — دقیقا همان ساختاری که در خروجیِ اکسلِ چندبرگی هم
|
||||
هست، فقط سبکتر و تکبرگی. قبلا این خروجی فقط یک دامپِ خامِ
|
||||
ردیفهای دفترِ ساده بود (بدونِ حساب/بدهکار/بستانکار/مانده) که هیچ
|
||||
شباهتی به دفتر معین نداشت.
|
||||
"""
|
||||
buf = io.StringIO()
|
||||
buf.write("") # BOM
|
||||
writer = csv.writer(buf)
|
||||
writer.writerow(
|
||||
[
|
||||
"تاریخ",
|
||||
"نوع",
|
||||
"دسته",
|
||||
"عنوان",
|
||||
"مبلغ",
|
||||
"طرف حساب",
|
||||
"شماره طرف حساب",
|
||||
"مبلغ کل تعهد",
|
||||
"پرداختشده",
|
||||
"مانده",
|
||||
"یادداشت",
|
||||
]
|
||||
)
|
||||
for e in qs:
|
||||
writer.writerow(["تاریخ", "شماره سند", "نوع", "شرح", "بدهکار", "بستانکار", "مانده"])
|
||||
|
||||
accounts = Account.objects.filter(user=user, level=Account.Level.MOIN).order_by("code")
|
||||
wrote_any_row = False
|
||||
for account in accounts:
|
||||
statement = SubsidiaryLedgerService.statement(user, account, start, end)
|
||||
if not statement["rows"]:
|
||||
continue
|
||||
wrote_any_row = True
|
||||
writer.writerow([f"{account.code} — {account.name}", "", "", "", "", "", ""])
|
||||
writer.writerow(
|
||||
[
|
||||
e.entry_date.isoformat(),
|
||||
e.get_entry_type_display(),
|
||||
e.get_category_display() if e.category else "",
|
||||
e.title,
|
||||
f"{e.amount:.0f}",
|
||||
e.counterparty_name,
|
||||
e.counterparty_phone,
|
||||
f"{e.total_amount:.0f}" if e.is_debt else "",
|
||||
f"{e.paid_amount:.0f}" if e.is_debt else "",
|
||||
f"{e.remaining_amount:.0f}" if e.is_debt else "",
|
||||
e.note,
|
||||
]
|
||||
["ماندهی ابتدای دوره", "", "", "", "", "", f"{statement['opening_balance']:.0f}"]
|
||||
)
|
||||
for row in statement["rows"]:
|
||||
row_date = row["date"]
|
||||
writer.writerow(
|
||||
[
|
||||
row_date.date().isoformat() if hasattr(row_date, "date") else row_date,
|
||||
row["number"],
|
||||
row["type"],
|
||||
row["description"],
|
||||
f"{row['debit']:.0f}" if row["debit"] else "",
|
||||
f"{row['credit']:.0f}" if row["credit"] else "",
|
||||
f"{row['balance']:.0f}",
|
||||
]
|
||||
)
|
||||
writer.writerow(
|
||||
["ماندهی پایانِ دوره", "", "", "", "", "", f"{statement['closing_balance']:.0f}"]
|
||||
)
|
||||
writer.writerow([])
|
||||
|
||||
if not wrote_any_row:
|
||||
writer.writerow(["در این بازه هیچ حسابی گردش نداشته.", "", "", "", "", "", ""])
|
||||
return buf.getvalue()
|
||||
|
||||
@@ -109,6 +109,7 @@ class LedgerEntryListCreateView(_OwnedMixin, generics.ListCreateAPIView):
|
||||
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)
|
||||
LedgerService.post_entry_transaction(self.request.user, entry)
|
||||
if entry.counterparty_phone:
|
||||
LedgerService.recompute_customer_balance(self.request.user, entry.counterparty_phone)
|
||||
|
||||
@@ -123,6 +124,7 @@ class LedgerEntryDetailView(_OwnedMixin, generics.RetrieveUpdateDestroyAPIView):
|
||||
def perform_update(self, serializer):
|
||||
entry = serializer.save()
|
||||
LedgerService.sync_entry_account(self.request.user, entry)
|
||||
LedgerService.post_entry_transaction(self.request.user, entry)
|
||||
if entry.counterparty_phone:
|
||||
LedgerService.recompute_customer_balance(self.request.user, entry.counterparty_phone)
|
||||
|
||||
@@ -473,6 +475,48 @@ class LedgerBookSummaryView(APIView):
|
||||
)
|
||||
|
||||
|
||||
class LedgerBookDetailView(generics.RetrieveUpdateDestroyAPIView):
|
||||
"""ویرایش/حذفِ یک فایل دفتر. فایلِ بایگانیشده فقط-خواندنی است — نه
|
||||
ویرایش و نه حذف. حذف فقط برای فایلِ خالی (بدون هیچ ثبتی) مجاز است تا
|
||||
دادهی مالی با حذفِ فایل بهطور آبشاری از بین نرود."""
|
||||
|
||||
serializer_class = LedgerBookSerializer
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def get_queryset(self):
|
||||
return LedgerBook.objects.filter(user=self.request.user)
|
||||
|
||||
def _guard_editable(self, book: LedgerBook):
|
||||
if book.is_closed:
|
||||
raise ValidationError("فایلِ بایگانیشده فقط-خواندنی است.")
|
||||
|
||||
def perform_update(self, serializer):
|
||||
self._guard_editable(serializer.instance)
|
||||
serializer.save()
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
self._guard_editable(instance)
|
||||
if instance.entries.exists():
|
||||
raise ValidationError("فقط فایلِ خالی (بدون هیچ ثبتی) قابلحذف است.")
|
||||
instance.delete()
|
||||
|
||||
|
||||
class LedgerBookArchiveView(APIView):
|
||||
"""بایگانیکردنِ دستیِ یک فایل دفتر — از این پس فقط-خواندنی میشود و در
|
||||
انتخابِ فایلِ دفتر برای فاکتورهای تازه نشان داده نمیشود."""
|
||||
|
||||
permission_classes = [IsAuthenticated]
|
||||
|
||||
def post(self, request, pk):
|
||||
book = LedgerBook.objects.filter(user=request.user, pk=pk).first()
|
||||
if book is None:
|
||||
raise NotFound("فایل دفتر یافت نشد.")
|
||||
if book.is_closed:
|
||||
raise ValidationError("این فایل قبلا بایگانی شده.")
|
||||
book = LedgerBookService.archive_book(book)
|
||||
return Response(LedgerBookSerializer(book).data)
|
||||
|
||||
|
||||
class SubsidiaryLedgerView(APIView):
|
||||
"""دفتر معین یک حساب — گردشِ حساب با ماندهی در جریان."""
|
||||
|
||||
|
||||
Reference in New Issue
Block a user