From ad965f034666ca58d1985d9ce13061fe786f9bfa Mon Sep 17 00:00:00 2001 From: mobiniqow Date: Tue, 15 Sep 2026 09:16:20 +0330 Subject: [PATCH] adding --- apps/account/migrations/0001_initial.py | 60 -- apps/banker/migrations/0001_initial.py | 117 --- apps/billing/migrations/0001_initial.py | 128 --- .../migrations/0002_featureplan_is_free.py | 18 - .../migrations/0003_seed_feature_plans.py | 71 -- .../migrations/0004_free_tier_split.py | 53 -- apps/bizapps/migrations/0001_initial.py | 246 ----- ..._social_handle_customer_social_platform.py | 23 - ...03_ledgerbook_ledgerentry_book_and_more.py | 46 - apps/landing/migrations/0001_initial.py | 35 - .../landing/migrations/0002_articles_batch.py | 898 ------------------ .../0003_restore_earlier_articles.py | 343 ------- apps/messaging/migrations/0001_initial.py | 54 -- apps/notifications/migrations/0001_initial.py | 46 - apps/orders/migrations/0001_initial.py | 72 -- apps/telemetry/migrations/0001_initial.py | 114 --- 16 files changed, 2324 deletions(-) delete mode 100644 apps/account/migrations/0001_initial.py delete mode 100644 apps/banker/migrations/0001_initial.py delete mode 100644 apps/billing/migrations/0001_initial.py delete mode 100644 apps/billing/migrations/0002_featureplan_is_free.py delete mode 100644 apps/billing/migrations/0003_seed_feature_plans.py delete mode 100644 apps/billing/migrations/0004_free_tier_split.py delete mode 100644 apps/bizapps/migrations/0001_initial.py delete mode 100644 apps/bizapps/migrations/0002_customer_social_handle_customer_social_platform.py delete mode 100644 apps/bizapps/migrations/0003_ledgerbook_ledgerentry_book_and_more.py delete mode 100644 apps/landing/migrations/0001_initial.py delete mode 100644 apps/landing/migrations/0002_articles_batch.py delete mode 100644 apps/landing/migrations/0003_restore_earlier_articles.py delete mode 100644 apps/messaging/migrations/0001_initial.py delete mode 100644 apps/notifications/migrations/0001_initial.py delete mode 100644 apps/orders/migrations/0001_initial.py delete mode 100644 apps/telemetry/migrations/0001_initial.py diff --git a/apps/account/migrations/0001_initial.py b/apps/account/migrations/0001_initial.py deleted file mode 100644 index f346480..0000000 --- a/apps/account/migrations/0001_initial.py +++ /dev/null @@ -1,60 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -import apps.account.models -import django.db.models.deletion -import django.utils.timezone -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ] - - operations = [ - migrations.CreateModel( - name='TermsAndConditions', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('content', models.TextField(blank=True, help_text='این متن در دیالوگ «قوانین و مقررات» صفحه\u200cی ورود نشان داده می\u200cشود.', verbose_name='متن قوانین و مقررات')), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'قوانین و مقررات', - 'verbose_name_plural': 'قوانین و مقررات', - }, - ), - migrations.CreateModel( - name='User', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('phone', models.CharField(max_length=11, unique=True, validators=[apps.account.models.phone_validator], verbose_name='شماره موبایل')), - ('name', models.CharField(blank=True, max_length=100, verbose_name='نام')), - ('state', models.CharField(choices=[('inactive', 'غیرفعال'), ('active', 'فعال'), ('blocked', 'مسدود شده')], default='inactive', max_length=10, verbose_name='وضعیت کاربر')), - ('otp_code', models.CharField(blank=True, max_length=128, null=True, verbose_name='کد OTP (هش\u200cشده)')), - ('otp_expiry', models.DateTimeField(blank=True, null=True, verbose_name='انقضای OTP')), - ('is_active', models.BooleanField(default=True, verbose_name='فعال')), - ('is_staff', models.BooleanField(default=False, verbose_name='کارمند')), - ('created_at', models.DateTimeField(default=django.utils.timezone.now, verbose_name='تاریخ ایجاد')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='تاریخ بروزرسانی')), - ('gem_balance', models.PositiveIntegerField(default=0, verbose_name='موجودی جم')), - ('trial_expires_at', models.DateTimeField(blank=True, default=None, null=True, verbose_name='پایان دوره\u200cی رایگان')), - ('referral_code', models.CharField(blank=True, max_length=8, unique=True, verbose_name='کد معرف')), - ('referral_bonus_granted', models.BooleanField(default=False, verbose_name='پاداش معرفی داده\u200cشد')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('referred_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='referrals', to=settings.AUTH_USER_MODEL, verbose_name='معرف')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'کاربر', - 'verbose_name_plural': 'کاربران', - }, - ), - ] diff --git a/apps/banker/migrations/0001_initial.py b/apps/banker/migrations/0001_initial.py deleted file mode 100644 index 3be760e..0000000 --- a/apps/banker/migrations/0001_initial.py +++ /dev/null @@ -1,117 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Contact', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=150, verbose_name='نام')), - ('phone', models.CharField(max_length=20, verbose_name='شماره موبایل')), - ('device_contact_id', models.CharField(blank=True, max_length=100, null=True)), - ('field_values', models.JSONField(blank=True, default=dict, verbose_name='مقادیر فیلدهای دلخواه')), - ('channel_ids', models.JSONField(blank=True, default=dict, verbose_name='شناسه\u200cی کانال\u200cها')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contacts', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'مخاطب', - 'verbose_name_plural': 'مخاطبین', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='ContactGroup', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=150, verbose_name='نام گروه')), - ('color', models.BigIntegerField(verbose_name='رنگ (ARGB)')), - ('is_general', models.BooleanField(default=False, help_text='گروه پیش\u200cفرض بدون فیلد سفارشی؛ هر کاربر دقیقاً یکی از این\u200cها دارد.', verbose_name='گروه عمومی')), - ('member_ids', models.JSONField(blank=True, default=list, verbose_name='اعضا')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_groups', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'گروه', - 'verbose_name_plural': 'گروه\u200cها', - 'ordering': ['-updated_at'], - }, - ), - migrations.CreateModel( - name='ContactField', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('label', models.CharField(max_length=100, verbose_name='عنوان فیلد')), - ('sort_order', models.PositiveIntegerField(default=0)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_fields', to=settings.AUTH_USER_MODEL)), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='banker.contactgroup')), - ], - options={ - 'verbose_name': 'فیلد دلخواه مخاطب', - 'verbose_name_plural': 'فیلدهای دلخواه مخاطبین', - 'ordering': ['group', 'sort_order'], - }, - ), - migrations.CreateModel( - name='MessageTemplate', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=150, verbose_name='نام قالب')), - ('body', models.TextField(verbose_name='متن قالب')), - ('mode', models.CharField(choices=[('merge', 'قالب\u200cبندی'), ('quick', 'تمپلیت آماده')], max_length=10)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='templates', to='banker.contactgroup')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='templates', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'قالب پیام', - 'verbose_name_plural': 'قالب\u200cهای پیام', - 'ordering': ['-updated_at'], - }, - ), - migrations.CreateModel( - name='StructuredTemplate', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=150, verbose_name='نام قالب')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='structured_templates', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'قالب کلید-مقدار', - 'verbose_name_plural': 'قالب\u200cهای کلید-مقدار', - 'ordering': ['-updated_at'], - }, - ), - migrations.CreateModel( - name='StructuredTemplateField', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('label', models.CharField(max_length=100, verbose_name='عنوان')), - ('sort_order', models.PositiveIntegerField(default=0)), - ('template', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='fields', to='banker.structuredtemplate')), - ], - options={ - 'verbose_name': 'فیلد قالب کلید-مقدار', - 'verbose_name_plural': 'فیلدهای قالب کلید-مقدار', - 'ordering': ['template', 'sort_order'], - }, - ), - migrations.AddConstraint( - model_name='contactgroup', - constraint=models.UniqueConstraint(condition=models.Q(('is_general', True)), fields=('user',), name='one_general_group_per_user'), - ), - ] diff --git a/apps/billing/migrations/0001_initial.py b/apps/billing/migrations/0001_initial.py deleted file mode 100644 index 1791250..0000000 --- a/apps/billing/migrations/0001_initial.py +++ /dev/null @@ -1,128 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='BazaarSettings', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('access_token', models.CharField(blank=True, help_text='یا توکن ثابت «روش جدید» پیشخان را مستقیم اینجا بچسبان، یا با دکمه\u200cی اتصال زیر بگذار خودش (روش قدیمی/OAuth) پر و بعدا خودکار تمدید شود.', max_length=255, verbose_name='Access Token')), - ('refresh_token', models.CharField(blank=True, help_text='فقط برای روش OAuth — با دکمه\u200cی اتصال خودش پر می\u200cشود، دستی لازم نیست.', max_length=255, verbose_name='Refresh Token')), - ('access_token_expires_at', models.DateTimeField(blank=True, help_text='اگر خالی باشد یعنی توکن ثابتِ روش جدید است و هیچ\u200cوقت خودکار تمدید نمی\u200cشود. اگر پر باشد یعنی از OAuth آمده و پیش از انقضا خودش تمدید خواهد شد.', null=True, verbose_name='انقضای Access Token (فقط OAuth)')), - ('client_id', models.CharField(blank=True, max_length=255, verbose_name='Client ID (روش قدیمی/OAuth)')), - ('client_secret', models.CharField(blank=True, max_length=255, verbose_name='Client Secret (روش قدیمی/OAuth)')), - ('package_name', models.CharField(blank=True, max_length=150, verbose_name='application id اپ (مثلا ir.example.app)')), - ('dynamic_discount_secret', models.CharField(blank=True, help_text='از پیشخان → برنامه\u200cات → پرداخت درون\u200cبرنامه\u200cای → تخفیف پویا. فقط سمت سرور استفاده می\u200cشود (apps.billing.providers.dynamic_discount) — هیچ\u200cوقت نباید در اپ فلاتر ذخیره شود، وگرنه کلید افشا می\u200cشود و کافه\u200cبازار مسئولیتی نمی\u200cپذیرد.', max_length=255, verbose_name='کلید امضای تخفیف پویا')), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'تنظیمات کافه\u200cبازار', - 'verbose_name_plural': 'تنظیمات کافه\u200cبازار', - }, - ), - migrations.CreateModel( - name='FeaturePlan', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('key', models.SlugField(unique=True, verbose_name='کلید ریزاپ')), - ('name', models.CharField(max_length=150, verbose_name='نام نمایشی')), - ('is_purchasable', models.BooleanField(default=True, verbose_name='قابل\u200cفروش')), - ], - options={ - 'verbose_name': 'ریزاپ قابل\u200cفعال\u200cسازی', - 'verbose_name_plural': 'ریزاپ\u200cهای قابل\u200cفعال\u200cسازی', - }, - ), - migrations.CreateModel( - name='GemPack', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('bazaar_sku', models.CharField(max_length=100, unique=True, verbose_name='SKU کافه\u200cبازار')), - ('display_name', models.CharField(max_length=150, verbose_name='نام نمایشی')), - ('gem_amount', models.PositiveIntegerField(verbose_name='تعداد جم')), - ('is_active', models.BooleanField(default=True, verbose_name='قابل\u200cخرید')), - ('sort_order', models.PositiveIntegerField(default=0)), - ], - options={ - 'verbose_name': 'بسته\u200cی جم', - 'verbose_name_plural': 'بسته\u200cهای جم', - 'ordering': ['sort_order', 'gem_amount'], - }, - ), - migrations.CreateModel( - name='BazaarPurchase', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('purchase_token', models.CharField(max_length=255, unique=True, verbose_name='Purchase Token')), - ('status', models.CharField(choices=[('pending', 'در انتظار تایید'), ('verified', 'تاییدشده'), ('failed', 'ناموفق')], default='pending', max_length=10)), - ('gems_granted', models.PositiveIntegerField(default=0)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('verified_at', models.DateTimeField(blank=True, null=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bazaar_purchases', to=settings.AUTH_USER_MODEL)), - ('gem_pack', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='+', to='billing.gempack')), - ], - options={ - 'verbose_name': 'خرید کافه\u200cبازار', - 'verbose_name_plural': 'خریدهای کافه\u200cبازار', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='GemTransaction', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount', models.IntegerField(verbose_name='مقدار (مثبت/منفی)')), - ('reason', models.CharField(choices=[('purchase', 'خرید از کافه\u200cبازار'), ('feature_activation', 'فعال\u200cسازی ریزاپ'), ('referral_bonus', 'پاداش معرفی'), ('referral_milestone', 'پاداش دستاورد معرفی'), ('admin_adjustment', 'اصلاح دستی ادمین')], max_length=30)), - ('note', models.CharField(blank=True, max_length=255)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='gem_transactions', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'تراکنش جم', - 'verbose_name_plural': 'تراکنش\u200cهای جم', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='FeatureActivation', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('expires_at', models.DateTimeField(verbose_name='تا تاریخ')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='feature_activations', to=settings.AUTH_USER_MODEL)), - ('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='billing.featureplan')), - ], - options={ - 'verbose_name': 'فعال\u200cسازی ریزاپ', - 'verbose_name_plural': 'فعال\u200cسازی\u200cهای ریزاپ', - 'constraints': [models.UniqueConstraint(fields=('user', 'feature'), name='unique_user_feature_activation')], - }, - ), - migrations.CreateModel( - name='FeaturePlanTier', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('duration_months', models.PositiveSmallIntegerField(verbose_name='مدت (ماه)')), - ('gem_price', models.PositiveIntegerField(verbose_name='قیمت (جم)')), - ('feature', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tiers', to='billing.featureplan')), - ], - options={ - 'verbose_name': 'پلن قیمتی', - 'verbose_name_plural': 'پلن\u200cهای قیمتی', - 'ordering': ['feature', 'duration_months'], - 'constraints': [models.UniqueConstraint(fields=('feature', 'duration_months'), name='unique_feature_duration')], - }, - ), - ] diff --git a/apps/billing/migrations/0002_featureplan_is_free.py b/apps/billing/migrations/0002_featureplan_is_free.py deleted file mode 100644 index 98e1e90..0000000 --- a/apps/billing/migrations/0002_featureplan_is_free.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 11:45 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('billing', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='featureplan', - name='is_free', - field=models.BooleanField(default=False, help_text='اگر روشن باشد، این ابزار بدون جم و بدون دوره\u200cی آزمایشی در دسترس است.', verbose_name='رایگان'), - ), - ] diff --git a/apps/billing/migrations/0003_seed_feature_plans.py b/apps/billing/migrations/0003_seed_feature_plans.py deleted file mode 100644 index e44e5e0..0000000 --- a/apps/billing/migrations/0003_seed_feature_plans.py +++ /dev/null @@ -1,71 +0,0 @@ -from django.db import migrations - -# داده‌ی پایه‌ی فروشگاه — ابزارها، پلن‌های قیمتی، و بسته‌های جم. -# -# این seed قبلا در یک مهاجرتِ جداگانه بود که همراهِ بقیه‌ی فایل‌های مهاجرت -# از دست رفت. نبودش دقیقا همان باگی است که کافه‌بازار گزارش کرد: وقتی هیچ -# FeaturePlan ای در دیتابیس نباشد، اندپوینت features/ لیستِ خالی می‌دهد، -# اپ برای هر ابزار isFeatureActive=false می‌گیرد و پیغام «این ابزار فعال -# نیست» را نشان می‌دهد — حتی در دوره‌ی آزمایشی و حتی بعد از خرید جم. -# -# دو ابزار عمدا رایگان‌اند (is_free=True): «مشتریان» و «دفتر درآمد و هزینه». -# طبق قوانین انتشار کافه‌بازار باید بخشی از امکانات پیش از هر خریدی -# در دسترس باشد؛ این دو، هسته‌ی دفترداریِ اپ‌اند و بدون آن‌ها بقیه‌ی -# ابزارها هم معنی نمی‌دهند، پس بهترین گزینه برای رایگان‌بودن‌اند. -# هر وقت خواستی می‌توانی از پنل ادمین (ریزاپ‌های قابل‌فعال‌سازی → ستون -# «رایگان») این ترکیب را عوض کنی، بدون نیاز به مهاجرت تازه. - -FEATURES = [ - # (key, name, is_free, [(months, gem_price), ...]) - ("customers", "مشتریان", True, []), - ("ledger", "دفتر درآمد و هزینه", True, []), - ("invoice", "فاکتور", False, [(1, 1000), (3, 2700), (6, 5000)]), - ("sms_bank", "پیام گروهی", False, [(1, 1200), (3, 3200), (6, 6000)]), - ("inventory", "انبارداری", False, [(1, 800), (3, 2200), (6, 4000)]), - ("orders", "پیگیری سفارش", False, [(1, 900), (3, 2400), (6, 4500)]), -] - -GEM_PACKS = [ - # (sku, name, gems, sort) - ("gem_pack_500", "۵۰۰ جم", 500, 1), - ("gem_pack_1200", "۱۲۰۰ جم", 1200, 2), - ("gem_pack_3000", "۳۰۰۰ جم", 3000, 3), -] - - -def seed(apps, schema_editor): - FeaturePlan = apps.get_model("billing", "FeaturePlan") - FeaturePlanTier = apps.get_model("billing", "FeaturePlanTier") - GemPack = apps.get_model("billing", "GemPack") - - for key, name, is_free, tiers in FEATURES: - feature, _ = FeaturePlan.objects.update_or_create( - key=key, - defaults={"name": name, "is_free": is_free, "is_purchasable": not is_free}, - ) - for months, price in tiers: - FeaturePlanTier.objects.update_or_create( - feature=feature, duration_months=months, defaults={"gem_price": price} - ) - - for sku, name, gems, sort_order in GEM_PACKS: - GemPack.objects.update_or_create( - bazaar_sku=sku, - defaults={"display_name": name, "gem_amount": gems, "sort_order": sort_order, "is_active": True}, - ) - - -def unseed(apps, schema_editor): - FeaturePlan = apps.get_model("billing", "FeaturePlan") - GemPack = apps.get_model("billing", "GemPack") - FeaturePlan.objects.filter(key__in=[f[0] for f in FEATURES]).delete() - GemPack.objects.filter(bazaar_sku__in=[g[0] for g in GEM_PACKS]).delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ("billing", "0002_featureplan_is_free"), - ] - - operations = [migrations.RunPython(seed, unseed)] diff --git a/apps/billing/migrations/0004_free_tier_split.py b/apps/billing/migrations/0004_free_tier_split.py deleted file mode 100644 index 31d07e1..0000000 --- a/apps/billing/migrations/0004_free_tier_split.py +++ /dev/null @@ -1,53 +0,0 @@ -from django.db import migrations - -# تغییرِ ترکیبِ رایگان/پولی طبق تصمیمِ محصول: -# رایگان (بدون اکانت): مشتریان، پیام گروهی، فاکتور -# پولی (فقط با جم): دفتر درآمد و هزینه، انبارداری، پیگیری سفارش -# -# سه ابزارِ رایگان عمدا همان‌هایی‌اند که بدونِ سرور هم معنی دارند (روی خودِ -# گوشی کار می‌کنند) تا واقعا بدونِ حساب کاربری قابل‌استفاده باشند. - -FREE_KEYS = ["customers", "sms_bank", "invoice"] -PAID_KEYS = ["ledger", "inventory", "orders"] - -# قیمتِ ابزارهایی که تازه پولی شده‌اند (اگر tier نداشته باشند). -PAID_TIERS = { - "ledger": [(1, 900), (3, 2400), (6, 4500)], - "inventory": [(1, 800), (3, 2200), (6, 4000)], - "orders": [(1, 900), (3, 2400), (6, 4500)], -} - - -def apply_split(apps, schema_editor): - FeaturePlan = apps.get_model("billing", "FeaturePlan") - FeaturePlanTier = apps.get_model("billing", "FeaturePlanTier") - - FeaturePlan.objects.filter(key__in=FREE_KEYS).update(is_free=True, is_purchasable=False) - # ابزارِ رایگان نباید قیمت داشته باشد. - FeaturePlanTier.objects.filter(feature__key__in=FREE_KEYS).delete() - - FeaturePlan.objects.filter(key__in=PAID_KEYS).update(is_free=False, is_purchasable=True) - for key, tiers in PAID_TIERS.items(): - feature = FeaturePlan.objects.filter(key=key).first() - if feature is None: - continue - for months, price in tiers: - FeaturePlanTier.objects.update_or_create( - feature=feature, duration_months=months, defaults={"gem_price": price} - ) - - -def revert_split(apps, schema_editor): - # برگشت به ترکیبِ قبلی (customers/ledger رایگان بودند). - FeaturePlan = apps.get_model("billing", "FeaturePlan") - FeaturePlan.objects.filter(key__in=["customers", "ledger"]).update(is_free=True, is_purchasable=False) - FeaturePlan.objects.filter(key__in=["invoice", "sms_bank", "inventory", "orders"]).update( - is_free=False, is_purchasable=True - ) - - -class Migration(migrations.Migration): - - dependencies = [("billing", "0003_seed_feature_plans")] - - operations = [migrations.RunPython(apply_split, revert_split)] diff --git a/apps/bizapps/migrations/0001_initial.py b/apps/bizapps/migrations/0001_initial.py deleted file mode 100644 index d37add2..0000000 --- a/apps/bizapps/migrations/0001_initial.py +++ /dev/null @@ -1,246 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Account', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('code', models.CharField(max_length=20, verbose_name='کد حساب')), - ('name', models.CharField(max_length=150, verbose_name='نام حساب')), - ('level', models.PositiveSmallIntegerField(choices=[(1, 'حساب کل'), (2, 'حساب معین'), (3, 'حساب تفصیلی')], verbose_name='سطح')), - ('account_type', models.CharField(choices=[('asset', 'دارایی'), ('liability', 'بدهی'), ('equity', 'سرمایه'), ('income', 'درآمد'), ('expense', 'هزینه')], max_length=15, verbose_name='نوع حساب')), - ('is_system', models.BooleanField(default=False, verbose_name='حساب سیستمی')), - ('is_active', models.BooleanField(default=True, verbose_name='فعال')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='children', to='bizapps.account', verbose_name='حساب والد')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='accounts', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'حساب', - 'verbose_name_plural': 'درخت حساب\u200cها', - 'ordering': ['code'], - }, - ), - migrations.CreateModel( - name='Customer', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=150, verbose_name='نام')), - ('phone', models.CharField(blank=True, max_length=20, verbose_name='شماره تماس')), - ('email', models.CharField(blank=True, max_length=150, verbose_name='ایمیل')), - ('address', models.CharField(blank=True, max_length=255, verbose_name='آدرس')), - ('notes', models.TextField(blank=True, verbose_name='یادداشت')), - ('total_purchases_count', models.PositiveIntegerField(default=0, verbose_name='تعداد خرید')), - ('total_purchase_amount', models.FloatField(default=0, verbose_name='مجموع خرید')), - ('last_purchase_date', models.DateTimeField(blank=True, null=True, verbose_name='آخرین خرید')), - ('outstanding_balance', models.FloatField(default=0, verbose_name='مانده بدهی')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('account', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='customer', to='bizapps.account', verbose_name='حساب تفصیلی')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='customers', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'مشتری', - 'verbose_name_plural': 'مشتریان', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='Invoice', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('invoice_number', models.CharField(max_length=30, verbose_name='شماره فاکتور')), - ('invoice_date', models.DateTimeField(verbose_name='تاریخ فاکتور')), - ('buyer_name', models.CharField(blank=True, max_length=150, verbose_name='نام خریدار')), - ('buyer_phone', models.CharField(blank=True, max_length=20, verbose_name='شماره خریدار')), - ('buyer_address', models.CharField(blank=True, max_length=255, verbose_name='آدرس خریدار')), - ('seller_name', models.CharField(blank=True, max_length=150, verbose_name='نام فروشنده')), - ('seller_phone', models.CharField(blank=True, max_length=20, verbose_name='شماره فروشنده')), - ('seller_address', models.CharField(blank=True, max_length=255, verbose_name='آدرس فروشنده')), - ('total_amount', models.FloatField(default=0, verbose_name='جمع کل')), - ('discount_amount', models.FloatField(default=0, verbose_name='تخفیف')), - ('tax_lines', models.JSONField(blank=True, default=list, verbose_name='ردیف\u200cهای مالیات')), - ('final_amount', models.FloatField(default=0, verbose_name='مبلغ نهایی')), - ('payment_type', models.CharField(choices=[('cash', 'نقدی'), ('credit', 'قسطی/نسیه')], default='cash', max_length=10)), - ('payment_status', models.CharField(choices=[('unpaid', 'پرداخت\u200cنشده'), ('paid', 'پرداخت\u200cشده')], default='unpaid', max_length=10)), - ('order_status', models.CharField(choices=[('pending_payment', 'در انتظار پرداخت'), ('ready_to_ship', 'آماده\u200cی ارسال'), ('shipped', 'ارسال\u200cشده'), ('delivered', 'تحویل\u200cشده'), ('cancelled', 'لغوشده')], default='ready_to_ship', max_length=20)), - ('shipping_method', models.CharField(choices=[('pickup', 'تحویل حضوری'), ('post', 'پست'), ('courier', 'پیک')], default='pickup', max_length=10)), - ('tracking_code', models.CharField(blank=True, max_length=100, verbose_name='کد رهگیری')), - ('notes', models.TextField(blank=True, verbose_name='یادداشت')), - ('paid_at', models.DateTimeField(blank=True, null=True, verbose_name='زمان پرداخت')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='invoices', to='bizapps.customer')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='invoices', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'فاکتور', - 'verbose_name_plural': 'فاکتورها', - 'ordering': ['-invoice_date'], - }, - ), - migrations.CreateModel( - name='LedgerEntry', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('entry_type', models.CharField(choices=[('income', 'درآمد'), ('expense', 'هزینه')], max_length=10, verbose_name='نوع')), - ('title', models.CharField(max_length=150, verbose_name='عنوان')), - ('amount', models.FloatField(verbose_name='مبلغ')), - ('category', models.CharField(blank=True, choices=[('sale_cash', 'فروش نقدی'), ('sale_credit', 'فروش قسطی/نسیه'), ('service_income', 'درآمد خدمات'), ('other_income', 'سایر درآمدها'), ('purchase_cash', 'خرید نقدی'), ('purchase_credit', 'خرید نسیه'), ('rent', 'اجاره'), ('salary', 'حقوق'), ('utility', 'قبض و آب\u200cوبرق'), ('transport', 'حمل\u200cونقل'), ('other_expense', 'سایر هزینه\u200cها')], max_length=100, verbose_name='دسته')), - ('entry_date', models.DateField(verbose_name='تاریخ')), - ('note', models.TextField(blank=True, verbose_name='یادداشت')), - ('total_amount', models.FloatField(default=0, verbose_name='مبلغ کل تعهد')), - ('counterparty_name', models.CharField(blank=True, max_length=150, verbose_name='طرف حساب')), - ('counterparty_phone', models.CharField(blank=True, max_length=20, verbose_name='شماره طرف حساب')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('account', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ledger_entries', to='bizapps.account', verbose_name='حساب معین')), - ('linked_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='ledger_entries', to='bizapps.invoice')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ledger_entries', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'ردیف دفتر درآمد/هزینه', - 'verbose_name_plural': 'دفتر درآمد و هزینه', - 'ordering': ['-entry_date'], - }, - ), - migrations.CreateModel( - name='LedgerInstallment', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('amount', models.FloatField(verbose_name='مبلغ قسط')), - ('paid_date', models.DateField(verbose_name='تاریخ پرداخت')), - ('reference', models.CharField(blank=True, max_length=150, verbose_name='شماره مرجع')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('ledger_entry', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='installments', to='bizapps.ledgerentry')), - ], - options={ - 'verbose_name': 'قسط', - 'verbose_name_plural': 'اقساط', - 'ordering': ['paid_date'], - }, - ), - migrations.CreateModel( - name='Product', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=150, verbose_name='نام کالا')), - ('sku', models.CharField(blank=True, max_length=100, verbose_name='کد کالا')), - ('category', models.CharField(blank=True, max_length=100, verbose_name='دسته')), - ('unit', models.CharField(blank=True, default='عدد', max_length=30, verbose_name='واحد شمارش')), - ('quantity', models.FloatField(default=0, verbose_name='موجودی')), - ('unit_price', models.FloatField(default=0, verbose_name='قیمت فروش')), - ('purchase_price', models.FloatField(default=0, verbose_name='قیمت خرید')), - ('total_purchase_value', models.FloatField(default=0, verbose_name='ارزش موجودی')), - ('low_stock_threshold', models.FloatField(default=0, verbose_name='آستانه\u200cی هشدار موجودی کم')), - ('notes', models.TextField(blank=True, verbose_name='یادداشت')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'کالا', - 'verbose_name_plural': 'انبار', - 'ordering': ['name'], - }, - ), - migrations.CreateModel( - name='InvoiceItem', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('description', models.CharField(max_length=255, verbose_name='شرح')), - ('quantity', models.FloatField(verbose_name='تعداد')), - ('unit_price', models.FloatField(verbose_name='قیمت واحد')), - ('discount_percent', models.FloatField(default=0, verbose_name='درصد تخفیف')), - ('total_price', models.FloatField(default=0, verbose_name='جمع')), - ('invoice', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='bizapps.invoice')), - ('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='bizapps.product')), - ], - options={ - 'verbose_name': 'قلم فاکتور', - 'verbose_name_plural': 'اقلام فاکتور', - }, - ), - migrations.CreateModel( - name='StockMovement', - fields=[ - ('id', models.AutoField(primary_key=True, serialize=False)), - ('delta', models.FloatField(verbose_name='تغییر')), - ('quantity_after', models.FloatField(verbose_name='موجودی بعد از تغییر')), - ('note', models.CharField(blank=True, max_length=255, verbose_name='یادداشت')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='movements', to='bizapps.product')), - ], - options={ - 'verbose_name': 'تغییر موجودی', - 'verbose_name_plural': 'تاریخچه\u200cی موجودی', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='Transaction', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('transaction_number', models.CharField(max_length=30, verbose_name='شماره سند')), - ('transaction_date', models.DateTimeField(verbose_name='تاریخ سند')), - ('transaction_type', models.CharField(choices=[('cash_sale', 'فروش نقدی'), ('credit_sale', 'فروش نسیه'), ('installment', 'دریافت قسط'), ('purchase', 'خرید'), ('expense', 'هزینه'), ('adjustment', 'تعدیل')], max_length=20)), - ('debit_account', models.CharField(max_length=20, verbose_name='کد حساب بدهکار')), - ('credit_account', models.CharField(max_length=20, verbose_name='کد حساب بستانکار')), - ('amount', models.FloatField(verbose_name='مبلغ')), - ('description', models.CharField(blank=True, max_length=255, verbose_name='شرح')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('credit_account_ref', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='credit_transactions', to='bizapps.account', verbose_name='حساب بستانکار')), - ('debit_account_ref', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='debit_transactions', to='bizapps.account', verbose_name='حساب بدهکار')), - ('reference_invoice', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='transactions', to='bizapps.invoice')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'سند حسابداری', - 'verbose_name_plural': 'اسناد حسابداری', - 'ordering': ['-transaction_date'], - }, - ), - migrations.AddIndex( - model_name='account', - index=models.Index(fields=['user', 'level'], name='bizapps_acc_user_id_e1d510_idx'), - ), - migrations.AddConstraint( - model_name='account', - constraint=models.UniqueConstraint(fields=('user', 'code'), name='unique_user_account_code'), - ), - migrations.AddIndex( - model_name='customer', - index=models.Index(fields=['user', 'phone'], name='bizapps_cus_user_id_886a45_idx'), - ), - migrations.AddIndex( - model_name='invoice', - index=models.Index(fields=['user', 'invoice_date'], name='bizapps_inv_user_id_89a273_idx'), - ), - migrations.AddConstraint( - model_name='invoice', - constraint=models.UniqueConstraint(fields=('user', 'invoice_number'), name='unique_user_invoice_number'), - ), - migrations.AddIndex( - model_name='ledgerentry', - index=models.Index(fields=['user', 'entry_date'], name='bizapps_led_user_id_795e45_idx'), - ), - migrations.AddIndex( - model_name='product', - index=models.Index(fields=['user', 'category'], name='bizapps_pro_user_id_adf41e_idx'), - ), - migrations.AddIndex( - model_name='transaction', - index=models.Index(fields=['user', 'transaction_date'], name='bizapps_tra_user_id_7cbd25_idx'), - ), - ] diff --git a/apps/bizapps/migrations/0002_customer_social_handle_customer_social_platform.py b/apps/bizapps/migrations/0002_customer_social_handle_customer_social_platform.py deleted file mode 100644 index 3881e84..0000000 --- a/apps/bizapps/migrations/0002_customer_social_handle_customer_social_platform.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:38 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bizapps', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='customer', - name='social_handle', - field=models.CharField(blank=True, max_length=150, verbose_name='آیدی/نام کاربری'), - ), - migrations.AddField( - model_name='customer', - name='social_platform', - field=models.CharField(blank=True, choices=[('instagram', 'اینستاگرام'), ('telegram', 'تلگرام'), ('whatsapp', 'واتساپ'), ('bale', 'بله'), ('eitaa', 'ایتا'), ('other', 'سایر')], max_length=15, verbose_name='پلتفرم ارتباطی'), - ), - ] diff --git a/apps/bizapps/migrations/0003_ledgerbook_ledgerentry_book_and_more.py b/apps/bizapps/migrations/0003_ledgerbook_ledgerentry_book_and_more.py deleted file mode 100644 index 84620a4..0000000 --- a/apps/bizapps/migrations/0003_ledgerbook_ledgerentry_book_and_more.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 06:54 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('bizapps', '0002_customer_social_handle_customer_social_platform'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='LedgerBook', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('name', models.CharField(max_length=120, verbose_name='نام فایل')), - ('start_date', models.DateField(verbose_name='از تاریخ')), - ('end_date', models.DateField(blank=True, null=True, verbose_name='تا تاریخ')), - ('opening_balance', models.FloatField(default=0, verbose_name='مانده ابتدای دوره')), - ('is_active', models.BooleanField(default=True, verbose_name='فایل جاری')), - ('is_closed', models.BooleanField(default=False, verbose_name='بسته\u200cشده')), - ('closed_at', models.DateTimeField(blank=True, null=True)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ledger_books', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'فایل دفتر', - 'verbose_name_plural': 'فایل\u200cهای دفتر', - 'ordering': ['-start_date', '-created_at'], - }, - ), - migrations.AddField( - model_name='ledgerentry', - name='book', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='entries', to='bizapps.ledgerbook', verbose_name='فایل دفتر'), - ), - migrations.AddIndex( - model_name='ledgerbook', - index=models.Index(fields=['user', 'is_active'], name='bizapps_led_user_id_0ad2b0_idx'), - ), - ] diff --git a/apps/landing/migrations/0001_initial.py b/apps/landing/migrations/0001_initial.py deleted file mode 100644 index b5fb15b..0000000 --- a/apps/landing/migrations/0001_initial.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Article', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('slug', models.SlugField(max_length=120, unique=True, verbose_name='نامک (slug)')), - ('title', models.CharField(max_length=200, verbose_name='عنوان')), - ('meta_description', models.CharField(blank=True, help_text='در تگ\u200cهای meta description و og:description استفاده می\u200cشود.', max_length=300, verbose_name='توضیح متا (SEO)')), - ('excerpt', models.CharField(help_text='همان متن کوتاهی که زیر عنوان در لیست مقالات نشان داده می\u200cشود.', max_length=300, verbose_name='خلاصه')), - ('content_html', models.TextField(help_text='محتوای مقاله با تگ\u200cهای HTML ساده مثل

