831e656fa3
- 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)
154 lines
7.7 KiB
Python
154 lines
7.7 KiB
Python
from django.conf import settings
|
|
from django.db import models
|
|
from django.utils.translation import gettext_lazy as _
|
|
|
|
# این مدلها معادل دقیق دیتابیس محلی (SQLite) اپ فلاتر هستند تا اطلاعات
|
|
# مخاطبین/گروهها/قالبهای کاربر روی سرور هم پشتیبانگیری شود و با تعویض
|
|
# گوشی از دست نرود. آیدیها همان UUID تولیدشده در خود اپ هستند (نه
|
|
# auto-increment) تا هماهنگی هنگام بکاپ/ریستور ساده باشد.
|
|
#
|
|
# فیلدهای دلخواه (ContactField) و قالبها (MessageTemplate) به یک گروه
|
|
# (ContactGroup) تعلق دارند — نه سراسری. مثال: گروه «تور» فیلد «شماره
|
|
# گذرنامه» دارد، گروه دیگری «کد ملی». هر کاربر دقیقاً یک گروه با
|
|
# is_general=True دارد که فیلد سفارشی ندارد و قالبهای آن فقط از فیلدهای
|
|
# پایه (نام/شماره) استفاده میکنند — برای کاربرانی که نیاز به پیچیدگی ندارند.
|
|
|
|
|
|
class ContactGroup(models.Model):
|
|
id = models.CharField(max_length=36, primary_key=True)
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contact_groups")
|
|
name = models.CharField(max_length=150, verbose_name=_("نام گروه"))
|
|
color = models.BigIntegerField(verbose_name=_("رنگ (ARGB)"))
|
|
is_general = models.BooleanField(
|
|
default=False,
|
|
verbose_name=_("گروه عمومی"),
|
|
help_text=_("گروه پیشفرض بدون فیلد سفارشی؛ هر کاربر دقیقاً یکی از اینها دارد."),
|
|
)
|
|
# لیستی از contact.id های عضو این گروه
|
|
member_ids = models.JSONField(default=list, blank=True, verbose_name=_("اعضا"))
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = _("گروه")
|
|
verbose_name_plural = _("گروهها")
|
|
ordering = ["-updated_at"]
|
|
constraints = [
|
|
models.UniqueConstraint(
|
|
fields=["user"],
|
|
condition=models.Q(is_general=True),
|
|
name="one_general_group_per_user",
|
|
)
|
|
]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class ContactField(models.Model):
|
|
id = models.CharField(max_length=36, primary_key=True)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contact_fields"
|
|
)
|
|
group = models.ForeignKey(ContactGroup, on_delete=models.CASCADE, related_name="fields")
|
|
label = models.CharField(max_length=100, verbose_name=_("عنوان فیلد"))
|
|
sort_order = models.PositiveIntegerField(default=0)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = _("فیلد دلخواه مخاطب")
|
|
verbose_name_plural = _("فیلدهای دلخواه مخاطبین")
|
|
ordering = ["group", "sort_order"]
|
|
|
|
def __str__(self):
|
|
return f"{self.label} ({self.group.name})"
|
|
|
|
|
|
class Contact(models.Model):
|
|
id = models.CharField(max_length=36, primary_key=True)
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="contacts")
|
|
name = models.CharField(max_length=150, verbose_name=_("نام"))
|
|
phone = models.CharField(max_length=20, verbose_name=_("شماره موبایل"))
|
|
device_contact_id = models.CharField(max_length=100, blank=True, null=True)
|
|
# {field_id: value} — معادل جدول contact_field_values در اپ. فیلدهای
|
|
# مربوط به گروههایی هستند که این مخاطب عضوشان است (member_ids گروه)
|
|
field_values = models.JSONField(default=dict, blank=True, verbose_name=_("مقادیر فیلدهای دلخواه"))
|
|
# {channel: external_id} — مثلا {"telegram": "123456789"} — برای ارسال
|
|
# خودکار از طریق Bot API فقط وقتی گیرنده قبلا چت با بات را شروع کرده
|
|
channel_ids = models.JSONField(default=dict, blank=True, verbose_name=_("شناسهی کانالها"))
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = _("مخاطب")
|
|
verbose_name_plural = _("مخاطبین")
|
|
ordering = ["name"]
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.phone})"
|
|
|
|
|
|
class MessageTemplate(models.Model):
|
|
class Mode(models.TextChoices):
|
|
MERGE = "merge", _("قالببندی")
|
|
QUICK = "quick", _("تمپلیت آماده")
|
|
|
|
id = models.CharField(max_length=36, primary_key=True)
|
|
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="templates")
|
|
# null = قالب عمومی (فقط فیلدهای پایهی نام/شماره)
|
|
group = models.ForeignKey(
|
|
ContactGroup, on_delete=models.CASCADE, related_name="templates", null=True, blank=True
|
|
)
|
|
name = models.CharField(max_length=150, verbose_name=_("نام قالب"))
|
|
body = models.TextField(verbose_name=_("متن قالب"))
|
|
mode = models.CharField(max_length=10, choices=Mode.choices)
|
|
# فیلدهای «زمان اجرا» — مقدارشان از دیتای مخاطب خوانده نمیشود، هر بار
|
|
# موقع ارسال (یا رسیدن سفارش به مرحلهای که این قالب رویش است) از کاربر
|
|
# پرسیده میشود. لیست است چون یک قالب میتواند چند فیلدِ اینچنینی
|
|
# داشته باشد (مثلا هم «کد رهگیری» هم «مبلغ نهایی»).
|
|
runtime_field_labels = models.JSONField(default=list, blank=True, verbose_name=_("فیلدهای زمان اجرا"))
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = _("قالب پیام")
|
|
verbose_name_plural = _("قالبهای پیام")
|
|
ordering = ["-updated_at"]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class StructuredTemplate(models.Model):
|
|
"""قالب کلید-مقدار — برای گزارشهای تکرارشونده مثل «امروز چقدر از هر
|
|
کالا موجود دارم». برخلاف MessageTemplate (متن آزاد با {فیلد})، اینجا
|
|
فقط یک لیست از عنوانهاست (مثلا «کلم»، «کاهو») که اپ یکییکی مقدارشان
|
|
را از کاربر میپرسد و در نهایت خطبهخط بهشکل «عنوان: مقدار» میفرستد."""
|
|
|
|
id = models.CharField(max_length=36, primary_key=True)
|
|
user = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name="structured_templates"
|
|
)
|
|
name = models.CharField(max_length=150, verbose_name=_("نام قالب"))
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
verbose_name = _("قالب کلید-مقدار")
|
|
verbose_name_plural = _("قالبهای کلید-مقدار")
|
|
ordering = ["-updated_at"]
|
|
|
|
def __str__(self):
|
|
return self.name
|
|
|
|
|
|
class StructuredTemplateField(models.Model):
|
|
id = models.CharField(max_length=36, primary_key=True)
|
|
template = models.ForeignKey(StructuredTemplate, on_delete=models.CASCADE, related_name="fields")
|
|
label = models.CharField(max_length=100, verbose_name=_("عنوان"))
|
|
sort_order = models.PositiveIntegerField(default=0)
|
|
|
|
class Meta:
|
|
verbose_name = _("فیلد قالب کلید-مقدار")
|
|
verbose_name_plural = _("فیلدهای قالب کلید-مقدار")
|
|
ordering = ["template", "sort_order"]
|
|
|
|
def __str__(self):
|
|
return f"{self.label} ({self.template.name})"
|