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:
2026-09-15 23:18:05 +03:30
parent 61b7c91b02
commit 831e656fa3
32 changed files with 1036 additions and 77 deletions
+43 -10
View File
@@ -177,19 +177,24 @@ class LedgerServiceTests(TestCase):
self.customer.refresh_from_db()
self.assertEqual(self.customer.outstanding_balance, 0)
def _create_and_post(self, **kwargs):
"""معادلِ دستیِ کاری که ویوی API انجام می‌دهد: بعد از ساختِ ردیف،
حساب را هماهنگ و سندِ دوطرفه‌اش را می‌زند — بدونِ این سند، ردیف در
هیچ گزارشِ حرفه‌ای (تراز/دفتر معین/CSV) دیده نمی‌شود."""
entry = LedgerEntry.objects.create(id=str(uuid.uuid4()), user=self.user, **kwargs)
LedgerService.sync_entry_account(self.user, entry)
LedgerService.post_entry_transaction(self.user, entry)
return entry
def test_export_csv_filters_by_date_range(self):
LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
self._create_and_post(
entry_type=LedgerEntry.EntryType.INCOME,
title="داخل بازه",
amount=100,
category=LedgerEntry.Category.SALE_CASH,
entry_date=date(2026, 1, 15),
)
LedgerEntry.objects.create(
id=str(uuid.uuid4()),
user=self.user,
self._create_and_post(
entry_type=LedgerEntry.EntryType.EXPENSE,
title="خارج از بازه",
amount=50,
@@ -200,6 +205,31 @@ class LedgerServiceTests(TestCase):
self.assertIn("داخل بازه", csv_text)
self.assertNotIn("خارج از بازه", csv_text)
def test_post_entry_transaction_covers_plain_income_and_expense(self):
"""قبلا فقط فاکتور/قسط سند می‌خورد؛ یک ثبتِ سادهِ «فروش نقدی» یا
«اجاره» (که کارِ روزمرهِ اکثرِ کاربرهاست) اصلا در ترازِ حساب‌ها
دیده نمی‌شد. این تست دقیقا همان رگرسیون را قفل می‌کند."""
self._create_and_post(
entry_type=LedgerEntry.EntryType.INCOME,
title="فروش نقدی امروز",
amount=200,
category=LedgerEntry.Category.SALE_CASH,
entry_date=date.today(),
)
self._create_and_post(
entry_type=LedgerEntry.EntryType.EXPENSE,
title="اجاره مغازه",
amount=80,
category=LedgerEntry.Category.RENT,
entry_date=date.today(),
)
rows = ChartOfAccountsService.trial_balance(self.user)
by_code = {r["code"]: r for r in rows}
self.assertIn(ACCOUNT_CASH, by_code)
# فروشِ نقدی صندوق را بدهکار می‌کند، اجاره بستانکار — مانده‌ی خالص
# صندوق باید ۲۰۰ منهای ۸۰ باشد.
self.assertEqual(by_code[ACCOUNT_CASH]["balance"], 120)
class InvoiceLedgerIntegrationTests(TestCase):
"""اتصالِ فاکتور <-> دفتر: طبق درخواست، فقط فروشِ قسطی باید خودکار یک
@@ -553,7 +583,7 @@ class ExcelExportTests(TestCase):
wb = self._workbook()
self.assertEqual(
wb.sheetnames,
["خلاصه", "دفتر روزنامه", "تراز حساب‌ها", "بدهکاران", "فاکتورها", "انبار"],
["خلاصه", "دفتر روزنامه", "تراز حساب‌ها", "بدهکاران", "فاکتورها", "دفتر معین", "انبار"],
)
def test_sheets_are_right_to_left(self):
@@ -825,15 +855,18 @@ class LedgerBookTests(TestCase):
orphan.refresh_from_db()
self.assertEqual(orphan.book_id, book.id)
def test_opening_a_new_book_closes_the_previous_one(self):
def test_opening_a_new_book_deactivates_but_does_not_archive_the_previous_one(self):
"""بایگانی‌کردن یک اقدامِ دستیِ جداگانه است (LedgerBookService.
archive_book) — باز کردنِ فایلِ تازه فقط فایلِ قبلی را از حالتِ
«جاری» خارج می‌کند، نه این‌که خودکار بایگانی‌اش کند."""
old = LedgerBookService.ensure_active_book(self.user)
new = LedgerBookService.open_new_book(
self.user, name="سال ۱۴۰۶", start_date=date(2027, 3, 21)
)
old.refresh_from_db()
self.assertFalse(old.is_active)
self.assertTrue(old.is_closed)
self.assertIsNotNone(old.closed_at)
self.assertFalse(old.is_closed)
self.assertIsNone(old.closed_at)
self.assertTrue(new.is_active)
# همیشه دقیقا یک فایل جاری
self.assertEqual(LedgerBook.objects.filter(user=self.user, is_active=True).count(), 1)