،

، .', verbose_name='محتوا (HTML)')), - ('read_minutes', models.PositiveSmallIntegerField(default=3, verbose_name='زمان مطالعه (دقیقه)')), - ('is_published', models.BooleanField(default=True, verbose_name='منتشرشده')), - ('published_at', models.DateField(verbose_name='تاریخ انتشار')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ], - options={ - 'verbose_name': 'مقاله', - 'verbose_name_plural': 'مقالات', - 'ordering': ('-published_at', '-created_at'), - }, - ), - ] diff --git a/apps/landing/migrations/0002_articles_batch.py b/apps/landing/migrations/0002_articles_batch.py deleted file mode 100644 index cba7561..0000000 --- a/apps/landing/migrations/0002_articles_batch.py +++ /dev/null @@ -1,898 +0,0 @@ -import datetime - -from django.db import migrations - -# سی مقاله‌ی تازه — جدا از هشت‌تای قبلی (چهارتای بازنویسی‌شده + چهارتای -# جدیدِ فصل قبل). هرکدام حداقل یکی‌دو لینکِ داخلیِ واقعی و مرتبط، توی خودِ -# متن (نه فقط بخشِ «مقالات مرتبط»ِ خودکارِ پایین صفحه) به مقاله‌های دیگر -# دارد — هم قدیمی‌ها هم بین خودشان — تا یک وبِ لینکِ داخلیِ واقعی شکل بگیرد -# (دقیقا همان چیزی که خواسته شده بود: «مقالات توش لینک داشته باشه و به هم -# لینک بشن»). انتشار هفتگی: یکی هر هفته، شروع از یک هفته بعد از آخرین -# مقاله‌ی فصل قبل — با همان مکانیزمِ published_at-به‌عنوان-gate که در -# apps.landing.views._visible_articles پیاده شده، نیازی به کرون نیست. - -LAST_PREVIOUS_DATE = datetime.date(2026, 9, 8) - -ARTICLES = [ - # --- خوشه‌ی دفتر درآمد و هزینه / حسابداری --- - { - "slug": "tafavot-daramad-naghdi-talab", - "title": "تفاوت درآمد نقدی و طلب چیست؟", - "meta_description": "چرا هر پولی که «قرار است» به تو برسد را نباید همان لحظه درآمد حساب کرد — فرق درآمد نقدی و طلب در دفترداریِ کسب‌وکار کوچک.", - "excerpt": "هر پولی که «قرار است» برسد، هنوز پول نیست — فرق درآمد و طلب همین‌جاست.", - "read_minutes": 3, - "content_html": """ -

