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:
@@ -181,6 +181,9 @@ class Product(models.Model):
|
||||
# «ارزش کل انبار» بدون پیمایشِ همهی کالاها قابلجمعزدن باشد.
|
||||
total_purchase_value = models.FloatField(default=0, verbose_name=_("ارزش موجودی"))
|
||||
low_stock_threshold = models.FloatField(default=0, verbose_name=_("آستانهی هشدار موجودی کم"))
|
||||
is_active = models.BooleanField(default=True, verbose_name=_("فعال"))
|
||||
production_date = models.DateField(null=True, blank=True, verbose_name=_("تاریخ تولید"))
|
||||
expiry_date = models.DateField(null=True, blank=True, verbose_name=_("تاریخ انقضا"))
|
||||
notes = models.TextField(blank=True, verbose_name=_("یادداشت"))
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
@@ -257,7 +260,20 @@ class Invoice(models.Model):
|
||||
seller_phone = models.CharField(max_length=20, blank=True, verbose_name=_("شماره فروشنده"))
|
||||
seller_address = models.CharField(max_length=255, blank=True, verbose_name=_("آدرس فروشنده"))
|
||||
total_amount = models.FloatField(default=0, verbose_name=_("جمع کل"))
|
||||
# درصدِ تخفیفِ کلیِ فاکتور — منبعِ حقیقت همین درصد است (همالگو با
|
||||
# InvoiceItem.discount_percent)؛ discount_amount فقط cache شده تا در
|
||||
# گزارش/اکسل بدون محاسبهی دوباره قابلنمایش باشد.
|
||||
discount_percent = models.FloatField(default=0, verbose_name=_("درصد تخفیف"))
|
||||
discount_amount = models.FloatField(default=0, verbose_name=_("تخفیف"))
|
||||
# تخفیفِ ویژه/دورهای (مثلا «فروش فوقالعاده») — جدا از تخفیفِ معمولیِ
|
||||
# بالا تا هر دو در خروجی فاکتور با عنوانِ خودشان دیده شوند. یا با درصد
|
||||
# (روی مبلغ بعد از تخفیف معمولی) یا با مبلغ ثابت پر میشود، نه هر دو.
|
||||
extra_discount_label = models.CharField(
|
||||
max_length=100, blank=True, verbose_name=_("عنوان تخفیف ویژه"),
|
||||
help_text=_("مثلا: فروش فوقالعاده، تخفیف دورهای"),
|
||||
)
|
||||
extra_discount_percent = models.FloatField(default=0, verbose_name=_("درصد تخفیف ویژه"))
|
||||
extra_discount_amount = models.FloatField(default=0, verbose_name=_("مبلغ تخفیف ویژه"))
|
||||
# هر ردیف: {"title": "مالیات بر ارزش افزوده", "percent": 9}
|
||||
tax_lines = models.JSONField(default=list, blank=True, verbose_name=_("ردیفهای مالیات"))
|
||||
final_amount = models.FloatField(default=0, verbose_name=_("مبلغ نهایی"))
|
||||
@@ -326,6 +342,12 @@ class Transaction(models.Model):
|
||||
reference_invoice = models.ForeignKey(
|
||||
Invoice, null=True, blank=True, on_delete=models.SET_NULL, related_name="transactions"
|
||||
)
|
||||
# سندِ خودکارِ یک ردیفِ دفترِ ساده (غیرِ فاکتوری) — CASCADE عمدی است: اگر
|
||||
# خودِ ردیف حذف شود، سندش هم باید حذف شود، وگرنه تراز حسابها یک سندِ
|
||||
# یتیم و اشتباه نگه میدارد.
|
||||
reference_ledger_entry = models.ForeignKey(
|
||||
"LedgerEntry", null=True, blank=True, on_delete=models.CASCADE, related_name="transactions"
|
||||
)
|
||||
transaction_type = models.CharField(max_length=20, choices=TransactionType.choices)
|
||||
# کدِ حسابِ متنی برای سازگاری با دادهی قدیمی نگه داشته شده؛ debit/credit
|
||||
# بهصورتِ FK به Account همان چیزی است که گزارشهای جدید از آن میخوانند.
|
||||
@@ -403,6 +425,14 @@ class LedgerEntry(models.Model):
|
||||
)
|
||||
entry_type = models.CharField(max_length=10, choices=EntryType.choices, verbose_name=_("نوع"))
|
||||
title = models.CharField(max_length=150, verbose_name=_("عنوان"))
|
||||
# amount همیشه مبلغِ نهاییِ ثبتشده است (همان چیزی که در گزارش/جمعِ
|
||||
# درآمد-هزینه استفاده میشود) — سه فیلدِ زیر فقط یک ماشینحسابِ اختیاری
|
||||
# برای رسیدن به آن هستند، تا کاربر مجبور نباشد خودش دستی حساب کند:
|
||||
# تخفیف روی مبلغِ پایه (پیش از مالیات) اعمال میشود، بعد مالیات روی
|
||||
# مبلغِ تخفیفخورده — یعنی دقیقا «تخفیف خارج از مالیات و ارزشافزوده».
|
||||
base_amount = models.FloatField(default=0, blank=True, verbose_name=_("مبلغ پایه (پیش از تخفیف/مالیات)"))
|
||||
discount_percent = models.FloatField(default=0, blank=True, verbose_name=_("درصد تخفیف (پیش از مالیات)"))
|
||||
tax_percent = models.FloatField(default=0, blank=True, verbose_name=_("درصد مالیات/ارزشافزوده"))
|
||||
amount = models.FloatField(verbose_name=_("مبلغ"))
|
||||
# max_length=100 (نه تنگتر) عمداً — این فیلد قبلا آزاد بود و دادهی
|
||||
# واقعیِ قدیمی با متنِ فارسیِ دلخواه دارد؛ تنگکردنش موقعِ مهاجرت روی
|
||||
@@ -438,6 +468,24 @@ class LedgerEntry(models.Model):
|
||||
def is_debt(self) -> bool:
|
||||
return self.category in self.DEBT_CATEGORIES
|
||||
|
||||
@property
|
||||
def discount_amount(self) -> float:
|
||||
return (self.base_amount or 0) * (self.discount_percent or 0) / 100
|
||||
|
||||
@property
|
||||
def net_before_tax(self) -> float:
|
||||
return (self.base_amount or 0) - self.discount_amount
|
||||
|
||||
@property
|
||||
def tax_amount(self) -> float:
|
||||
return self.net_before_tax * (self.tax_percent or 0) / 100
|
||||
|
||||
@property
|
||||
def computed_amount(self) -> float:
|
||||
"""مبلغِ نهایی که از روی مبلغِ پایه، تخفیف و مالیات محاسبه میشود —
|
||||
برای پیشنمایش سمتِ کلاینت؛ مقدارِ واقعا ذخیرهشده همیشه amount است."""
|
||||
return self.net_before_tax + self.tax_amount
|
||||
|
||||
@property
|
||||
def professional_type(self) -> str:
|
||||
"""ترجمهی دستهی «عادی» به اصطلاحِ حسابداریِ حرفهای."""
|
||||
|
||||
Reference in New Issue
Block a user