- یک سوال ساده که جواب دادنش سخت‌تر از چیزی است که فکر می‌کنی: «همین - الان چقدر پول نقد در دسترس داری؟» خیلی از کسب‌وکارهای کوچک این عدد را - با «جمعِ کل فروش» اشتباه می‌گیرند — در حالی که بخشی از آن فروش هنوز - طلب است، نه پول در جیب. -

-

درآمد یعنی چه، طلب یعنی چه

-

- درآمدِ نقدی یعنی پولی که همین الان دستت رسیده. طلب (receivable) یعنی - حقی که داری تا در آینده پول را بگیری — مثل وقتی جنسی را - قسطی می‌فروشی. هر دو - «فروش» حساب می‌شوند، اما فقط یکی‌شان همین الان قابل‌خرج‌کردن است. -

-

چرا این فرق مهم است

-

- اگر طلب را با درآمدِ نقدی یکی حساب کنی، فکر می‌کنی پول بیشتری داری - که واقعا نداری — و همین باعث می‌شود در پرداختِ هزینه‌های خودت (اجاره، - خرید جنسِ تازه) کم بیاوری. دفتر درآمد و هزینه‌ی شاپ شیپ این دو را از - هم جدا نگه می‌دارد تا همیشه بدانی چقدر واقعا در دسترس داری. -

-""", - }, - { - "slug": "csv-daftar-baraye-hesabdar", - "title": "چطور خروجی CSV دفتر حساب را برای حسابدار بفرستم؟", - "meta_description": "دفتر درآمد و هزینه‌ی شاپ شیپ را برای یک بازه‌ی دلخواه به فایل CSV تبدیل و برای حسابدارت ارسال کن — سازگار با اکسل فارسی.", - "excerpt": "یک بازه‌ی تاریخ انتخاب کن، خروجی CSV بگیر، برای حسابدارت بفرست.", - "read_minutes": 2, - "content_html": """ -

- خیلی از کسب‌وکارهای کوچک هر چند ماه یک‌بار باید دفترشان را برای یک - حسابدار بفرستند — برای اظهارنامه‌ی مالیاتی یا فقط یک مرور کلی. شاپ - شیپ این کار را به چند ثانیه رسانده. -

-

خروجی گرفتن

-

- از دفتر درآمد و هزینه، یک بازه‌ی تاریخِ دلخواه (مثلا یک ماه یا یک - فصل) انتخاب کن و «خروجی CSV» را بزن. فایلی می‌سازد که مستقیم در اکسل - یا گوگل‌شیت باز می‌شود — با حروف فارسیِ درست، بدون به‌هم‌ریختگی. -

-

چه چیزی در فایل هست

-

- هر ردیف یک ثبت است: تاریخ، دسته‌بندی (مثلا فروش نقدی یا - خرید نسیه)، مبلغ، - و یادداشتی که خودت نوشته بودی. برای ثبت‌های بدهی/طلب، مانده‌حساب هم - در فایل می‌آید — حسابدارت همان چیزی را می‌بیند که تو در اپ می‌بینی. -

-""", - }, - { - "slug": "modiriyat-bedehi-moshtarian", - "title": "مدیریت بدهی مشتریان با موبایل", - "meta_description": "همه‌ی مشتریانی که به تو بدهکارند، مانده‌حساب و تاریخچه‌ی اقساطشان را در یک صفحه ببین — بدون دفترچه‌ی کاغذی.", - "excerpt": "مانده‌حساب هر مشتری، خودکار از فاکتورها و دفترت محاسبه می‌شود — نه با دفترچه‌ی کاغذی.", - "read_minutes": 3, - "content_html": """ -

- خیلی از مغازه‌دارها یک دفترچه‌ی کاغذیِ جدا برای «کیا بهم بدهکارن» - دارند — که هم گم می‌شود، هم به‌روزرسانیش یادشان می‌رود. در شاپ شیپ، - این دفترچه دیگر لازم نیست. -

-

مانده‌حساب خودکار

-

- هر بار که یک فروش - قسطی برای یک مشتری ثبت می‌کنی یا یک قسط از او می‌گیری، مانده‌حساب - همان مشتری در پروفایلش خودکار به‌روز می‌شود — جمع تمام بدهی‌های بازش، - نه فقط آخرین ثبت. -

-

یک نگاه، همه‌ی بدهکارها

-

- از پرونده‌ی مشتری - می‌توانی ببینی چه کسی چقدر بدهکار است و آخرین قسطش کِی بوده — و اگر - لازم شد، از همان‌جا یک پیامکِ یادآوری برایش بفرستی. -

-""", - }, - { - "slug": "sabt-hazine-rozane-maghaze", - "title": "ثبت هزینه‌های روزمره‌ی مغازه: از کجا شروع کنم؟", - "meta_description": "اجاره، قبض، حقوق، خرید جنس — راهنمای شروع ثبت هزینه‌های روزمره‌ی یک کسب‌وکار کوچک در دفتر درآمد و هزینه‌ی شاپ شیپ.", - "excerpt": "لازم نیست حسابدار باشی — فقط دسته‌ی درستِ نزدیک به هزینه‌ات را انتخاب کن.", - "read_minutes": 3, - "content_html": """ -

- خیلی‌ها ثبتِ هزینه را به این دلیل کنار می‌گذارند که فکر می‌کنند باید - اصطلاحاتِ حسابداری بلد باشند. خبر خوب: نیستی، فقط چند دسته‌ی ساده - کافی است. -

-

دسته‌های آماده برای شروع

-

- اجاره، حقوق، قبض/آب‌وبرق، خرید نقدی جنس، و «سایر هزینه‌ها» — این پنج - دسته اکثرِ هزینه‌های روزمره‌ی یک مغازه‌ی کوچک را پوشش می‌دهند. برای - خرید جنسی که نقد نپرداختی، دسته‌ی - خرید نسیه جداست. -

-

عادتِ هر روز، بهتر از هر هفته

-

- ثبتِ هر هزینه همان لحظه که اتفاق می‌افتد، چند ثانیه بیشتر طول - نمی‌کشد؛ ولی اگر یک هفته عقب بیفتی، خیلی از ریزه‌کاری‌ها یادت - می‌رود. بعد از چند هفته می‌توانی از - نمودارِ درآمد و - هزینه ببینی بیشترین هزینه‌ات کجا می‌رود. -

-""", - }, - { - "slug": "nemodar-daramad-hazine-tasmim", - "title": "نمودار درآمد و هزینه چطور به تصمیم‌گیری کمک می‌کند؟", - "meta_description": "نمودار دفتر درآمد و هزینه‌ی شاپ شیپ را برای یک بازه‌ی زمانی دلخواه ببین و بفهم درآمدت روند صعودی دارد یا نه.", - "excerpt": "یک نگاهِ بصری به روندِ درآمد و هزینه‌ات — نه فقط یک عدد خام.", - "read_minutes": 3, - "content_html": """ -

- یک لیستِ بلند از اعداد، فهمیدنِ «آیا وضعیتم بهتر شده یا بدتر» را سخت - می‌کند. نمودار به همین درد می‌خورد — یک نگاه، و روند مشخص است. -

-

بازه‌ی زمانیِ دلخواه

-

- از صفحه‌ی دفترِ درآمد و هزینه، وارد نمودار شو و بازه‌ای که می‌خواهی - (یک ماه، یک فصل، یک سال) را انتخاب کن. درآمد و هزینه‌ی هر بازه کنار - هم رسم می‌شود — همان چیزی که بعدا می‌توانی به‌عنوان - خروجی CSV هم - داشته باشی. -

-

سوالی که نمودار جواب می‌دهد

-

- «این ماه بهتر از ماه پیش بودم؟» یا «بعد از فعال‌کردنِ پیام گروهی، - فروشم بالا رفت؟» — این‌ها سوالاتی هستند که یک نمودارِ ساده بهتر از - یک لیستِ عددی جوابشان را می‌دهد. -

-""", - }, - { - "slug": "kharid-nasiye-dar-daftardari", - "title": "فرق خرید نسیه و خرید نقدی در دفترداری", - "meta_description": "خرید نسیه یک نوع بدهکاریِ تو به فروشنده است، نه یک هزینه‌ی ساده — چطور شاپ شیپ این تفاوت را در دفتر نشان می‌دهد.", - "excerpt": "خرید نسیه یعنی تو بدهکارِ فروشنده‌ای — دقیقا برعکسِ فروش قسطی.", - "read_minutes": 3, - "content_html": """ -

- مقاله‌ی فروش قسطی چطور - باید ثبت شود؟ نشان داد وقتی تو جنسی را قسطی می‌فروشی، مشتری به تو - بدهکار می‌شود. حالا برعکسش: وقتی تو از یک تامین‌کننده جنس نسیه - می‌خری، این‌بار خودت بدهکاری. -

-

چرا این را جدا از هزینه‌ی نقدی ثبت کنیم

-

- اگر خرید نسیه را همان لحظه «هزینه» حساب کنی، انگار همان لحظه پول - دادی — در حالی که ندادی. این یعنی موجودیِ نقدیِ حساب‌شده‌ات از - واقعیت بیشتر نشان داده می‌شود، دقیقا برعکسِ اشتباهی که در مقاله‌ی - تفاوت درآمد نقدی و - طلب گفتیم. -

-

در شاپ شیپ

-

- دسته‌ی «خرید نسیه» را که انتخاب کنی، اپ مبلغِ کل و نام طرف‌حساب را - می‌پرسد و یک بدهیِ باز برایت می‌سازد — دقیقا هم‌خانواده‌ی همان مکانیزمِ - طلب، فقط جهتش برعکس است. -

-""", - }, - { - "slug": "sabt-aghsat-bedehi", - "title": "چطور اقساط یک بدهی را ثبت و پیگیری کنم؟", - "meta_description": "برای هر بدهی/طلبِ باز در دفتر شاپ شیپ، هر قسطی که می‌گیری یا می‌دهی را جدا ثبت کن و مانده‌حساب خودکار کم شود.", - "excerpt": "هر قسط را جدا ثبت کن — مانده‌حساب خودش کم می‌شود، حساب‌وکتاب دستی لازم نیست.", - "read_minutes": 2, - "content_html": """ -

- وقتی یک فروش قسطی - یا خرید نسیه در - دفترت ثبت می‌کنی، یک «مانده‌حساب» برایش ساخته می‌شود. حالا سوال این - است: وقتی هر قسط می‌رسد، چطور ثبتش کنم؟ -

-

ثبت هر قسط، جدا

-

- روی همان ثبتِ بدهی/طلب باز کن و یک قسطِ جدید اضافه کن — مبلغ، تاریخ، - و اگر خواستی یک شماره‌ی مرجع (مثلا شماره‌ی رسید). همین که ثبت شد، - مانده‌حساب خودکار همان مقدار کم می‌شود. -

-

وقتی مانده صفر شود

-

- وقتی مجموعِ اقساط به کل بدهی برسد، آن ثبت دیگر در لیستِ «بدهی‌های - باز» نشان داده نمی‌شود — اما تاریخچه‌ی کاملِ اقساط همیشه قابل‌مرور - می‌ماند، برای وقتی که لازم شد به آن رجوع کنی. -

-""", - }, - # --- خوشه‌ی فاکتور --- - { - "slug": "fatoore-chand-ghalam-kala", - "title": "فاکتور با تخفیف و مالیات چند نوع کالا چطور صادر می‌شود؟", - "meta_description": "هر قلمِ کالا در فاکتورِ شاپ شیپ می‌تواند نرخ تخفیف و مالیاتِ جدا از بقیه داشته باشد — جمعِ نهایی خودکار محاسبه می‌شود.", - "excerpt": "هر قلم کالا، تخفیف و مالیاتِ خودش را دارد — جمعِ نهایی را اپ حساب می‌کند.", - "read_minutes": 3, - "content_html": """ -

- فرض کن یک فاکتور داری با سه قلم کالا: یکی تخفیف دارد، یکی مالیاتِ - متفاوت، و سومی هیچ‌کدام. حساب‌کردنِ دستیِ این‌ها با ماشین‌حساب، هم - وقت‌گیر است هم پرخطا. -

-

هر قلم، تنظیماتِ خودش

-

- وقتی کالایی را (از همان لیستِ - انبارت) به فاکتور - اضافه می‌کنی، می‌توانی برایش یک درصد تخفیف و یک نرخ مالیات جدا از - بقیه‌ی قلم‌ها بگذاری. -

-

جمعِ نهایی، خودکار

-

- جمعِ کل، جمعِ مالیات، و مبلغِ تخفیف‌خورده‌ی نهایی خودش محاسبه - می‌شود — همان چیزی که در مقاله‌ی - فاکتور رسمی برای - مغازه هم به اتصالش به انبار اشاره شده بود. -

-""", - }, - { - "slug": "vaziat-fatoore-pardakht", - "title": "فرق فاکتور پرداخت‌شده و در‌انتظار پرداخت", - "meta_description": "هر فاکتور در شاپ شیپ یک وضعیت روشن دارد — پرداخت‌شده یا در‌انتظار — و همین وضعیت تعیین می‌کند دفترت و انبارت چه تغییری می‌کنند.", - "excerpt": "وضعیتِ فاکتور فقط یک برچسب نیست — تعیین می‌کند انبار و دفترت چه اتفاقی برایشان می‌افتد.", - "read_minutes": 2, - "content_html": """ -

- یک فاکتور صرفا «صادر شدن» کافی نیست — باید بدانی پولش رسیده یا نه. - شاپ شیپ برای همین یک وضعیتِ روشن به هر فاکتور می‌دهد. -

-

وقتی پرداخت‌شده می‌شود

-

- همین که فاکتور را «پرداخت‌شده» علامت بزنی، دو اتفاق خودکار می‌افتد: - موجودیِ کالاهای همان فاکتور از - انبار کم می‌شود، و اگر - نوعِ پرداخت «قسطی/نسیه» بود، یک ثبتِ طلب هم در - دفترت ساخته - می‌شود. -

-

وقتی هنوز در‌انتظار است

-

- تا وضعیت پرداخت‌شده نشود، هیچ‌کدام از این دو اتفاق نمی‌افتد — یعنی - می‌توانی فاکتور را از قبل آماده کنی، بدهی به مشتری نشان دهی، و بعدا - وقتی واقعا پول رسید، وضعیتش را عوض کنی. -

-""", - }, - { - "slug": "chera-fatoore-rasmi-mohem-ast", - "title": "چرا باید فاکتور رسمی به مشتری بدهم حتی برای فروش کوچک؟", - "meta_description": "فاکتور رسمی فقط برای فروش‌های بزرگ نیست — اعتماد، پیگیریِ اختلاف، و تاریخچه‌ی خرید مشتری، سه دلیل برای صدورِ فاکتورِ هر فروش.", - "excerpt": "فاکتور فقط کاغذ نیست — سندِ اعتماد بین تو و مشتری است.", - "read_minutes": 3, - "content_html": """ -

- خیلی از مغازه‌دارها فقط برای فروش‌های «بزرگ» فاکتور صادر می‌کنند — - برای بقیه، یک رسیدِ دست‌نویس یا هیچی. اما فاکتورِ رسمی برای هر فروش، - چند فایده دارد که با رسیدِ دست‌نویس به‌دست نمی‌آید. -

-

اعتماد و حرفه‌ای‌بودن

-

- یک فاکتورِ با لوگو، حس حرفه‌ای‌تری به مشتری می‌دهد — دقیقا همان چیزی - که در فاکتور رسمی - برای مغازه توضیح داده شد. -

-

پیگیریِ اختلاف

-

- اگر روزی مشتری بگوید «من این قیمت را ندیدم» یا «این کالا را نخریدم»، - یک فاکتورِ ثبت‌شده در اپ، فورا قابل‌نشان‌دادن است — بدون گشتن در - دفترچه‌ای که شاید گم شده باشد. -

-

تاریخچه‌ی خرید مشتری

-

- هر فاکتور خودکار به پرونده‌ی - همان مشتری وصل می‌شود — یعنی بعد از چند ماه می‌توانی ببینی هر - مشتری چقدر و چه چیزی خریده، بدون این‌که چیزی را دوباره تایپ کنی. -

-""", - }, - # --- خوشه‌ی مشتریان --- - { - "slug": "parvande-moshtari-chist", - "title": "پرونده‌ی مشتری چیست و چه چیزهایی در آن ثبت می‌شود؟", - "meta_description": "پرونده‌ی هر مشتری در شاپ شیپ، تاریخچه‌ی فاکتورها و مانده‌حسابش را خودکار جمع می‌کند — بدون هیچ ثبتِ دستیِ اضافه.", - "excerpt": "تاریخچه‌ی خرید و مانده‌حساب هر مشتری، خودکار و در یک صفحه.", - "read_minutes": 2, - "content_html": """ -

- وقتی چند ده یا چند صد مشتری داری، به‌خاطر سپردنِ این‌که هرکدام چه - خریده و چقدر بدهکار است، عملا غیرممکن می‌شود. پرونده‌ی مشتری همین - کار را برایت انجام می‌دهد. -

-

چیزی که خودکار ساخته می‌شود

-

- هر بار که برای یک مشتری فاکتور - صادر می‌کنی یا در دفترت یک بدهی/طلب برایش ثبت می‌کنی، همه‌ی این‌ها - خودکار در پرونده‌ی همان مشتری جمع می‌شود — تو چیزی جدا وارد نمی‌کنی. -

-

مانده‌حساب، همان‌جا

-

- اگر مشتری‌ای بدهیِ - باز داشته باشد، همان‌جا در پرونده‌اش می‌بینی — دیگر نیازی به گشتن - در دفتر یا فاکتورهای قدیمی نیست. -

-""", - }, - { - "slug": "faal-sazi-mojadad-moshtarian", - "title": "چطور مشتریان قدیمی را دوباره فعال کنم؟", - "meta_description": "با پیام گروهی و لیستِ مشتریانی که مدتی خرید نکرده‌اند، یک یادآوریِ ساده بفرست و مشتریِ فراموش‌شده را برگردان.", - "excerpt": "یک پیامِ یادآوریِ ساده، بیشترِ وقت‌ها کافی است — نه تخفیفِ بزرگ.", - "read_minutes": 3, - "content_html": """ -

- خیلی از کسب‌وکارهای کوچک انرژی‌شان را فقط صرفِ جذبِ مشتریِ تازه - می‌کنند و مشتریِ قدیمی که مدتی سراغش نرفته‌اند را فراموش می‌کنند — - در حالی که برگرداندنِ یک مشتریِ قدیمی معمولا ارزان‌تر از جذبِ تازه است. -

-

پیدا کردنِ مشتریانِ ساکت

-

- از پرونده‌های مشتری، - کسانی که مدتی فاکتور یا خریدی نداشته‌اند مشخص‌اند — همین لیست، شروعِ - خوبی برای یک یادآوری است. -

-

یک قالبِ ساده کافی است

-

- لازم نیست تخفیفِ بزرگ بدهی؛ گاهی یک - قالبِ پیام ساده مثل «دلمون - برات تنگ شده، سر بزن» با نامِ خودِ مشتری، از طریقِ - ارسالِ گروهی برایِ - همان لیست، کافی است. -

-""", - }, - # --- خوشه‌ی انبار --- - { - "slug": "dastebandi-kala-anbar", - "title": "دسته‌بندی کالا در انبار: از کجا شروع کنم؟", - "meta_description": "برای انبارِ چند ده یا چند صد قلمی، دسته‌بندی کالا در شاپ شیپ پیدا کردن و مدیریت را چند برابر سریع‌تر می‌کند.", - "excerpt": "دسته‌های ساده مثل «نوشیدنی» یا «لبنیات» — پیدا کردن یک کالا چند برابر سریع‌تر می‌شود.", - "read_minutes": 2, - "content_html": """ -

- اگر انبارت فقط چند قلم کالا دارد، دسته‌بندی شاید لازم نباشد. اما - همین که به چند ده قلم برسی، یک لیستِ بلندِ بی‌ترتیب پیدا کردنِ یک - کالا را کند می‌کند. -

-

دسته‌های دلخواهت

-

- دسته‌ها را خودت تعریف می‌کنی — چیزی که برای کسب‌وکارت معنی‌دار باشد، - نه یک قالبِ از‌قبل‌تعیین‌شده. یک سوپرمارکت شاید «نوشیدنی/لبنیات/خشکبار» - داشته باشد، یک لوازم‌یدکی‌فروشی چیزِ کاملا متفاوتی. -

-

اثرش روی فاکتور هم هست

-

- وقتی برای فاکتور - کالا اضافه می‌کنی، همین دسته‌بندی کمک می‌کند سریع‌تر کالای موردنظر را - پیدا کنی — به‌جای اسکرول‌کردنِ یک لیستِ طولانی. -

-""", - }, - { - "slug": "gheymat-kharid-vs-arzesh-anbar", - "title": "فرق قیمت خرید و ارزش کل انبار", - "meta_description": "قیمت خرید هر کالا را ثبت کن تا شاپ شیپ خودش ارزشِ کلِ سرمایه‌ی خوابیده در انبارت را برایت حساب کند.", - "excerpt": "دو عدد ساده (قیمت خرید و موجودی)، یک جوابِ مهم: چقدر سرمایه در انبارت خوابیده؟", - "read_minutes": 2, - "content_html": """ -

- «الان چقدر پول در قالبِ جنس، روی طاقچه‌های مغازه‌ام خوابیده؟» سوالی - است که خیلی از صاحبانِ مغازه دقیق جوابش را نمی‌دانند. -

-

یک ضرب ساده، محاسبه‌شده برایت

-

- برای هر کالا در انبار، - قیمتِ خریدش را ثبت می‌کنی. اپ همین عدد را در موجودیِ فعلی ضرب می‌کند و - «ارزشِ کلِ انبار» را نشان می‌دهد — بدون این‌که خودت دستی جمع بزنی. -

-

چرا این عدد مهم است

-

- این عدد به تو می‌گوید چقدر سرمایه در جنسِ فروخته‌نشده گیر افتاده — - اطلاعاتی که برای تصمیمِ «الان جنسِ تازه بخرم یا نه» مستقیم به کار - می‌آید. -

-""", - }, - { - "slug": "hoshdar-kambood-mojoodi", - "title": "هشدار کمبود موجودی چطور کار می‌کند؟", - "meta_description": "برای هر کالا یک آستانه‌ی «کم شد» تعریف کن — قبل از این‌که مشتری بیاید و نداشته باشی، خودت متوجه می‌شوی.", - "excerpt": "یک آستانه تعریف کن — قبل از تمام‌شدنِ واقعیِ جنس، خودت باخبر می‌شوی.", - "read_minutes": 2, - "content_html": """ -

- یکی از بدترین حس‌ها برای یک مغازه‌دار، این است که مشتری بیاید و جنسی - که فکر می‌کردی داری، تمام شده باشد. هشدارِ کمبود دقیقا برای جلوگیری - از همین لحظه است. -

-

آستانه‌ی دلخواهِ هر کالا

-

- برای هر کالا در انبار، - یک عدد به‌عنوان «کم شد» تعریف می‌کنی — مثلا وقتی موجودی به ۵ رسید. - این عدد برای هر کالا می‌تواند فرق کند. -

-

باخبر شدن زودتر

-

- وقتی موجودی به آن آستانه برسد، اپ نشانش می‌دهد — قبل از این‌که واقعا - صفر شود. یعنی وقت کافی برای سفارشِ جنسِ تازه داری، بدون فشارِ - لحظه‌ی‌آخری. -

-""", - }, - # --- خوشه‌ی پیام گروهی --- - { - "slug": "ghaleb-payam-chist", - "title": "قالب پیام چیست و چطور بسازمش؟", - "meta_description": "قالب پیام یک متنِ آماده با جای‌خالیِ نام/شماره است که یک‌بار می‌سازی و بارها برای مخاطبین مختلف استفاده می‌کنی.", - "excerpt": "یک متن، با جای‌خالی برای نام — یک‌بار بساز، بارها استفاده کن.", - "read_minutes": 2, - "content_html": """ -

- اگر هر بار که می‌خواهی پیام گروهی بفرستی، از صفر متن تایپ کنی، هم - وقت تلف می‌شود هم احتمالِ فراموش‌کردنِ چیزی بالا می‌رود. قالبِ پیام - همین مشکل را حل می‌کند. -

-

جای‌خالیِ نام و شماره

-

- در متنِ قالب، می‌توانی از جای‌خالی‌هایی مثل «نام» استفاده کنی — وقتی - برای هر مخاطب ارسال می‌شود، خودکار با اطلاعاتِ همان مخاطب پر می‌شود. -

-

فیلدِ اجرا، برای مقادیرِ متغیر

-

- اگر یک مقدار داری که از قبل معلوم نیست (مثل کد رهگیری)، قالب می‌تواند - یک فیلدِ اجرا - هم داشته باشد — همان لحظه‌ی ارسال از تو پرسیده می‌شود. -

-""", - }, - { - "slug": "ersal-payam-be-goruh-khas", - "title": "ارسال پیام به گروه خاصی از مشتریان", - "meta_description": "به‌جای فرستادن یک پیام به همه، فقط برای بدهکارها، مشتریانِ ساکت، یا هر گروهِ دلخواه دیگری در شاپ شیپ پیام بفرست.", - "excerpt": "همه یک پیام یکسان نمی‌خواهند — پیامِ درست را برای گروهِ درست بفرست.", - "read_minutes": 2, - "content_html": """ -

- فرستادنِ یک پیامِ یکسان به همه‌ی مخاطبین، همیشه بهترین راه نیست — - پیامِ یادآوریِ بدهی برای بدهکارها معنی دارد، نه برای همه. -

-

انتخاب گیرنده‌ها

-

- قبل از ارسال، مخاطبینی که پیام برایشان می‌رود را خودت انتخاب می‌کنی — - مثلا فقط کسانی که بدهیِ - باز دارند، یا فقط - مشتریانِ مدتی - ساکت. -

-

یک قالب، گیرنده‌های متفاوت

-

- همان قالبِ پیام که یک‌بار - ساختی، برای هر گروهی که بعدا انتخاب کنی دوباره قابل‌استفاده است — - فقط لیستِ گیرنده‌ها را عوض می‌کنی. -

-""", - }, - { - "slug": "vaziat-tahvil-payamak", - "title": "چک کردن وضعیت تحویل پیامک‌های ارسالی", - "meta_description": "بعد از ارسال گروهی، بفهم کدام پیامک واقعا تحویل داده شده، کدام در صف مانده و کدام ناموفق بوده — همه در یک لیست.", - "excerpt": "بعد از ارسال، بفهم واقعا چند نفر پیامک را گرفتند.", - "read_minutes": 2, - "content_html": """ -

- «آیا واقعا رسید؟» سوالی است که بعد از هر ارسالِ گروهی می‌آید — به‌خصوص - برای قالب‌های پیام مهم - مثل یادآوریِ پرداخت. -

-

سه وضعیتِ ممکن

-

- هر پیامکِ ارسالی یکی از این سه وضعیت را دارد: تحویل‌شده، در‌صف - (هنوز پردازش می‌شود)، یا ناموفق. لیستِ کاملِ همه‌ی ارسال‌ها، با - وضعیتِ هرکدام، در دسترس است. -

-

وقتی چیزی ناموفق بود

-

- اگر پیامکی به دلیلی (مثلا شماره‌ی اشتباه) ناموفق بود، می‌دانی دقیقا - چه کسی پیام را نگرفته — می‌توانی جداگانه دوباره برایش بفرستی، به‌جای - حدس‌زدن. -

-""", - }, - { - "slug": "kodam-kanal-baraye-kodam-kasbokar", - "title": "کدام کانال برای کدام کسب‌وکار بهتر است؟", - "meta_description": "فروشگاه، آنلاین‌شاپ، یا کسب‌وکار خدماتی — هرکدام معمولا با یک ترکیبِ متفاوت از پیامک/واتساپ/تلگرام بهتر جواب می‌گیرند.", - "excerpt": "نوعِ کسب‌وکارت مشخص می‌کند کدام کانال بیشترین اثر را دارد.", - "read_minutes": 3, - "content_html": """ -

- مقاله‌ی پیامک یا - واتساپ‌بیزینس؟ تفاوتِ فنیِ کانال‌ها را توضیح داد. این‌جا می‌خواهیم - ببینیم کدام کسب‌وکار معمولا کدام ترکیب را بیشتر لازم دارد. -

-

فروشگاه‌های محلی

-

- برای اعلامِ موجودیِ روزانه یا تخفیف، پیامک قابل‌اطمینان‌ترین است — - همه به آن دسترسی دارند، حتی بدون اینترنت. -

-

آنلاین‌شاپ‌های کوچک

-

- اطلاع‌رسانیِ مراحلِ - سفارش معمولا با پیامک بهتر جواب می‌دهد، چون مشتری منتظرِ خبرِ - «ارسال شد» است و نمی‌خواهد وابسته به بازکردنِ یک اپ خاص باشد. -

-

کسب‌وکارهای خدماتی

-

- برای ارتباطِ دوطرفه‌تر (سوال‌وجواب با مشتری)، واتساپ یا تلگرام معمولا - راحت‌تر است — به‌شرطی که مسیرِ رسمیِ هرکدام رعایت شود. -

-""", - }, - # --- خوشه‌ی پیگیری سفارش --- - { - "slug": "tartib-marahel-sefaresh", - "title": "جابه‌جا کردن ترتیب مراحل سفارش با کشیدن و رها کردن", - "meta_description": "مراحلِ پیگیری سفارش در شاپ شیپ ثابت نیستند — با کشیدن و رها کردن، ترتیبشان را دقیقا مطابق جریان کاریِ واقعیت تنظیم کن.", - "excerpt": "ترتیبِ مراحل، دقیقا مطابقِ جریانِ کاریِ واقعیِ خودت — با یک کشیدن و رها کردن.", - "read_minutes": 2, - "content_html": """ -

- همان‌طور که در مقاله‌ی - پیگیری سفارش - آنلاین‌شاپ گفتیم، مراحلِ کار را خودت تعریف می‌کنی. اما گاهی بعد از - مدتی می‌فهمی ترتیبِ اولیه دقیق نبوده. -

-

کشیدن و رها کردن

-

- از صفحه‌ی مراحلِ سفارش، هر مرحله را می‌توانی بگیری و به جای درست - بکشی — بدون نیاز به حذف و ساختِ دوباره. مثلا اگر «بسته‌بندی» را قبل - از «آماده‌سازی» گذاشته بودی، فقط جابه‌جایش کن. -

-

تغییر ترتیب، تاثیری روی سفارش‌های قبلی ندارد

-

- سفارش‌هایی که از قبل در یک مرحله بودند، همان‌جا می‌مانند — تغییرِ - ترتیب فقط روی مسیرِ سفارش‌های بعدی اثر می‌گذارد. -

-""", - }, - { - "slug": "payam-mokhtalef-har-marhale", - "title": "چطور برای هر مرحله‌ی سفارش پیامک متفاوت بفرستم؟", - "meta_description": "هر مرحله‌ی پیگیری سفارش در شاپ شیپ به یک قالبِ پیام جداگانه وصل می‌شود — «آماده شد» یک متن، «ارسال شد» متنی دیگر.", - "excerpt": "هر مرحله، قالبِ پیامِ خودش را دارد — نه یک متنِ یکسان برای همه‌جا.", - "read_minutes": 2, - "content_html": """ -

- یک پیامِ یکسان برای «سفارشت آماده شد» و «سفارشت تحویل داده شد» - منطقی نیست — هرکدام باید خبرِ متفاوتی بدهند. -

-

هر مرحله، یک قالب

-

- وقتی یک مرحله - می‌سازی، همان‌جا یک قالبِ - پیام برایش انتخاب می‌کنی — می‌تواند برای هر مرحله کاملا متفاوت - باشد. -

-

وقتی مرحله فیلدِ اجرا دارد

-

- برای مرحله‌ای مثل «ارسال شد» که یک کدِ رهگیری لازم دارد، قالبِ همان - مرحله می‌تواند فیلدِ - اجرا داشته باشد — همان لحظه از تو پرسیده و در پیامک جای می‌شود. -

-""", - }, - # --- خوشه‌ی راهنمای عمومی/آشناسازی --- - { - "slug": "behtarin-estefade-az-hafte-raygan", - "title": "یک هفته‌ی رایگان شاپ شیپ را چطور بهترین استفاده را ببرم؟", - "meta_description": "راهنمای هفت‌روزه: چه کارهایی در هفته‌ی رایگانِ شاپ شیپ انجام بده تا قبل از هر تصمیمِ پرداختی، ارزشِ واقعیِ اپ را ببینی.", - "excerpt": "هفت روز، هفت قدمِ کوچک — تا قبل از هر تصمیم پرداختی، ارزشِ واقعی را ببینی.", - "read_minutes": 3, - "content_html": """ -

- یک هفته زمانِ کمی است، ولی برای فهمیدنِ این‌که شاپ شیپ واقعا به‌دردت - می‌خورد یا نه، کافی است — اگر درست ازش استفاده کنی. -

-

روزهای اول: داده‌های واقعی

-

- به‌جای امتحان‌کردنِ با داده‌ی الکی، از همین روزِ اول - چند کالای واقعیِ انبارت - و چند مخاطبِ - واقعی را وارد کن. -

-

وسط هفته: یک فاکتور و یک پیام واقعی

-

- یک فاکتورِ واقعی - برای یک فروشِ واقعی بزن، و یک قالبِ پیام بساز و برایِ چند مشتریِ - واقعی بفرست. -

-

آخر هفته: مرور

-

- به دفترِ درآمد و هزینه‌ات نگاه کن — همان چیزی که در این هفته ثبت شده، - تصویرِ کوچکی از این است که یک ماهِ کامل با شاپ شیپ چه شکلی خواهد بود. -

-""", - }, - { - "slug": "amniat-etelaat-kasbokar", - "title": "امنیت اطلاعات کسب‌وکارت در شاپ شیپ", - "meta_description": "قفل اثرانگشت، دیتای اختصاصی هر کاربر، و این‌که چرا اطلاعاتِ مالی و مشتریانِ تو فقط مالِ خودت است.", - "excerpt": "قفل اثرانگشت روی اطلاعاتِ حساس، و دیتایی که فقط مالِ خودت است.", - "read_minutes": 2, - "content_html": """ -

- اطلاعاتِ مالی و لیستِ مشتریانِ یک کسب‌وکار، از حساس‌ترین چیزهایی است - که روی گوشیِ کسی می‌تواند باشد — به همین دلیل امنیت اینجا جدی گرفته - شده. -

-

قفل اثرانگشت

-

- می‌توانی ورود به بخش‌های حساس (مثل - دفتر درآمد و - هزینه) را پشتِ قفلِ اثرانگشتِ گوشی‌ات بگذاری — حتی اگر کسی - گوشی‌ات را باز کند، به این بخش‌ها بدون اثرانگشتِ خودت دسترسی ندارد. -

-

دیتای تو، مالِ خودت

-

- اطلاعاتِ کسب‌وکارِ هر کاربر کاملا از بقیه جدا است — چیزی بینِ - حساب‌های مختلف به اشتراک گذاشته نمی‌شود. -

-""", - }, - { - "slug": "taghvim-shamsi-dar-narmafzar", - "title": "تقویم شمسی در نرم‌افزارهای کسب‌وکار چرا مهم است؟", - "meta_description": "تاریخِ میلادی در نرم‌افزارهای مالی، محاسبه‌ی سررسیدها و مرورِ گزارش‌ها را برای کاربر فارسی‌زبان سخت‌تر می‌کند — شاپ شیپ همه‌جا شمسی است.", - "excerpt": "وقتی سررسیدِ قسط یا تاریخِ فاکتور را می‌بینی، باید همان لحظه معنی‌اش را بفهمی — نه تبدیل کنی.", - "read_minutes": 2, - "content_html": """ -

- خیلی از نرم‌افزارهای مالی و حسابداری با تاریخِ میلادی کار می‌کنند — - و کاربرِ فارسی‌زبان باید در ذهنش تبدیل کند. این تبدیلِ ذهنی، هرچقدر - هم سریع، یک لایه‌ی اضافه‌ی خستگی است. -

-

همه‌جا شمسی

-

- از تاریخِ فاکتور تا - سررسیدِ اقساط تا - نمودارِ دفتر، - همه‌جا با تقویمِ شمسی نمایش داده می‌شود. -

-

یعنی چه در عمل

-

- وقتی می‌بینی «سررسید ۱۵ مهر»، همان لحظه معنایش را می‌فهمی — بدون - این‌که مجبور باشی محاسبه کنی امروز چندمِ میلادی است و چند روز مانده. -

-""", - }, - { - "slug": "jam-chist-chetor-kar-mikonad", - "title": "جم چیست و چطور برای فعال‌سازی ابزارها استفاده می‌شود؟", - "meta_description": "شاپ شیپ به‌جای اشتراکِ همه‌یا‌هیچ، هر بخش را جدا با «جم» فعال می‌کند — جم چطور خریداری و خرج می‌شود.", - "excerpt": "به‌جای اشتراکِ همه‌یا‌هیچ، هر بخش را جدا، فقط با جم، فعال می‌کنی.", - "read_minutes": 2, - "content_html": """ -

- خیلی از نرم‌افزارها یک اشتراکِ ثابت می‌فروشند که شاملِ همه‌ی - امکانات است — حتی آن‌هایی که لازم نداری. شاپ شیپ این مدل را عوض - کرده. -

-

جم چیست

-

- جم واحدِ داخلیِ شاپ شیپ برای فعال‌سازیِ بخش‌هاست — فاکتور، پیام - گروهی، دفتر، انبار، هرکدام قیمتِ جداگانه‌ای به جم دارند. -

-

از کجا جم بگیرم

-

- جم را از فروشگاهِ داخلِ اپ می‌خری، یا از راهِ - دعوتِ دوستان به‌عنوانِ - پاداش دریافت می‌کنی. هر بخش را فقط برایِ مدتی که واقعا لازمش داری - فعال می‌کنی — تمدیدِ اجباری وجود ندارد. -

-""", - }, - { - "slug": "davat-doostan-refferal", - "title": "دعوت دوستان و پاداش معرفی در شاپ شیپ", - "meta_description": "کد معرفِ خودت را با یک صاحب کسب‌وکار دیگر به اشتراک بگذار — هر دو نفر جم پاداش می‌گیرید که می‌شود برای فعال‌سازیِ ابزارها خرج کرد.", - "excerpt": "کدِ معرفت را با یک صاحب کسب‌وکارِ دیگر به اشتراک بگذار — هر دو برنده می‌شوید.", - "read_minutes": 2, - "content_html": """ -

- اگر یک صاحب کسب‌وکارِ دیگر را می‌شناسی که ممکن است شاپ شیپ به دردش - بخورد، معرفیِ او هم به نفعِ او هم به نفعِ خودت است. -

-

چطور کار می‌کند

-

- کدِ معرفِ خودت را (از پروفایلِ خودت در اپ) با او به اشتراک بگذار. - وقتی او با همان کد ثبت‌نام و فعالیتِ واقعی را شروع کند، هر دوی شما - جمِ پاداش - می‌گیرید. -

-

خرجِ جمِ پاداش

-

- جمی که از این راه می‌گیری، دقیقا مثلِ جمِ خریداری‌شده است — می‌توانی - با آن هر بخشی از اپ (فاکتور، پیام گروهی، دفتر) را برای مدتی فعال - کنی، بدون پرداختِ پول. -

-""", - }, - { - "slug": "kasbokar-khord-chist", - "title": "کسب‌وکار خرد چیست و چه نرم‌افزارهایی واقعا به دردش می‌خورد؟", - "meta_description": "یک مغازه‌ی یک‌نفره یا یک آنلاین‌شاپِ کوچک، نیازِ نرم‌افزاریِ متفاوتی از یک شرکتِ بزرگ دارد — چرا سادگی مهم‌تر از قابلیت‌های زیاد است.", - "excerpt": "کسب‌وکارِ یک یا دو‌نفره، نیازِ نرم‌افزاریِ متفاوتی از یک شرکتِ بزرگ دارد.", - "read_minutes": 3, - "content_html": """ -

- «کسب‌وکار خرد» یعنی معمولا یک یا چند نفر، بدونِ بخشِ IT جدا، بدونِ - وقتِ زیاد برای یادگیریِ نرم‌افزارِ پیچیده. نیازهایش با یک شرکتِ - بزرگ خیلی فرق دارد. -

-

چرا نرم‌افزارِ شرکت‌های بزرگ جواب نمی‌دهد

-

- نرم‌افزارهایی که برای تیمِ حسابداریِ چندنفره طراحی شده‌اند، پر از - تنظیماتی هستند که یک مغازه‌دارِ تنها هیچ‌وقت لازمشان ندارد — و همین - پیچیدگی باعث می‌شود اصلا استفاده نشود. -

-

چه چیزی واقعا لازم است

-

- چیزهایی مثل فاکتورِ - ساده، ثبتِ - هزینه‌ی روزمره، و پیامِ - گروهیِ ساده — نه گزارش‌های پیچیده‌ای که هیچ‌وقت باز نمی‌شوند. -

-""", - }, - { - "slug": "checklist-rahandazi-maghaze", - "title": "چک‌لیست راه‌اندازی یک مغازه‌ی کوچک با موبایل", - "meta_description": "از ثبتِ اولین کالا تا صدور اولین فاکتور — چک‌لیستِ قدم‌به‌قدمِ راه‌اندازیِ مدیریتِ یک مغازه‌ی کوچک با شاپ شیپ.", - "excerpt": "از اولین کالا تا اولین فاکتور — یک چک‌لیستِ قدم‌به‌قدم.", - "read_minutes": 3, - "content_html": """ -

- شروعِ مدیریتِ یک مغازه با یک اپِ جدید، اگر قدم‌به‌قدم نباشد، گیج‌کننده - می‌شود. این چک‌لیست، ترتیبِ پیشنهادی است. -

-

قدم اول: انبار

-

- چند کالای پرتکرارت را با - دسته‌بندی و قیمتِ خرید - وارد کن — نیازی نیست همه را یک‌جا وارد کنی، می‌توانی کم‌کم اضافه کنی. -

-

قدم دوم: چند مشتریِ ثابت

-

- اگر مشتریانِ ثابت داری، آن‌ها را با - وارد‌کردن از - مخاطبینِ گوشی اضافه کن. -

-

قدم سوم: اولین فاکتور

-

- برای یک فروشِ واقعی، - اولین فاکتورت را - بزن — از همین‌جا می‌بینی چطور فاکتور، انبار، و دفترت به هم وصل - می‌شوند. -

-

قدم چهارم: اولین پیامِ گروهی

-

- یک قالبِ پیام ساده بساز و - برای همان چند مشتری بفرست — تمام. -

-""", - }, - { - "slug": "shopship-vs-excel", - "title": "تفاوت شاپ شیپ با اکسل برای مدیریت کسب‌وکار", - "meta_description": "اکسل رایگان است ولی محاسبه، یادآوری، و اتصالِ خودکارِ بین فاکتور/انبار/دفتر را ندارد — مقایسه‌ای صادق بین دو رویکرد.", - "excerpt": "اکسل رایگان است، ولی چیزهایی را خودکار نمی‌کند که هر روز وقتت را می‌گیرد.", - "read_minutes": 3, - "content_html": """ -

- اکسل ابزارِ قدرتمندی است و خیلی از کسب‌وکارهای کوچک سال‌ها با آن کار - کرده‌اند. سوال این نیست که اکسل بد است — سوال این است که چه چیزی - اکسل به‌طورِ پیش‌فرض برایت انجام نمی‌دهد. -

-

اتصالِ خودکار بین بخش‌ها

-

- در اکسل، وقتی فاکتور می‌زنی باید جداگانه هم - موجودیِ انبار را کم - کنی، هم دفترت - را به‌روز کنی — سه کار برای یک فروش. در شاپ شیپ این‌ها خودکار به هم - وصل‌اند. -

-

یادآوری و پیام، بدونِ ابزارِ جدا

-

- اکسل نمی‌تواند برایت پیامک بفرستد. برای - یادآوریِ مشتری - یا هشدارِ کمبودِ موجودی، باز باید ابزارِ جدا داشته باشی. -

-

وقتی اکسل هنوز کافی است

-

- اگر فقط چند فروش در هفته داری و هیچ‌وقت نیاز به پیام گروهی یا - پیگیریِ بدهی نداری، شاید اکسل هنوز جواب بدهد. اما همین که کسب‌وکارت - رشد کند، این محاسبات و یادآوری‌های دستی، وقتِ زیادی می‌گیرند. -

-""", - }, - { - "slug": "yek-app-bejaye-chand-narmafzar", - "title": "چطور از یک اپ واحد به‌جای چند نرم‌افزار جدا استفاده کنم؟", - "meta_description": "به‌جای یک اپ برای فاکتور، یک اپ برای پیامک، و یک دفترچه برای حساب‌وکتاب، همه را در شاپ شیپ یک‌جا داشته باش.", - "excerpt": "فاکتور، پیام، انبار، دفتر — همه یک‌جا، و به‌هم وصل.", - "read_minutes": 3, - "content_html": """ -

- خیلی از کسب‌وکارهای کوچک بدونِ این‌که تصمیمِ آگاهانه‌ای گرفته باشند، - سرِ آخر با پنج ابزارِ جدا کار می‌کنند: یک اپ برای فاکتور، یکی برای - پیامک، یک دفترچه برای حساب، و اکسلِ انبار. -

-

مشکلِ اصلیِ ابزارهای جدا

-

- هیچ‌کدام از این ابزارها با هم حرف نمی‌زنند — یعنی هر فروش را باید سه - یا چهار بار جدا ثبت کنی. همان‌طور که در مقاله‌ی - تفاوتِ شاپ شیپ با اکسل - گفتیم، همین دوباره‌کاری‌هاست که وقتِ زیادی می‌گیرد. -

-

وقتی همه‌چیز یک‌جاست

-

- یک فاکتور که - پرداخت می‌شود، خودش انبار و دفتر را به‌روز می‌کند. یک - مشتری که فاکتور - می‌گیرد، خودش پرونده‌اش ساخته می‌شود. کارِ کمتر، خطای کمتر. -

-

شروعِ آسان

-

- نیازی نیست همه‌چیز را یک‌جا شروع کنی — می‌توانی از - چک‌لیستِ - راه‌اندازی شروع کنی و کم‌کم بخش‌های دیگر را اضافه کنی. -

-""", - }, -] - - -def add_articles(apps, schema_editor): - Article = apps.get_model("landing", "Article") - for index, data in enumerate(ARTICLES): - Article.objects.update_or_create( - slug=data["slug"], - defaults={ - "title": data["title"], - "meta_description": data["meta_description"], - "excerpt": data["excerpt"], - "content_html": data["content_html"], - "read_minutes": data["read_minutes"], - "is_published": True, - "published_at": LAST_PREVIOUS_DATE + datetime.timedelta(weeks=index + 1), - }, - ) - - -def remove_articles(apps, schema_editor): - Article = apps.get_model("landing", "Article") - Article.objects.filter(slug__in=[a["slug"] for a in ARTICLES]).delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ("landing", "0001_initial"), - ] - - operations = [ - migrations.RunPython(add_articles, remove_articles), - ] diff --git a/apps/landing/migrations/0003_restore_earlier_articles.py b/apps/landing/migrations/0003_restore_earlier_articles.py deleted file mode 100644 index 97c7abd..0000000 --- a/apps/landing/migrations/0003_restore_earlier_articles.py +++ /dev/null @@ -1,343 +0,0 @@ -import datetime - -from django.db import migrations - -# هشت مقاله‌ی دوره‌ی قبل — این‌ها قبلا در مهاجرت‌های 0002/0003 قدیمی seed -# شده بودند که همراهِ بقیه‌ی فایل‌های مهاجرت از دست رفتند. چون مقاله‌های -# دسته‌ی جدید (0002_articles_batch) مستقیما به این slugها لینک می‌دهند، -# بدونِ این‌ها شش لینکِ داخلی ۴۰۴ می‌شد. تاریخ انتشارشان عمدا در گذشته است -# تا بلافاصله دیده شوند (برخلافِ دسته‌ی جدید که پلکانیِ هفتگی است). - -PUBLISHED_AT = datetime.date(2026, 8, 23) - -ARTICLES = [ - { - "slug": "peyam-goruhi-ba-shomare", - "title": "پیام گروهی با شماره چیست و چطور بدون مسدود شدن ارسال کنیم؟", - "meta_description": "راهنمای ارسال پیام گروهی با شماره در شاپ شیپ: فرق روش رسمی (Business API/Bot) با روش‌های غیررسمی، و چرا روش دوم دیر یا زود منجر به مسدودشدن اکانت می‌شود.", - "excerpt": "فرق ارسال گروهی رسمی با روش‌های غیررسمی، و چرا مسیر Business/Bot API تنها راه پایدار است.", - "read_minutes": 4, - "content_html": """ -

- خیلی از کسب‌وکارهای کوچک اولین باری که «پیام گروهی» را امتحان می‌کنند، - یا از یک نرم‌افزار مشکوک استفاده می‌کنند که چند شماره را همزمان روی - چند گوشی می‌چرخاند، یا دستی و طاقت‌فرسا یکی‌یکی کپی‌پیست می‌کنند. هر دو - راه یک مشکل مشترک دارند: یکی امن نیست، یکی زمان‌بر. شاپ شیپ یک راه سوم - را انتخاب کرده — دقیقا همان مسیری که خودِ پلتفرم‌ها رسمی اعلام کرده‌اند. -

-

چرا اکانت‌ها مسدود می‌شوند

-

- واتساپ، تلگرام، بله و ایتا همه یک سیستم ضدهرزنامه دارند که رفتار - غیرطبیعی (ارسال حجم بالا از یک شماره‌ی معمولی، چرخاندن چند دستگاه/سشن، - شبیه‌سازی رفتار انسانی برای فرار از تشخیص) را شناسایی و مسدود می‌کند. - این محدودیت تصادفی نیست — دقیقا همان مکانیزمی است که جلوی اسپم واقعی - را می‌گیرد، و هیچ «ترفند» پایداری برای دور زدنش وجود ندارد. -

-

راه پایدار: مسیر رسمی هر پلتفرم

-

- پیامک از طریق درگاه‌های بالک اپراتور، واتساپ - از طریق WhatsApp Business Platform (با Template از‌پیش‌تاییدشده)، و - تلگرام/بله/ایتا از طریق Bot API — با یک محدودیت مهم: - بات فقط می‌تواند به کسانی پیام بدهد که خودشان قبلا چت را با بات شروع - کرده‌اند (opt-in). جزئیاتِ انتخاب بین این کانال‌ها در مقاله‌ی - پیامک یا واتساپ‌بیزینس؟ - آمده است. -

-

شاپ شیپ چطور این را حل می‌کند

-

- برای کانال‌هایی که opt-in لازم دارند و مخاطب هنوز چت را شروع نکرده، - شاپ شیپ به‌جای اتوماسیونِ مخفی و پرخطر، یک صفِ ارسال دستیِ یک‌لمسی نشان - می‌دهد: قالب پیام را از قبل آماده کرده‌ای، اپ مقصد را با متنِ آماده باز - می‌کند، خودت فقط دکمه‌ی ارسال را می‌زنی. -

-""", - }, - { - "slug": "field-sefareshi-dar-template", - "title": "فیلد اجرا در قالب پیام یعنی چه؟", - "meta_description": "قالب پیام در شاپ شیپ می‌تواند یک «فیلد اجرا» داشته باشد — مقداری که هر بار لحظه‌ی ارسال پرسیده می‌شود، نه از قبل ثبت شده.", - "excerpt": "یک فیلد در قالب پیام که مقدارش از قبل معلوم نیست و هر بار لحظه‌ی ارسال پرسیده می‌شود — مثلا کد رهگیری پستی.", - "read_minutes": 3, - "content_html": """ -

- قالب پیام معمولی یک متن ثابت با چند جای‌خالیِ از‌قبل‌معلوم است، مثلا - «سلام {نام} عزیز». اما بعضی وقت‌ها مقداری که باید در پیام بیاید اصلا - از قبل معلوم نیست — مثلا کد رهگیری پستیِ یک مرسوله که همان لحظه‌ی - تحویل به پست تولید می‌شود. این‌جاست که «فیلد اجرا» به کمک می‌آید. -

-

فرقش با جای‌خالی‌های معمولی چیست

-

- جای‌خالی‌های معمولیِ قالب (مثل نام یا شماره) از اطلاعاتِ همان مخاطب پر - می‌شوند — از قبل در اپ ثبت شده‌اند. فیلد اجرا برعکس است: هیچ مقدار - از‌پیش‌ذخیره‌شده‌ای ندارد؛ دقیقا همان لحظه که پیام دارد ارسال می‌شود، - اپ یک‌بار از تو می‌پرسدش. -

-

کاربرد اول: پیگیری سفارش

-

- وقتی برای یک مرحله از - پیگیری سفارش - قالبی انتخاب می‌کنی که فیلد اجرا دارد، همین که سفارشی به آن مرحله - می‌رسد، اپ همان لحظه مقدار را می‌پرسد — مثلا کد رهگیری — و بعد پیامک - را با همان مقدار برای مشتری می‌فرستد. -

-

کاربرد دوم: ارسال گروهی

-

- در ارسال گروهی هم اگر قالب انتخابی فیلد اجرا داشته باشد، اپ مخاطبین - را یکی‌یکی نشان می‌دهد و مقدار را برای هرکدام می‌پرسد — و برای - راحتی، مقداری که برای نفرِ قبلی وارد کردی به‌عنوان پیش‌فرضِ نفرِ بعدی - هم پیشنهاد می‌شود. -

-

هر قالب فقط یک فیلد اجرا دارد

-

- ساده نگه داشتنش عمدی است: هر قالب پیام حداکثر یک فیلد اجرا دارد، با - یک برچسبِ دلخواه (مثلا «کد رهگیری»). اگر قالبی این فیلد را نداشته - باشد، مثل قبل مستقیم ارسال می‌شود، بدون هیچ پرسش اضافه‌ای. -

-""", - }, - { - "slug": "merge-mokhatebin-ganoshi", - "title": "وارد کردن مخاطبین گوشی به شاپ شیپ", - "meta_description": "سریع‌ترین راه برای شروع: مخاطبین گوشی‌ات را با یک تیک انتخاب‌همه، بدون تعیین گروه و بدون تایپ دستی، به مخاطبین یا مشتریانِ شاپ شیپ اضافه کن.", - "excerpt": "مخاطبین گوشی‌ات را با یک تیک انتخاب‌همه وارد کن — بدون گروه‌بندی اضافه، بدون تایپ دستی.", - "read_minutes": 2, - "content_html": """ -

- اگر لیست مشتری یا مخاطبت همین حالا در دفترچه‌تلفنِ گوشی‌ات است، نیازی - به تایپ دوباره‌ی تک‌تک شماره‌ها نیست. شاپ شیپ می‌تواند مستقیم به - مخاطبین گوشی‌ات دسترسی بگیرد (فقط با اجازه‌ی خودت) و هرکدام را که - خواستی یک‌جا وارد اپ کند. -

-

چطور کار می‌کند

-

- از صفحه‌ی «مخاطبین» یا «مشتریان»، گزینه‌ی «وارد کردن از مخاطبین گوشی» - را بزن. اپ لیست مخاطبین گوشی‌ات را نشان می‌دهد — با یک تیکِ - «انتخاب همه» بالای لیست برای وقتی که می‌خواهی همه را یک‌جا اضافه کنی، - یا تیک تک‌تک برای انتخاب دستی. -

-

بدون مرحله‌ی گروه‌بندی

-

- برخلاف نسخه‌های قدیمی‌تر، این‌جا دیگر مجبور نیستی قبل از وارد کردن - یک «گروه مقصد» انتخاب کنی — همین یک مرحله‌ی کمتر یعنی وارد کردن ده‌ها - مخاطب چند ثانیه بیشتر طول نمی‌کشد. -

-

دوباره‌کاری نمی‌شود

-

- مخاطبینی که شماره‌شان از قبل در اپ ثبت شده، خودشان از لیست وارد‌کردن - کنار گذاشته می‌شوند — تطبیق بر اساس شماره‌ی نرمال‌شده انجام می‌شود، نه - اسم، پس اختلاف املایی اسم مشکلی ایجاد نمی‌کند. -

-""", - }, - { - "slug": "sms-ya-whatsapp-business", - "title": "پیامک یا واتساپ‌بیزینس؟ کدام برای ارسال گروهی بهتر است", - "meta_description": "مقایسه‌ی پیامک و واتساپ‌بیزینس برای ارسال پیام گروهی در شاپ شیپ: هزینه، نرخ باز شدن، محدودیت Template، و کدام برای چه کسب‌وکاری مناسب‌تر است.", - "excerpt": "مقایسه‌ی هزینه، نرخ باز شدن، و محدودیت‌های هرکدام برای پیام گروهی کسب‌وکارها.", - "read_minutes": 5, - "content_html": """ -

- هر دو کانال جای خودشان را دارند و در شاپ شیپ می‌توانی همزمان از هردو - (به‌علاوه‌ی تلگرام، بله و ایتا) استفاده کنی — یک - قالب پیام بنویسی، - برای هرکدام از کانال‌ها بفرستی. -

-

پیامک

-

- به هر شماره‌ای می‌رسد، حتی بدون اینترنت یا اپ نصب‌شده — قابل‌اطمینان‌ترین - کانال از نظر تحویل. هزینه‌ی هر پیام مشخص و قابل‌پیش‌بینی است. محدودیت: - طول پیام (پیامک‌های بلند چند‌قسمتی حساب می‌شوند) و نداشتن تصویر/دکمه. -

-

واتساپ‌بیزینس

-

- نرخ باز شدن پیام معمولا بالاتر از پیامک است و امکاناتی مثل دکمه و - رسانه دارد. اما برای ارسال گروهی (business-initiated) باید از یک - Template از‌پیش‌تاییدشده در Meta Business Manager استفاده کنی — - یعنی نمی‌توانی هر متن دلخواهی را بی‌محدودیت به هرکسی بفرستی. -

-

تلگرام، بله، ایتا

-

- این‌ها فقط به کسانی می‌رسند که قبلا با بات کسب‌وکارت چت را شروع - کرده‌اند. برای مخاطبینی که این کار را نکرده‌اند، شاپ شیپ یک صف - ارسال دستیِ یک‌لمسی نشان می‌دهد. -

-

جمع‌بندی

-

- اگر تحویل تضمینی برایت مهم‌تر است، پیامک را انتخاب کن. اگر مخاطبینت - بیشتر واتساپی هستند و Template تاییدشده داری، واتساپ‌بیزینس گزینه‌ی - بهتری است. بهترین نتیجه معمولا ترکیب چند کانال است. -

-""", - }, - { - "slug": "forosh-ghesti-hesabdari", - "title": "فروش قسطی چطور باید در دفترداری ثبت شود؟", - "meta_description": "فروش قسطی از نظر حسابداری یک نوع بدهکاری/طلب است، نه فقط یک عدد در لیست درآمد. دفتر درآمد و هزینه‌ی شاپ شیپ این تفاوت را خودش تشخیص می‌دهد.", - "excerpt": "فروش قسطی فقط «درآمد» نیست — یک نوع طلب از مشتری است که باید جدا پیگیری شود.", - "read_minutes": 4, - "content_html": """ -

- فرض کن یک کالا را ۵ میلیون تومان قسطی می‌فروشی: امروز ۲ میلیون - نقد می‌گیری، ۳ میلیون باقی‌مانده قرار است طی سه قسط پرداخت شود. سوال - این‌جاست: همین امروز چقدر «درآمد» داشته‌ای؟ -

-

چرا فروش قسطی «فقط درآمد» نیست

-

- از نظر حسابداریِ حرفه‌ای، فروش قسطی یک نوع بدهکاریِ مشتری به - تو (طلب/receivable) ایجاد می‌کند، نه یک عدد ساده در لیست - درآمد. اگر همان ۵ میلیون را یک‌جا «درآمد» ثبت کنی، ترازِ نقدینگی‌ات - اشتباه می‌شود — چون آن ۳ میلیونِ باقی‌مانده هنوز دستت نیامده. -

-

دفتر درآمد و هزینه‌ی شاپ شیپ چطور حلش می‌کند

-

- وقتی دسته‌بندیِ «فروش قسطی» را برای یک ثبت انتخاب می‌کنی، اپ خودش - می‌فهمد این یک نوع بدهکاری است — و سه فیلد اضافه از تو می‌پرسد: مبلغِ - کل، نام و شماره‌ی طرفِ حساب. از همان لحظه، این ثبت یک «مانده‌حساب» - دارد که هر قسطِ پرداختی از آن کم می‌شود. -

-

ثبتِ ساده برای تو، حسابداریِ درست پشتِ صحنه

-

- لازم نیست اصطلاحاتِ حسابداری بلد باشی. کافی است دسته‌ی درستی که به - زبانِ خودت نزدیک‌تر است انتخاب کنی — بقیه‌اش (تشخیص طلب/بدهی، محاسبه‌ی - مانده، تفکیک از درآمد نقدی) را اپ خودش پشتِ صحنه انجام می‌دهد. -

-

اتصال به فاکتور و مشتری

-

- اگر فاکتوری را با - نوعِ پرداختِ «قسطی/نسیه» صادر کنی، شاپ شیپ همان لحظه یک ثبتِ طلب در - دفترت می‌سازد و آن را به پرونده‌ی همان مشتری هم وصل می‌کند. -

-""", - }, - { - "slug": "fatoore-rasmi-baraye-maghaze", - "title": "فاکتور رسمی برای مغازه: چرا لازم است و چطور با انبار وصل می‌شود", - "meta_description": "فاکتور رسمی با لوگو، محاسبه‌ی خودکار مالیات و تخفیف، و اتصال مستقیم به انبار — همین که پرداخت شد، موجودی خودش کم می‌شود.", - "excerpt": "فاکتوری که وقتی پرداخت می‌شود، موجودیِ انبار را هم خودش کم می‌کند — بدون ثبت دوباره.", - "read_minutes": 4, - "content_html": """ -

- خیلی از مغازه‌دارها فاکتور را یا اصلا صادر نمی‌کنند، یا دستی روی - یک برگه می‌نویسند — و بعد جداگانه همان کالا را از موجودیِ انبار کم - می‌کنند. این یعنی هر فروش، دوبار ثبت می‌شود. -

-

یک فاکتور، هم برای مشتری هم برای انبار

-

- در شاپ شیپ، وقتی برای یک فاکتور کالا اضافه می‌کنی، همان کالا را از - لیستِ محصولاتِ - انبارت انتخاب - می‌کنی — نه این‌که اسمش را دوباره تایپ کنی. همین که فاکتور ثبت شود، - تعداد همان کالا خودش از موجودیِ انبار کم می‌شود. -

-

مالیات و تخفیف، خودکار

-

- هر قلمِ کالا می‌تواند نرخِ مالیاتِ خودش و تخفیفِ خودش را داشته باشد — - جمعِ نهایی، مالیات، و مبلغِ تخفیف‌خورده خودکار محاسبه می‌شود. -

-

پرداخت‌شده یا نشده، همیشه مشخص

-

- هر فاکتور یک وضعیتِ روشن دارد. برای فاکتورهای نقدی، دفترِ درآمد و - هزینه‌ات را دستکاری نمی‌کند؛ اما اگر نوعِ پرداخت را «قسطی/نسیه» - بگذاری، همان لحظه یک ثبتِ طلب هم در دفترت ساخته می‌شود — جزئیاتش در - فروش قسطی چطور ثبت - می‌شود؟ آمده. -

-""", - }, - { - "slug": "kontorol-mojoodi-anbar-ba-mobile", - "title": "کنترل موجودی انبار با موبایل، بدون اکسل", - "meta_description": "دسته‌بندی کالا، کم‌وزیاد کردن موجودی با یک ضربه، دیدنِ ارزش کلِ انبار، و هشدار وقتی چیزی کم می‌شود — همه از گوشی.", - "excerpt": "موجودی را با یک ضربه کم‌وزیاد کن، ارزش کلِ انبارت را ببین، وقتی چیزی کم شد زودتر از مشتری بفهم.", - "read_minutes": 3, - "content_html": """ -

- خیلی از کسب‌وکارهای کوچک موجودیِ انبار را در یک فایلِ اکسل نگه - می‌دارند که همیشه یک نسخه‌ی «قدیمی‌تر از واقعیت» است. -

-

هر کالا با واحد و قیمتِ خرید

-

- برای هر کالا، واحدِ شمارشش (عدد، کیلوگرم، بسته و…) و قیمتِ خریدش را - ثبت می‌کنی. با همین دو عدد، اپ خودش ارزشِ کلِ انبارت - را محاسبه می‌کند — یعنی می‌دانی چقدر سرمایه روی طاقچه‌هایت خوابیده. -

-

کم‌وزیاد با یک ضربه

-

- موجودیِ هر کالا را می‌شود با یک ضربه کم یا زیاد کرد. اگر هم - فاکتور برای همان - کالا صادر کنی، موجودی خودش کم می‌شود، بدون ثبتِ دستیِ دوباره. -

-

هشدارِ کمبود

-

- برای هر کالا می‌شود یک آستانه‌ی «کم شد» تعریف کرد؛ وقتی موجودی به آن - آستانه برسد، پیش از این‌که مشتری بیاید و نداشته باشی، خودت متوجه - می‌شوی. -

-""", - }, - { - "slug": "peygiri-sefaresh-online-shop-etemad", - "title": "پیگیری سفارش آنلاین‌شاپ: اعتماد مشتری با یک پیامک خودکار", - "meta_description": "برای آنلاین‌شاپ‌های کوچک، شاپ شیپ مراحل سفارش را قابل‌تعریف می‌کند و با رسیدنِ سفارش به هر مرحله، خودکار از روی قالبِ دلخواهت به مشتری پیامک می‌رود.", - "excerpt": "مراحل کارت را تعریف کن — با رسیدنِ سفارش به هر مرحله، پیامکِ خودکار برای مشتری می‌رود.", - "read_minutes": 4, - "content_html": """ -

- یکی از بزرگ‌ترین دلایلِ نگرانیِ مشتریِ آنلاین‌شاپ‌های کوچک همین است: - «سفارشم الان کجاست؟» وقتی جوابِ این سوال را با تاخیر یا اصلا نمی‌دهی، - اعتماد از دست می‌رود. -

-

مراحلِ کارِ خودت را تعریف کن

-

- در شاپ شیپ، مراحلِ سفارش را خودت تعریف می‌کنی — مثلا «آماده‌سازی»، - «بسته‌بندی»، «ارسال»، «تحویل‌شد» — دقیقا همان جریانِ کاریِ واقعیِ - کسب‌وکارت. ترتیبِ مراحل را هم می‌شود با کشیدن‌و‌رهاکردن جابه‌جا کرد. -

-

هر مرحله، یک قالبِ پیام

-

- هر مرحله را به یک قالبِ پیام وصل می‌کنی. همین که سفارشی به آن مرحله - می‌رسد، پیامک از روی همان قالب — خودکار — برای مشتری می‌رود. -

-

وقتی یک مقدار متغیر لازم است

-

- برای مرحله‌ی «ارسال» معمولا یک کد رهگیریِ پستی لازم است که از قبل - معلوم نیست. برای همین، قالبِ همین مرحله می‌تواند یک - فیلدِ اجرا - داشته باشد. -

-""", - }, -] - - -def add_articles(apps, schema_editor): - Article = apps.get_model("landing", "Article") - for data in ARTICLES: - Article.objects.update_or_create( - slug=data["slug"], - defaults={ - "title": data["title"], - "meta_description": data["meta_description"], - "excerpt": data["excerpt"], - "content_html": data["content_html"], - "read_minutes": data["read_minutes"], - "is_published": True, - "published_at": PUBLISHED_AT, - }, - ) - - -def remove_articles(apps, schema_editor): - Article = apps.get_model("landing", "Article") - Article.objects.filter(slug__in=[a["slug"] for a in ARTICLES]).delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ("landing", "0002_articles_batch"), - ] - - operations = [ - migrations.RunPython(add_articles, remove_articles), - ] diff --git a/apps/messaging/migrations/0001_initial.py b/apps/messaging/migrations/0001_initial.py deleted file mode 100644 index db04df4..0000000 --- a/apps/messaging/migrations/0001_initial.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('banker', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='BulkMessage', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('body_override', models.TextField(blank=True, verbose_name='متن آزاد (اگر قالب انتخاب نشده)')), - ('channels', models.JSONField(default=list, verbose_name='کانال\u200cهای انتخاب\u200cشده')), - ('status', models.CharField(choices=[('dispatched', 'ارسال\u200cشده'), ('partially_manual', 'بخشی نیاز به تایید دستی')], default='dispatched', max_length=20)), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='banker.contactgroup')), - ('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='banker.messagetemplate')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='bulk_messages', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'ارسال گروهی', - 'verbose_name_plural': 'ارسال\u200cهای گروهی', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='MessageRecipient', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('channel', models.CharField(choices=[('sms', 'پیامک'), ('whatsapp', 'واتساپ'), ('telegram', 'تلگرام'), ('bale', 'بله'), ('eitaa', 'ایتا')], max_length=10)), - ('status', models.CharField(choices=[('sent', 'ارسال\u200cشده'), ('failed', 'ناموفق'), ('needs_manual', 'نیاز به تایید دستی'), ('manual_confirmed', 'تایید دستی شد')], max_length=20)), - ('rendered_body', models.TextField()), - ('error', models.CharField(blank=True, max_length=255)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('bulk_message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='messaging.bulkmessage')), - ('contact', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='banker.contact')), - ], - options={ - 'verbose_name': 'گیرنده\u200cی پیام', - 'verbose_name_plural': 'گیرنده\u200cهای پیام', - 'indexes': [models.Index(fields=['bulk_message', 'status'], name='messaging_m_bulk_me_a7a1eb_idx')], - }, - ), - ] diff --git a/apps/notifications/migrations/0001_initial.py b/apps/notifications/migrations/0001_initial.py deleted file mode 100644 index 6909abe..0000000 --- a/apps/notifications/migrations/0001_initial.py +++ /dev/null @@ -1,46 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='Notification', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('title', models.CharField(max_length=150, verbose_name='عنوان')), - ('body', models.TextField(blank=True, verbose_name='متن')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='notifications', to=settings.AUTH_USER_MODEL, verbose_name='کاربر (خالی = عمومی برای همه)')), - ], - options={ - 'verbose_name': 'اعلان', - 'verbose_name_plural': 'اعلان\u200cها', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='NotificationRead', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('read_at', models.DateTimeField(auto_now_add=True)), - ('notification', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reads', to='notifications.notification')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='notification_reads', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'خوانده\u200cشدنِ اعلان', - 'verbose_name_plural': 'خوانده\u200cشدنِ اعلان\u200cها', - 'constraints': [models.UniqueConstraint(fields=('user', 'notification'), name='unique_user_notification_read')], - }, - ), - ] diff --git a/apps/orders/migrations/0001_initial.py b/apps/orders/migrations/0001_initial.py deleted file mode 100644 index bfde1dc..0000000 --- a/apps/orders/migrations/0001_initial.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:19 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('banker', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='OrderStage', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('label', models.CharField(max_length=150, verbose_name='عنوان مرحله')), - ('sort_order', models.PositiveIntegerField(default=0, verbose_name='ترتیب')), - ('requires_extra_input', models.BooleanField(default=False, verbose_name='این مرحله به یک ورودی اضافه نیاز دارد')), - ('extra_input_label', models.CharField(blank=True, help_text='مثلا: کد تسویه، کد محصول — وقتی سفارش به این مرحله می\u200cرسد از کاربر پرسیده می\u200cشود.', max_length=100, verbose_name='عنوان ورودی اضافه')), - ('updated_at', models.DateTimeField(auto_now=True)), - ('template', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_stages', to='banker.messagetemplate', verbose_name='قالب پیامک این مرحله')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='order_stages', to=settings.AUTH_USER_MODEL)), - ], - options={ - 'verbose_name': 'مرحله\u200cی پیگیری سفارش', - 'verbose_name_plural': 'مراحل پیگیری سفارش', - 'ordering': ['sort_order'], - }, - ), - migrations.CreateModel( - name='Order', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('customer_name', models.CharField(max_length=150, verbose_name='نام مشتری')), - ('customer_phone', models.CharField(max_length=20, verbose_name='شماره مشتری')), - ('tracking_code', models.CharField(blank=True, max_length=100, verbose_name='کد پیگیری پستی')), - ('is_completed', models.BooleanField(default=False, verbose_name='تکمیل\u200cشده')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('updated_at', models.DateTimeField(auto_now=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='orders', to=settings.AUTH_USER_MODEL)), - ('current_stage', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='orders.orderstage')), - ], - options={ - 'verbose_name': 'سفارش', - 'verbose_name_plural': 'سفارش\u200cها', - 'ordering': ['-updated_at'], - }, - ), - migrations.CreateModel( - name='OrderStageLog', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('stage_label', models.CharField(max_length=150, verbose_name='عنوان مرحله')), - ('message_body', models.TextField(blank=True, verbose_name='متن پیامک ارسالی')), - ('extra_input_value', models.CharField(blank=True, max_length=255, verbose_name='ورودی اضافه\u200cی این مرحله')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stage_logs', to='orders.order')), - ('stage', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='orders.orderstage')), - ], - options={ - 'verbose_name': 'تاریخچه\u200cی مرحله\u200cی سفارش', - 'verbose_name_plural': 'تاریخچه\u200cی مراحل سفارش', - 'ordering': ['created_at'], - }, - ), - ] diff --git a/apps/telemetry/migrations/0001_initial.py b/apps/telemetry/migrations/0001_initial.py deleted file mode 100644 index 86f28d3..0000000 --- a/apps/telemetry/migrations/0001_initial.py +++ /dev/null @@ -1,114 +0,0 @@ -# Generated by Django 5.1.15 on 2026-09-02 00:25 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='AppSession', - fields=[ - ('id', models.CharField(max_length=36, primary_key=True, serialize=False)), - ('platform', models.CharField(blank=True, max_length=30, verbose_name='پلتفرم')), - ('app_version', models.CharField(blank=True, max_length=30, verbose_name='نسخه\u200cی اپ')), - ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='آی\u200cپی')), - ('started_at', models.DateTimeField(verbose_name='شروع')), - ('ended_at', models.DateTimeField(blank=True, null=True, verbose_name='پایان')), - ('created_at', models.DateTimeField(auto_now_add=True)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='app_sessions', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')), - ], - options={ - 'verbose_name': 'جلسه\u200cی استفاده از اپ', - 'verbose_name_plural': 'جلسه\u200cهای استفاده از اپ', - 'ordering': ['-started_at'], - }, - ), - migrations.CreateModel( - name='ClickEvent', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('screen_name', models.CharField(blank=True, max_length=150, verbose_name='نام صفحه')), - ('element', models.CharField(blank=True, max_length=150, verbose_name='برچسبِ عنصر (اختیاری)')), - ('x', models.FloatField(blank=True, null=True)), - ('y', models.FloatField(blank=True, null=True)), - ('created_at', models.DateTimeField(verbose_name='زمان')), - ('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='clicks', to='telemetry.appsession')), - ], - options={ - 'verbose_name': 'رویداد کلیک', - 'verbose_name_plural': 'رویدادهای کلیک', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='ClientErrorLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('screen_name', models.CharField(blank=True, max_length=150, verbose_name='نام صفحه')), - ('message', models.TextField(verbose_name='پیام خطا')), - ('stack_trace', models.TextField(blank=True, verbose_name='Stack trace')), - ('created_at', models.DateTimeField(verbose_name='زمان')), - ('session', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='errors', to='telemetry.appsession')), - ], - options={ - 'verbose_name': 'خطای سمت کاربر', - 'verbose_name_plural': 'خطاهای سمت کاربر', - 'ordering': ['-created_at'], - }, - ), - migrations.CreateModel( - name='PageView', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('screen_name', models.CharField(max_length=150, verbose_name='نام صفحه')), - ('entered_at', models.DateTimeField(verbose_name='زمان ورود')), - ('duration_seconds', models.FloatField(blank=True, null=True, verbose_name='مدت ماندن (ثانیه)')), - ('session', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='page_views', to='telemetry.appsession')), - ], - options={ - 'verbose_name': 'بازدید صفحه', - 'verbose_name_plural': 'بازدیدهای صفحه', - 'ordering': ['entered_at'], - }, - ), - migrations.CreateModel( - name='ApiCallLog', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('method', models.CharField(max_length=10, verbose_name='متد')), - ('path', models.CharField(max_length=255, verbose_name='مسیر')), - ('status_code', models.PositiveSmallIntegerField(verbose_name='کد وضعیت')), - ('duration_ms', models.FloatField(verbose_name='زمان پاسخ (میلی\u200cثانیه)')), - ('ip_address', models.GenericIPAddressField(blank=True, null=True, verbose_name='آی\u200cپی')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='زمان')), - ('user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='api_calls', to=settings.AUTH_USER_MODEL, verbose_name='کاربر')), - ], - options={ - 'verbose_name': 'لاگ فراخوانی API', - 'verbose_name_plural': 'لاگ\u200cهای فراخوانی API', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['user', 'created_at'], name='telemetry_a_user_id_7a7591_idx'), models.Index(fields=['path', 'created_at'], name='telemetry_a_path_267c85_idx')], - }, - ), - migrations.AddIndex( - model_name='appsession', - index=models.Index(fields=['user', 'started_at'], name='telemetry_a_user_id_142b1f_idx'), - ), - migrations.AddIndex( - model_name='clickevent', - index=models.Index(fields=['screen_name'], name='telemetry_c_screen__d3940b_idx'), - ), - migrations.AddIndex( - model_name='pageview', - index=models.Index(fields=['screen_name'], name='telemetry_p_screen__242757_idx'), - ), - ]