adding
This commit is contained in:
@@ -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': 'کاربران',
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='رایگان'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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)]
|
|
||||||
@@ -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)]
|
|
||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='پلتفرم ارتباطی'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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 ساده مثل <p>، <h2>، <strong>.', 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'),
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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": """
|
|
||||||
<p>
|
|
||||||
یک سوال ساده که جواب دادنش سختتر از چیزی است که فکر میکنی: «همین
|
|
||||||
الان چقدر پول نقد در دسترس داری؟» خیلی از کسبوکارهای کوچک این عدد را
|
|
||||||
با «جمعِ کل فروش» اشتباه میگیرند — در حالی که بخشی از آن فروش هنوز
|
|
||||||
طلب است، نه پول در جیب.
|
|
||||||
</p>
|
|
||||||
<h2>درآمد یعنی چه، طلب یعنی چه</h2>
|
|
||||||
<p>
|
|
||||||
درآمدِ نقدی یعنی پولی که همین الان دستت رسیده. طلب (receivable) یعنی
|
|
||||||
حقی که داری تا در آینده پول را بگیری — مثل وقتی جنسی را
|
|
||||||
<a href="/articles/forosh-ghesti-hesabdari/">قسطی میفروشی</a>. هر دو
|
|
||||||
«فروش» حساب میشوند، اما فقط یکیشان همین الان قابلخرجکردن است.
|
|
||||||
</p>
|
|
||||||
<h2>چرا این فرق مهم است</h2>
|
|
||||||
<p>
|
|
||||||
اگر طلب را با درآمدِ نقدی یکی حساب کنی، فکر میکنی پول بیشتری داری
|
|
||||||
که واقعا نداری — و همین باعث میشود در پرداختِ هزینههای خودت (اجاره،
|
|
||||||
خرید جنسِ تازه) کم بیاوری. دفتر درآمد و هزینهی شاپ شیپ این دو را از
|
|
||||||
هم جدا نگه میدارد تا همیشه بدانی چقدر واقعا در دسترس داری.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "csv-daftar-baraye-hesabdar",
|
|
||||||
"title": "چطور خروجی CSV دفتر حساب را برای حسابدار بفرستم؟",
|
|
||||||
"meta_description": "دفتر درآمد و هزینهی شاپ شیپ را برای یک بازهی دلخواه به فایل CSV تبدیل و برای حسابدارت ارسال کن — سازگار با اکسل فارسی.",
|
|
||||||
"excerpt": "یک بازهی تاریخ انتخاب کن، خروجی CSV بگیر، برای حسابدارت بفرست.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از کسبوکارهای کوچک هر چند ماه یکبار باید دفترشان را برای یک
|
|
||||||
حسابدار بفرستند — برای اظهارنامهی مالیاتی یا فقط یک مرور کلی. شاپ
|
|
||||||
شیپ این کار را به چند ثانیه رسانده.
|
|
||||||
</p>
|
|
||||||
<h2>خروجی گرفتن</h2>
|
|
||||||
<p>
|
|
||||||
از دفتر درآمد و هزینه، یک بازهی تاریخِ دلخواه (مثلا یک ماه یا یک
|
|
||||||
فصل) انتخاب کن و «خروجی CSV» را بزن. فایلی میسازد که مستقیم در اکسل
|
|
||||||
یا گوگلشیت باز میشود — با حروف فارسیِ درست، بدون بههمریختگی.
|
|
||||||
</p>
|
|
||||||
<h2>چه چیزی در فایل هست</h2>
|
|
||||||
<p>
|
|
||||||
هر ردیف یک ثبت است: تاریخ، دستهبندی (مثلا فروش نقدی یا
|
|
||||||
<a href="/articles/kharid-nasiye-dar-daftardari/">خرید نسیه</a>)، مبلغ،
|
|
||||||
و یادداشتی که خودت نوشته بودی. برای ثبتهای بدهی/طلب، ماندهحساب هم
|
|
||||||
در فایل میآید — حسابدارت همان چیزی را میبیند که تو در اپ میبینی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "modiriyat-bedehi-moshtarian",
|
|
||||||
"title": "مدیریت بدهی مشتریان با موبایل",
|
|
||||||
"meta_description": "همهی مشتریانی که به تو بدهکارند، ماندهحساب و تاریخچهی اقساطشان را در یک صفحه ببین — بدون دفترچهی کاغذی.",
|
|
||||||
"excerpt": "ماندهحساب هر مشتری، خودکار از فاکتورها و دفترت محاسبه میشود — نه با دفترچهی کاغذی.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از مغازهدارها یک دفترچهی کاغذیِ جدا برای «کیا بهم بدهکارن»
|
|
||||||
دارند — که هم گم میشود، هم بهروزرسانیش یادشان میرود. در شاپ شیپ،
|
|
||||||
این دفترچه دیگر لازم نیست.
|
|
||||||
</p>
|
|
||||||
<h2>ماندهحساب خودکار</h2>
|
|
||||||
<p>
|
|
||||||
هر بار که یک <a href="/articles/forosh-ghesti-hesabdari/">فروش
|
|
||||||
قسطی</a> برای یک مشتری ثبت میکنی یا یک قسط از او میگیری، ماندهحساب
|
|
||||||
همان مشتری در پروفایلش خودکار بهروز میشود — جمع تمام بدهیهای بازش،
|
|
||||||
نه فقط آخرین ثبت.
|
|
||||||
</p>
|
|
||||||
<h2>یک نگاه، همهی بدهکارها</h2>
|
|
||||||
<p>
|
|
||||||
از <a href="/articles/parvande-moshtari-chist/">پروندهی مشتری</a>
|
|
||||||
میتوانی ببینی چه کسی چقدر بدهکار است و آخرین قسطش کِی بوده — و اگر
|
|
||||||
لازم شد، از همانجا یک پیامکِ یادآوری برایش بفرستی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "sabt-hazine-rozane-maghaze",
|
|
||||||
"title": "ثبت هزینههای روزمرهی مغازه: از کجا شروع کنم؟",
|
|
||||||
"meta_description": "اجاره، قبض، حقوق، خرید جنس — راهنمای شروع ثبت هزینههای روزمرهی یک کسبوکار کوچک در دفتر درآمد و هزینهی شاپ شیپ.",
|
|
||||||
"excerpt": "لازم نیست حسابدار باشی — فقط دستهی درستِ نزدیک به هزینهات را انتخاب کن.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلیها ثبتِ هزینه را به این دلیل کنار میگذارند که فکر میکنند باید
|
|
||||||
اصطلاحاتِ حسابداری بلد باشند. خبر خوب: نیستی، فقط چند دستهی ساده
|
|
||||||
کافی است.
|
|
||||||
</p>
|
|
||||||
<h2>دستههای آماده برای شروع</h2>
|
|
||||||
<p>
|
|
||||||
اجاره، حقوق، قبض/آبوبرق، خرید نقدی جنس، و «سایر هزینهها» — این پنج
|
|
||||||
دسته اکثرِ هزینههای روزمرهی یک مغازهی کوچک را پوشش میدهند. برای
|
|
||||||
خرید جنسی که نقد نپرداختی، دستهی
|
|
||||||
<a href="/articles/kharid-nasiye-dar-daftardari/">خرید نسیه</a> جداست.
|
|
||||||
</p>
|
|
||||||
<h2>عادتِ هر روز، بهتر از هر هفته</h2>
|
|
||||||
<p>
|
|
||||||
ثبتِ هر هزینه همان لحظه که اتفاق میافتد، چند ثانیه بیشتر طول
|
|
||||||
نمیکشد؛ ولی اگر یک هفته عقب بیفتی، خیلی از ریزهکاریها یادت
|
|
||||||
میرود. بعد از چند هفته میتوانی از
|
|
||||||
<a href="/articles/nemodar-daramad-hazine-tasmim/">نمودارِ درآمد و
|
|
||||||
هزینه</a> ببینی بیشترین هزینهات کجا میرود.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "nemodar-daramad-hazine-tasmim",
|
|
||||||
"title": "نمودار درآمد و هزینه چطور به تصمیمگیری کمک میکند؟",
|
|
||||||
"meta_description": "نمودار دفتر درآمد و هزینهی شاپ شیپ را برای یک بازهی زمانی دلخواه ببین و بفهم درآمدت روند صعودی دارد یا نه.",
|
|
||||||
"excerpt": "یک نگاهِ بصری به روندِ درآمد و هزینهات — نه فقط یک عدد خام.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
یک لیستِ بلند از اعداد، فهمیدنِ «آیا وضعیتم بهتر شده یا بدتر» را سخت
|
|
||||||
میکند. نمودار به همین درد میخورد — یک نگاه، و روند مشخص است.
|
|
||||||
</p>
|
|
||||||
<h2>بازهی زمانیِ دلخواه</h2>
|
|
||||||
<p>
|
|
||||||
از صفحهی دفترِ درآمد و هزینه، وارد نمودار شو و بازهای که میخواهی
|
|
||||||
(یک ماه، یک فصل، یک سال) را انتخاب کن. درآمد و هزینهی هر بازه کنار
|
|
||||||
هم رسم میشود — همان چیزی که بعدا میتوانی بهعنوان
|
|
||||||
<a href="/articles/csv-daftar-baraye-hesabdar/">خروجی CSV</a> هم
|
|
||||||
داشته باشی.
|
|
||||||
</p>
|
|
||||||
<h2>سوالی که نمودار جواب میدهد</h2>
|
|
||||||
<p>
|
|
||||||
«این ماه بهتر از ماه پیش بودم؟» یا «بعد از فعالکردنِ پیام گروهی،
|
|
||||||
فروشم بالا رفت؟» — اینها سوالاتی هستند که یک نمودارِ ساده بهتر از
|
|
||||||
یک لیستِ عددی جوابشان را میدهد.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "kharid-nasiye-dar-daftardari",
|
|
||||||
"title": "فرق خرید نسیه و خرید نقدی در دفترداری",
|
|
||||||
"meta_description": "خرید نسیه یک نوع بدهکاریِ تو به فروشنده است، نه یک هزینهی ساده — چطور شاپ شیپ این تفاوت را در دفتر نشان میدهد.",
|
|
||||||
"excerpt": "خرید نسیه یعنی تو بدهکارِ فروشندهای — دقیقا برعکسِ فروش قسطی.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
مقالهی <a href="/articles/forosh-ghesti-hesabdari/">فروش قسطی چطور
|
|
||||||
باید ثبت شود؟</a> نشان داد وقتی تو جنسی را قسطی میفروشی، مشتری به تو
|
|
||||||
بدهکار میشود. حالا برعکسش: وقتی تو از یک تامینکننده جنس نسیه
|
|
||||||
میخری، اینبار خودت بدهکاری.
|
|
||||||
</p>
|
|
||||||
<h2>چرا این را جدا از هزینهی نقدی ثبت کنیم</h2>
|
|
||||||
<p>
|
|
||||||
اگر خرید نسیه را همان لحظه «هزینه» حساب کنی، انگار همان لحظه پول
|
|
||||||
دادی — در حالی که ندادی. این یعنی موجودیِ نقدیِ حسابشدهات از
|
|
||||||
واقعیت بیشتر نشان داده میشود، دقیقا برعکسِ اشتباهی که در مقالهی
|
|
||||||
<a href="/articles/tafavot-daramad-naghdi-talab/">تفاوت درآمد نقدی و
|
|
||||||
طلب</a> گفتیم.
|
|
||||||
</p>
|
|
||||||
<h2>در شاپ شیپ</h2>
|
|
||||||
<p>
|
|
||||||
دستهی «خرید نسیه» را که انتخاب کنی، اپ مبلغِ کل و نام طرفحساب را
|
|
||||||
میپرسد و یک بدهیِ باز برایت میسازد — دقیقا همخانوادهی همان مکانیزمِ
|
|
||||||
طلب، فقط جهتش برعکس است.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "sabt-aghsat-bedehi",
|
|
||||||
"title": "چطور اقساط یک بدهی را ثبت و پیگیری کنم؟",
|
|
||||||
"meta_description": "برای هر بدهی/طلبِ باز در دفتر شاپ شیپ، هر قسطی که میگیری یا میدهی را جدا ثبت کن و ماندهحساب خودکار کم شود.",
|
|
||||||
"excerpt": "هر قسط را جدا ثبت کن — ماندهحساب خودش کم میشود، حسابوکتاب دستی لازم نیست.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
وقتی یک <a href="/articles/forosh-ghesti-hesabdari/">فروش قسطی</a>
|
|
||||||
یا <a href="/articles/kharid-nasiye-dar-daftardari/">خرید نسیه</a> در
|
|
||||||
دفترت ثبت میکنی، یک «ماندهحساب» برایش ساخته میشود. حالا سوال این
|
|
||||||
است: وقتی هر قسط میرسد، چطور ثبتش کنم؟
|
|
||||||
</p>
|
|
||||||
<h2>ثبت هر قسط، جدا</h2>
|
|
||||||
<p>
|
|
||||||
روی همان ثبتِ بدهی/طلب باز کن و یک قسطِ جدید اضافه کن — مبلغ، تاریخ،
|
|
||||||
و اگر خواستی یک شمارهی مرجع (مثلا شمارهی رسید). همین که ثبت شد،
|
|
||||||
ماندهحساب خودکار همان مقدار کم میشود.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی مانده صفر شود</h2>
|
|
||||||
<p>
|
|
||||||
وقتی مجموعِ اقساط به کل بدهی برسد، آن ثبت دیگر در لیستِ «بدهیهای
|
|
||||||
باز» نشان داده نمیشود — اما تاریخچهی کاملِ اقساط همیشه قابلمرور
|
|
||||||
میماند، برای وقتی که لازم شد به آن رجوع کنی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
# --- خوشهی فاکتور ---
|
|
||||||
{
|
|
||||||
"slug": "fatoore-chand-ghalam-kala",
|
|
||||||
"title": "فاکتور با تخفیف و مالیات چند نوع کالا چطور صادر میشود؟",
|
|
||||||
"meta_description": "هر قلمِ کالا در فاکتورِ شاپ شیپ میتواند نرخ تخفیف و مالیاتِ جدا از بقیه داشته باشد — جمعِ نهایی خودکار محاسبه میشود.",
|
|
||||||
"excerpt": "هر قلم کالا، تخفیف و مالیاتِ خودش را دارد — جمعِ نهایی را اپ حساب میکند.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
فرض کن یک فاکتور داری با سه قلم کالا: یکی تخفیف دارد، یکی مالیاتِ
|
|
||||||
متفاوت، و سومی هیچکدام. حسابکردنِ دستیِ اینها با ماشینحساب، هم
|
|
||||||
وقتگیر است هم پرخطا.
|
|
||||||
</p>
|
|
||||||
<h2>هر قلم، تنظیماتِ خودش</h2>
|
|
||||||
<p>
|
|
||||||
وقتی کالایی را (از همان لیستِ
|
|
||||||
<a href="/articles/dastebandi-kala-anbar/">انبارت</a>) به فاکتور
|
|
||||||
اضافه میکنی، میتوانی برایش یک درصد تخفیف و یک نرخ مالیات جدا از
|
|
||||||
بقیهی قلمها بگذاری.
|
|
||||||
</p>
|
|
||||||
<h2>جمعِ نهایی، خودکار</h2>
|
|
||||||
<p>
|
|
||||||
جمعِ کل، جمعِ مالیات، و مبلغِ تخفیفخوردهی نهایی خودش محاسبه
|
|
||||||
میشود — همان چیزی که در مقالهی
|
|
||||||
<a href="/articles/fatoore-rasmi-baraye-maghaze/">فاکتور رسمی برای
|
|
||||||
مغازه</a> هم به اتصالش به انبار اشاره شده بود.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "vaziat-fatoore-pardakht",
|
|
||||||
"title": "فرق فاکتور پرداختشده و درانتظار پرداخت",
|
|
||||||
"meta_description": "هر فاکتور در شاپ شیپ یک وضعیت روشن دارد — پرداختشده یا درانتظار — و همین وضعیت تعیین میکند دفترت و انبارت چه تغییری میکنند.",
|
|
||||||
"excerpt": "وضعیتِ فاکتور فقط یک برچسب نیست — تعیین میکند انبار و دفترت چه اتفاقی برایشان میافتد.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
یک فاکتور صرفا «صادر شدن» کافی نیست — باید بدانی پولش رسیده یا نه.
|
|
||||||
شاپ شیپ برای همین یک وضعیتِ روشن به هر فاکتور میدهد.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی پرداختشده میشود</h2>
|
|
||||||
<p>
|
|
||||||
همین که فاکتور را «پرداختشده» علامت بزنی، دو اتفاق خودکار میافتد:
|
|
||||||
موجودیِ کالاهای همان فاکتور از
|
|
||||||
<a href="/articles/dastebandi-kala-anbar/">انبار</a> کم میشود، و اگر
|
|
||||||
نوعِ پرداخت «قسطی/نسیه» بود، یک ثبتِ طلب هم در
|
|
||||||
<a href="/articles/tafavot-daramad-naghdi-talab/">دفترت</a> ساخته
|
|
||||||
میشود.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی هنوز درانتظار است</h2>
|
|
||||||
<p>
|
|
||||||
تا وضعیت پرداختشده نشود، هیچکدام از این دو اتفاق نمیافتد — یعنی
|
|
||||||
میتوانی فاکتور را از قبل آماده کنی، بدهی به مشتری نشان دهی، و بعدا
|
|
||||||
وقتی واقعا پول رسید، وضعیتش را عوض کنی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "chera-fatoore-rasmi-mohem-ast",
|
|
||||||
"title": "چرا باید فاکتور رسمی به مشتری بدهم حتی برای فروش کوچک؟",
|
|
||||||
"meta_description": "فاکتور رسمی فقط برای فروشهای بزرگ نیست — اعتماد، پیگیریِ اختلاف، و تاریخچهی خرید مشتری، سه دلیل برای صدورِ فاکتورِ هر فروش.",
|
|
||||||
"excerpt": "فاکتور فقط کاغذ نیست — سندِ اعتماد بین تو و مشتری است.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از مغازهدارها فقط برای فروشهای «بزرگ» فاکتور صادر میکنند —
|
|
||||||
برای بقیه، یک رسیدِ دستنویس یا هیچی. اما فاکتورِ رسمی برای هر فروش،
|
|
||||||
چند فایده دارد که با رسیدِ دستنویس بهدست نمیآید.
|
|
||||||
</p>
|
|
||||||
<h2>اعتماد و حرفهایبودن</h2>
|
|
||||||
<p>
|
|
||||||
یک فاکتورِ با لوگو، حس حرفهایتری به مشتری میدهد — دقیقا همان چیزی
|
|
||||||
که در <a href="/articles/fatoore-rasmi-baraye-maghaze/">فاکتور رسمی
|
|
||||||
برای مغازه</a> توضیح داده شد.
|
|
||||||
</p>
|
|
||||||
<h2>پیگیریِ اختلاف</h2>
|
|
||||||
<p>
|
|
||||||
اگر روزی مشتری بگوید «من این قیمت را ندیدم» یا «این کالا را نخریدم»،
|
|
||||||
یک فاکتورِ ثبتشده در اپ، فورا قابلنشاندادن است — بدون گشتن در
|
|
||||||
دفترچهای که شاید گم شده باشد.
|
|
||||||
</p>
|
|
||||||
<h2>تاریخچهی خرید مشتری</h2>
|
|
||||||
<p>
|
|
||||||
هر فاکتور خودکار به <a href="/articles/parvande-moshtari-chist/">پروندهی
|
|
||||||
همان مشتری</a> وصل میشود — یعنی بعد از چند ماه میتوانی ببینی هر
|
|
||||||
مشتری چقدر و چه چیزی خریده، بدون اینکه چیزی را دوباره تایپ کنی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
# --- خوشهی مشتریان ---
|
|
||||||
{
|
|
||||||
"slug": "parvande-moshtari-chist",
|
|
||||||
"title": "پروندهی مشتری چیست و چه چیزهایی در آن ثبت میشود؟",
|
|
||||||
"meta_description": "پروندهی هر مشتری در شاپ شیپ، تاریخچهی فاکتورها و ماندهحسابش را خودکار جمع میکند — بدون هیچ ثبتِ دستیِ اضافه.",
|
|
||||||
"excerpt": "تاریخچهی خرید و ماندهحساب هر مشتری، خودکار و در یک صفحه.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
وقتی چند ده یا چند صد مشتری داری، بهخاطر سپردنِ اینکه هرکدام چه
|
|
||||||
خریده و چقدر بدهکار است، عملا غیرممکن میشود. پروندهی مشتری همین
|
|
||||||
کار را برایت انجام میدهد.
|
|
||||||
</p>
|
|
||||||
<h2>چیزی که خودکار ساخته میشود</h2>
|
|
||||||
<p>
|
|
||||||
هر بار که برای یک مشتری <a href="/articles/vaziat-fatoore-pardakht/">فاکتور</a>
|
|
||||||
صادر میکنی یا در دفترت یک بدهی/طلب برایش ثبت میکنی، همهی اینها
|
|
||||||
خودکار در پروندهی همان مشتری جمع میشود — تو چیزی جدا وارد نمیکنی.
|
|
||||||
</p>
|
|
||||||
<h2>ماندهحساب، همانجا</h2>
|
|
||||||
<p>
|
|
||||||
اگر مشتریای <a href="/articles/modiriyat-bedehi-moshtarian/">بدهیِ
|
|
||||||
باز</a> داشته باشد، همانجا در پروندهاش میبینی — دیگر نیازی به گشتن
|
|
||||||
در دفتر یا فاکتورهای قدیمی نیست.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "faal-sazi-mojadad-moshtarian",
|
|
||||||
"title": "چطور مشتریان قدیمی را دوباره فعال کنم؟",
|
|
||||||
"meta_description": "با پیام گروهی و لیستِ مشتریانی که مدتی خرید نکردهاند، یک یادآوریِ ساده بفرست و مشتریِ فراموششده را برگردان.",
|
|
||||||
"excerpt": "یک پیامِ یادآوریِ ساده، بیشترِ وقتها کافی است — نه تخفیفِ بزرگ.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از کسبوکارهای کوچک انرژیشان را فقط صرفِ جذبِ مشتریِ تازه
|
|
||||||
میکنند و مشتریِ قدیمی که مدتی سراغش نرفتهاند را فراموش میکنند —
|
|
||||||
در حالی که برگرداندنِ یک مشتریِ قدیمی معمولا ارزانتر از جذبِ تازه است.
|
|
||||||
</p>
|
|
||||||
<h2>پیدا کردنِ مشتریانِ ساکت</h2>
|
|
||||||
<p>
|
|
||||||
از <a href="/articles/parvande-moshtari-chist/">پروندههای مشتری</a>،
|
|
||||||
کسانی که مدتی فاکتور یا خریدی نداشتهاند مشخصاند — همین لیست، شروعِ
|
|
||||||
خوبی برای یک یادآوری است.
|
|
||||||
</p>
|
|
||||||
<h2>یک قالبِ ساده کافی است</h2>
|
|
||||||
<p>
|
|
||||||
لازم نیست تخفیفِ بزرگ بدهی؛ گاهی یک
|
|
||||||
<a href="/articles/ghaleb-payam-chist/">قالبِ پیام</a> ساده مثل «دلمون
|
|
||||||
برات تنگ شده، سر بزن» با نامِ خودِ مشتری، از طریقِ
|
|
||||||
<a href="/articles/ersal-payam-be-goruh-khas/">ارسالِ گروهی</a> برایِ
|
|
||||||
همان لیست، کافی است.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
# --- خوشهی انبار ---
|
|
||||||
{
|
|
||||||
"slug": "dastebandi-kala-anbar",
|
|
||||||
"title": "دستهبندی کالا در انبار: از کجا شروع کنم؟",
|
|
||||||
"meta_description": "برای انبارِ چند ده یا چند صد قلمی، دستهبندی کالا در شاپ شیپ پیدا کردن و مدیریت را چند برابر سریعتر میکند.",
|
|
||||||
"excerpt": "دستههای ساده مثل «نوشیدنی» یا «لبنیات» — پیدا کردن یک کالا چند برابر سریعتر میشود.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
اگر انبارت فقط چند قلم کالا دارد، دستهبندی شاید لازم نباشد. اما
|
|
||||||
همین که به چند ده قلم برسی، یک لیستِ بلندِ بیترتیب پیدا کردنِ یک
|
|
||||||
کالا را کند میکند.
|
|
||||||
</p>
|
|
||||||
<h2>دستههای دلخواهت</h2>
|
|
||||||
<p>
|
|
||||||
دستهها را خودت تعریف میکنی — چیزی که برای کسبوکارت معنیدار باشد،
|
|
||||||
نه یک قالبِ ازقبلتعیینشده. یک سوپرمارکت شاید «نوشیدنی/لبنیات/خشکبار»
|
|
||||||
داشته باشد، یک لوازمیدکیفروشی چیزِ کاملا متفاوتی.
|
|
||||||
</p>
|
|
||||||
<h2>اثرش روی فاکتور هم هست</h2>
|
|
||||||
<p>
|
|
||||||
وقتی برای <a href="/articles/fatoore-chand-ghalam-kala/">فاکتور</a>
|
|
||||||
کالا اضافه میکنی، همین دستهبندی کمک میکند سریعتر کالای موردنظر را
|
|
||||||
پیدا کنی — بهجای اسکرولکردنِ یک لیستِ طولانی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "gheymat-kharid-vs-arzesh-anbar",
|
|
||||||
"title": "فرق قیمت خرید و ارزش کل انبار",
|
|
||||||
"meta_description": "قیمت خرید هر کالا را ثبت کن تا شاپ شیپ خودش ارزشِ کلِ سرمایهی خوابیده در انبارت را برایت حساب کند.",
|
|
||||||
"excerpt": "دو عدد ساده (قیمت خرید و موجودی)، یک جوابِ مهم: چقدر سرمایه در انبارت خوابیده؟",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
«الان چقدر پول در قالبِ جنس، روی طاقچههای مغازهام خوابیده؟» سوالی
|
|
||||||
است که خیلی از صاحبانِ مغازه دقیق جوابش را نمیدانند.
|
|
||||||
</p>
|
|
||||||
<h2>یک ضرب ساده، محاسبهشده برایت</h2>
|
|
||||||
<p>
|
|
||||||
برای هر کالا در <a href="/articles/dastebandi-kala-anbar/">انبار</a>،
|
|
||||||
قیمتِ خریدش را ثبت میکنی. اپ همین عدد را در موجودیِ فعلی ضرب میکند و
|
|
||||||
«ارزشِ کلِ انبار» را نشان میدهد — بدون اینکه خودت دستی جمع بزنی.
|
|
||||||
</p>
|
|
||||||
<h2>چرا این عدد مهم است</h2>
|
|
||||||
<p>
|
|
||||||
این عدد به تو میگوید چقدر سرمایه در جنسِ فروختهنشده گیر افتاده —
|
|
||||||
اطلاعاتی که برای تصمیمِ «الان جنسِ تازه بخرم یا نه» مستقیم به کار
|
|
||||||
میآید.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "hoshdar-kambood-mojoodi",
|
|
||||||
"title": "هشدار کمبود موجودی چطور کار میکند؟",
|
|
||||||
"meta_description": "برای هر کالا یک آستانهی «کم شد» تعریف کن — قبل از اینکه مشتری بیاید و نداشته باشی، خودت متوجه میشوی.",
|
|
||||||
"excerpt": "یک آستانه تعریف کن — قبل از تمامشدنِ واقعیِ جنس، خودت باخبر میشوی.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
یکی از بدترین حسها برای یک مغازهدار، این است که مشتری بیاید و جنسی
|
|
||||||
که فکر میکردی داری، تمام شده باشد. هشدارِ کمبود دقیقا برای جلوگیری
|
|
||||||
از همین لحظه است.
|
|
||||||
</p>
|
|
||||||
<h2>آستانهی دلخواهِ هر کالا</h2>
|
|
||||||
<p>
|
|
||||||
برای هر کالا در <a href="/articles/dastebandi-kala-anbar/">انبار</a>،
|
|
||||||
یک عدد بهعنوان «کم شد» تعریف میکنی — مثلا وقتی موجودی به ۵ رسید.
|
|
||||||
این عدد برای هر کالا میتواند فرق کند.
|
|
||||||
</p>
|
|
||||||
<h2>باخبر شدن زودتر</h2>
|
|
||||||
<p>
|
|
||||||
وقتی موجودی به آن آستانه برسد، اپ نشانش میدهد — قبل از اینکه واقعا
|
|
||||||
صفر شود. یعنی وقت کافی برای سفارشِ جنسِ تازه داری، بدون فشارِ
|
|
||||||
لحظهیآخری.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
# --- خوشهی پیام گروهی ---
|
|
||||||
{
|
|
||||||
"slug": "ghaleb-payam-chist",
|
|
||||||
"title": "قالب پیام چیست و چطور بسازمش؟",
|
|
||||||
"meta_description": "قالب پیام یک متنِ آماده با جایخالیِ نام/شماره است که یکبار میسازی و بارها برای مخاطبین مختلف استفاده میکنی.",
|
|
||||||
"excerpt": "یک متن، با جایخالی برای نام — یکبار بساز، بارها استفاده کن.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
اگر هر بار که میخواهی پیام گروهی بفرستی، از صفر متن تایپ کنی، هم
|
|
||||||
وقت تلف میشود هم احتمالِ فراموشکردنِ چیزی بالا میرود. قالبِ پیام
|
|
||||||
همین مشکل را حل میکند.
|
|
||||||
</p>
|
|
||||||
<h2>جایخالیِ نام و شماره</h2>
|
|
||||||
<p>
|
|
||||||
در متنِ قالب، میتوانی از جایخالیهایی مثل «نام» استفاده کنی — وقتی
|
|
||||||
برای هر مخاطب ارسال میشود، خودکار با اطلاعاتِ همان مخاطب پر میشود.
|
|
||||||
</p>
|
|
||||||
<h2>فیلدِ اجرا، برای مقادیرِ متغیر</h2>
|
|
||||||
<p>
|
|
||||||
اگر یک مقدار داری که از قبل معلوم نیست (مثل کد رهگیری)، قالب میتواند
|
|
||||||
یک <a href="/articles/field-sefareshi-dar-template/">فیلدِ اجرا</a>
|
|
||||||
هم داشته باشد — همان لحظهی ارسال از تو پرسیده میشود.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "ersal-payam-be-goruh-khas",
|
|
||||||
"title": "ارسال پیام به گروه خاصی از مشتریان",
|
|
||||||
"meta_description": "بهجای فرستادن یک پیام به همه، فقط برای بدهکارها، مشتریانِ ساکت، یا هر گروهِ دلخواه دیگری در شاپ شیپ پیام بفرست.",
|
|
||||||
"excerpt": "همه یک پیام یکسان نمیخواهند — پیامِ درست را برای گروهِ درست بفرست.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
فرستادنِ یک پیامِ یکسان به همهی مخاطبین، همیشه بهترین راه نیست —
|
|
||||||
پیامِ یادآوریِ بدهی برای بدهکارها معنی دارد، نه برای همه.
|
|
||||||
</p>
|
|
||||||
<h2>انتخاب گیرندهها</h2>
|
|
||||||
<p>
|
|
||||||
قبل از ارسال، مخاطبینی که پیام برایشان میرود را خودت انتخاب میکنی —
|
|
||||||
مثلا فقط کسانی که <a href="/articles/modiriyat-bedehi-moshtarian/">بدهیِ
|
|
||||||
باز</a> دارند، یا فقط
|
|
||||||
<a href="/articles/faal-sazi-mojadad-moshtarian/">مشتریانِ مدتی
|
|
||||||
ساکت</a>.
|
|
||||||
</p>
|
|
||||||
<h2>یک قالب، گیرندههای متفاوت</h2>
|
|
||||||
<p>
|
|
||||||
همان <a href="/articles/ghaleb-payam-chist/">قالبِ پیام</a> که یکبار
|
|
||||||
ساختی، برای هر گروهی که بعدا انتخاب کنی دوباره قابلاستفاده است —
|
|
||||||
فقط لیستِ گیرندهها را عوض میکنی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "vaziat-tahvil-payamak",
|
|
||||||
"title": "چک کردن وضعیت تحویل پیامکهای ارسالی",
|
|
||||||
"meta_description": "بعد از ارسال گروهی، بفهم کدام پیامک واقعا تحویل داده شده، کدام در صف مانده و کدام ناموفق بوده — همه در یک لیست.",
|
|
||||||
"excerpt": "بعد از ارسال، بفهم واقعا چند نفر پیامک را گرفتند.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
«آیا واقعا رسید؟» سوالی است که بعد از هر ارسالِ گروهی میآید — بهخصوص
|
|
||||||
برای <a href="/articles/ghaleb-payam-chist/">قالبهای پیام</a> مهم
|
|
||||||
مثل یادآوریِ پرداخت.
|
|
||||||
</p>
|
|
||||||
<h2>سه وضعیتِ ممکن</h2>
|
|
||||||
<p>
|
|
||||||
هر پیامکِ ارسالی یکی از این سه وضعیت را دارد: تحویلشده، درصف
|
|
||||||
(هنوز پردازش میشود)، یا ناموفق. لیستِ کاملِ همهی ارسالها، با
|
|
||||||
وضعیتِ هرکدام، در دسترس است.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی چیزی ناموفق بود</h2>
|
|
||||||
<p>
|
|
||||||
اگر پیامکی به دلیلی (مثلا شمارهی اشتباه) ناموفق بود، میدانی دقیقا
|
|
||||||
چه کسی پیام را نگرفته — میتوانی جداگانه دوباره برایش بفرستی، بهجای
|
|
||||||
حدسزدن.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "kodam-kanal-baraye-kodam-kasbokar",
|
|
||||||
"title": "کدام کانال برای کدام کسبوکار بهتر است؟",
|
|
||||||
"meta_description": "فروشگاه، آنلاینشاپ، یا کسبوکار خدماتی — هرکدام معمولا با یک ترکیبِ متفاوت از پیامک/واتساپ/تلگرام بهتر جواب میگیرند.",
|
|
||||||
"excerpt": "نوعِ کسبوکارت مشخص میکند کدام کانال بیشترین اثر را دارد.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
مقالهی <a href="/articles/sms-ya-whatsapp-business/">پیامک یا
|
|
||||||
واتساپبیزینس؟</a> تفاوتِ فنیِ کانالها را توضیح داد. اینجا میخواهیم
|
|
||||||
ببینیم کدام کسبوکار معمولا کدام ترکیب را بیشتر لازم دارد.
|
|
||||||
</p>
|
|
||||||
<h2>فروشگاههای محلی</h2>
|
|
||||||
<p>
|
|
||||||
برای اعلامِ موجودیِ روزانه یا تخفیف، پیامک قابلاطمینانترین است —
|
|
||||||
همه به آن دسترسی دارند، حتی بدون اینترنت.
|
|
||||||
</p>
|
|
||||||
<h2>آنلاینشاپهای کوچک</h2>
|
|
||||||
<p>
|
|
||||||
اطلاعرسانیِ <a href="/articles/tartib-marahel-sefaresh/">مراحلِ
|
|
||||||
سفارش</a> معمولا با پیامک بهتر جواب میدهد، چون مشتری منتظرِ خبرِ
|
|
||||||
«ارسال شد» است و نمیخواهد وابسته به بازکردنِ یک اپ خاص باشد.
|
|
||||||
</p>
|
|
||||||
<h2>کسبوکارهای خدماتی</h2>
|
|
||||||
<p>
|
|
||||||
برای ارتباطِ دوطرفهتر (سوالوجواب با مشتری)، واتساپ یا تلگرام معمولا
|
|
||||||
راحتتر است — بهشرطی که مسیرِ رسمیِ هرکدام رعایت شود.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
# --- خوشهی پیگیری سفارش ---
|
|
||||||
{
|
|
||||||
"slug": "tartib-marahel-sefaresh",
|
|
||||||
"title": "جابهجا کردن ترتیب مراحل سفارش با کشیدن و رها کردن",
|
|
||||||
"meta_description": "مراحلِ پیگیری سفارش در شاپ شیپ ثابت نیستند — با کشیدن و رها کردن، ترتیبشان را دقیقا مطابق جریان کاریِ واقعیت تنظیم کن.",
|
|
||||||
"excerpt": "ترتیبِ مراحل، دقیقا مطابقِ جریانِ کاریِ واقعیِ خودت — با یک کشیدن و رها کردن.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
همانطور که در مقالهی
|
|
||||||
<a href="/articles/peygiri-sefaresh-online-shop-etemad/">پیگیری سفارش
|
|
||||||
آنلاینشاپ</a> گفتیم، مراحلِ کار را خودت تعریف میکنی. اما گاهی بعد از
|
|
||||||
مدتی میفهمی ترتیبِ اولیه دقیق نبوده.
|
|
||||||
</p>
|
|
||||||
<h2>کشیدن و رها کردن</h2>
|
|
||||||
<p>
|
|
||||||
از صفحهی مراحلِ سفارش، هر مرحله را میتوانی بگیری و به جای درست
|
|
||||||
بکشی — بدون نیاز به حذف و ساختِ دوباره. مثلا اگر «بستهبندی» را قبل
|
|
||||||
از «آمادهسازی» گذاشته بودی، فقط جابهجایش کن.
|
|
||||||
</p>
|
|
||||||
<h2>تغییر ترتیب، تاثیری روی سفارشهای قبلی ندارد</h2>
|
|
||||||
<p>
|
|
||||||
سفارشهایی که از قبل در یک مرحله بودند، همانجا میمانند — تغییرِ
|
|
||||||
ترتیب فقط روی مسیرِ سفارشهای بعدی اثر میگذارد.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "payam-mokhtalef-har-marhale",
|
|
||||||
"title": "چطور برای هر مرحلهی سفارش پیامک متفاوت بفرستم؟",
|
|
||||||
"meta_description": "هر مرحلهی پیگیری سفارش در شاپ شیپ به یک قالبِ پیام جداگانه وصل میشود — «آماده شد» یک متن، «ارسال شد» متنی دیگر.",
|
|
||||||
"excerpt": "هر مرحله، قالبِ پیامِ خودش را دارد — نه یک متنِ یکسان برای همهجا.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
یک پیامِ یکسان برای «سفارشت آماده شد» و «سفارشت تحویل داده شد»
|
|
||||||
منطقی نیست — هرکدام باید خبرِ متفاوتی بدهند.
|
|
||||||
</p>
|
|
||||||
<h2>هر مرحله، یک قالب</h2>
|
|
||||||
<p>
|
|
||||||
وقتی <a href="/articles/tartib-marahel-sefaresh/">یک مرحله</a>
|
|
||||||
میسازی، همانجا یک <a href="/articles/ghaleb-payam-chist/">قالبِ
|
|
||||||
پیام</a> برایش انتخاب میکنی — میتواند برای هر مرحله کاملا متفاوت
|
|
||||||
باشد.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی مرحله فیلدِ اجرا دارد</h2>
|
|
||||||
<p>
|
|
||||||
برای مرحلهای مثل «ارسال شد» که یک کدِ رهگیری لازم دارد، قالبِ همان
|
|
||||||
مرحله میتواند <a href="/articles/field-sefareshi-dar-template/">فیلدِ
|
|
||||||
اجرا</a> داشته باشد — همان لحظه از تو پرسیده و در پیامک جای میشود.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
# --- خوشهی راهنمای عمومی/آشناسازی ---
|
|
||||||
{
|
|
||||||
"slug": "behtarin-estefade-az-hafte-raygan",
|
|
||||||
"title": "یک هفتهی رایگان شاپ شیپ را چطور بهترین استفاده را ببرم؟",
|
|
||||||
"meta_description": "راهنمای هفتروزه: چه کارهایی در هفتهی رایگانِ شاپ شیپ انجام بده تا قبل از هر تصمیمِ پرداختی، ارزشِ واقعیِ اپ را ببینی.",
|
|
||||||
"excerpt": "هفت روز، هفت قدمِ کوچک — تا قبل از هر تصمیم پرداختی، ارزشِ واقعی را ببینی.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
یک هفته زمانِ کمی است، ولی برای فهمیدنِ اینکه شاپ شیپ واقعا بهدردت
|
|
||||||
میخورد یا نه، کافی است — اگر درست ازش استفاده کنی.
|
|
||||||
</p>
|
|
||||||
<h2>روزهای اول: دادههای واقعی</h2>
|
|
||||||
<p>
|
|
||||||
بهجای امتحانکردنِ با دادهی الکی، از همین روزِ اول
|
|
||||||
<a href="/articles/dastebandi-kala-anbar/">چند کالای واقعیِ انبارت</a>
|
|
||||||
و <a href="/articles/merge-mokhatebin-ganoshi/">چند مخاطبِ
|
|
||||||
واقعی</a> را وارد کن.
|
|
||||||
</p>
|
|
||||||
<h2>وسط هفته: یک فاکتور و یک پیام واقعی</h2>
|
|
||||||
<p>
|
|
||||||
یک <a href="/articles/chera-fatoore-rasmi-mohem-ast/">فاکتورِ واقعی</a>
|
|
||||||
برای یک فروشِ واقعی بزن، و یک قالبِ پیام بساز و برایِ چند مشتریِ
|
|
||||||
واقعی بفرست.
|
|
||||||
</p>
|
|
||||||
<h2>آخر هفته: مرور</h2>
|
|
||||||
<p>
|
|
||||||
به دفترِ درآمد و هزینهات نگاه کن — همان چیزی که در این هفته ثبت شده،
|
|
||||||
تصویرِ کوچکی از این است که یک ماهِ کامل با شاپ شیپ چه شکلی خواهد بود.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "amniat-etelaat-kasbokar",
|
|
||||||
"title": "امنیت اطلاعات کسبوکارت در شاپ شیپ",
|
|
||||||
"meta_description": "قفل اثرانگشت، دیتای اختصاصی هر کاربر، و اینکه چرا اطلاعاتِ مالی و مشتریانِ تو فقط مالِ خودت است.",
|
|
||||||
"excerpt": "قفل اثرانگشت روی اطلاعاتِ حساس، و دیتایی که فقط مالِ خودت است.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
اطلاعاتِ مالی و لیستِ مشتریانِ یک کسبوکار، از حساسترین چیزهایی است
|
|
||||||
که روی گوشیِ کسی میتواند باشد — به همین دلیل امنیت اینجا جدی گرفته
|
|
||||||
شده.
|
|
||||||
</p>
|
|
||||||
<h2>قفل اثرانگشت</h2>
|
|
||||||
<p>
|
|
||||||
میتوانی ورود به بخشهای حساس (مثل
|
|
||||||
<a href="/articles/tafavot-daramad-naghdi-talab/">دفتر درآمد و
|
|
||||||
هزینه</a>) را پشتِ قفلِ اثرانگشتِ گوشیات بگذاری — حتی اگر کسی
|
|
||||||
گوشیات را باز کند، به این بخشها بدون اثرانگشتِ خودت دسترسی ندارد.
|
|
||||||
</p>
|
|
||||||
<h2>دیتای تو، مالِ خودت</h2>
|
|
||||||
<p>
|
|
||||||
اطلاعاتِ کسبوکارِ هر کاربر کاملا از بقیه جدا است — چیزی بینِ
|
|
||||||
حسابهای مختلف به اشتراک گذاشته نمیشود.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "taghvim-shamsi-dar-narmafzar",
|
|
||||||
"title": "تقویم شمسی در نرمافزارهای کسبوکار چرا مهم است؟",
|
|
||||||
"meta_description": "تاریخِ میلادی در نرمافزارهای مالی، محاسبهی سررسیدها و مرورِ گزارشها را برای کاربر فارسیزبان سختتر میکند — شاپ شیپ همهجا شمسی است.",
|
|
||||||
"excerpt": "وقتی سررسیدِ قسط یا تاریخِ فاکتور را میبینی، باید همان لحظه معنیاش را بفهمی — نه تبدیل کنی.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از نرمافزارهای مالی و حسابداری با تاریخِ میلادی کار میکنند —
|
|
||||||
و کاربرِ فارسیزبان باید در ذهنش تبدیل کند. این تبدیلِ ذهنی، هرچقدر
|
|
||||||
هم سریع، یک لایهی اضافهی خستگی است.
|
|
||||||
</p>
|
|
||||||
<h2>همهجا شمسی</h2>
|
|
||||||
<p>
|
|
||||||
از تاریخِ <a href="/articles/vaziat-fatoore-pardakht/">فاکتور</a> تا
|
|
||||||
سررسیدِ <a href="/articles/sabt-aghsat-bedehi/">اقساط</a> تا
|
|
||||||
<a href="/articles/nemodar-daramad-hazine-tasmim/">نمودارِ دفتر</a>،
|
|
||||||
همهجا با تقویمِ شمسی نمایش داده میشود.
|
|
||||||
</p>
|
|
||||||
<h2>یعنی چه در عمل</h2>
|
|
||||||
<p>
|
|
||||||
وقتی میبینی «سررسید ۱۵ مهر»، همان لحظه معنایش را میفهمی — بدون
|
|
||||||
اینکه مجبور باشی محاسبه کنی امروز چندمِ میلادی است و چند روز مانده.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "jam-chist-chetor-kar-mikonad",
|
|
||||||
"title": "جم چیست و چطور برای فعالسازی ابزارها استفاده میشود؟",
|
|
||||||
"meta_description": "شاپ شیپ بهجای اشتراکِ همهیاهیچ، هر بخش را جدا با «جم» فعال میکند — جم چطور خریداری و خرج میشود.",
|
|
||||||
"excerpt": "بهجای اشتراکِ همهیاهیچ، هر بخش را جدا، فقط با جم، فعال میکنی.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از نرمافزارها یک اشتراکِ ثابت میفروشند که شاملِ همهی
|
|
||||||
امکانات است — حتی آنهایی که لازم نداری. شاپ شیپ این مدل را عوض
|
|
||||||
کرده.
|
|
||||||
</p>
|
|
||||||
<h2>جم چیست</h2>
|
|
||||||
<p>
|
|
||||||
جم واحدِ داخلیِ شاپ شیپ برای فعالسازیِ بخشهاست — فاکتور، پیام
|
|
||||||
گروهی، دفتر، انبار، هرکدام قیمتِ جداگانهای به جم دارند.
|
|
||||||
</p>
|
|
||||||
<h2>از کجا جم بگیرم</h2>
|
|
||||||
<p>
|
|
||||||
جم را از فروشگاهِ داخلِ اپ میخری، یا از راهِ
|
|
||||||
<a href="/articles/davat-doostan-refferal/">دعوتِ دوستان</a> بهعنوانِ
|
|
||||||
پاداش دریافت میکنی. هر بخش را فقط برایِ مدتی که واقعا لازمش داری
|
|
||||||
فعال میکنی — تمدیدِ اجباری وجود ندارد.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "davat-doostan-refferal",
|
|
||||||
"title": "دعوت دوستان و پاداش معرفی در شاپ شیپ",
|
|
||||||
"meta_description": "کد معرفِ خودت را با یک صاحب کسبوکار دیگر به اشتراک بگذار — هر دو نفر جم پاداش میگیرید که میشود برای فعالسازیِ ابزارها خرج کرد.",
|
|
||||||
"excerpt": "کدِ معرفت را با یک صاحب کسبوکارِ دیگر به اشتراک بگذار — هر دو برنده میشوید.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
اگر یک صاحب کسبوکارِ دیگر را میشناسی که ممکن است شاپ شیپ به دردش
|
|
||||||
بخورد، معرفیِ او هم به نفعِ او هم به نفعِ خودت است.
|
|
||||||
</p>
|
|
||||||
<h2>چطور کار میکند</h2>
|
|
||||||
<p>
|
|
||||||
کدِ معرفِ خودت را (از پروفایلِ خودت در اپ) با او به اشتراک بگذار.
|
|
||||||
وقتی او با همان کد ثبتنام و فعالیتِ واقعی را شروع کند، هر دوی شما
|
|
||||||
<a href="/articles/jam-chist-chetor-kar-mikonad/">جمِ پاداش</a>
|
|
||||||
میگیرید.
|
|
||||||
</p>
|
|
||||||
<h2>خرجِ جمِ پاداش</h2>
|
|
||||||
<p>
|
|
||||||
جمی که از این راه میگیری، دقیقا مثلِ جمِ خریداریشده است — میتوانی
|
|
||||||
با آن هر بخشی از اپ (فاکتور، پیام گروهی، دفتر) را برای مدتی فعال
|
|
||||||
کنی، بدون پرداختِ پول.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "kasbokar-khord-chist",
|
|
||||||
"title": "کسبوکار خرد چیست و چه نرمافزارهایی واقعا به دردش میخورد؟",
|
|
||||||
"meta_description": "یک مغازهی یکنفره یا یک آنلاینشاپِ کوچک، نیازِ نرمافزاریِ متفاوتی از یک شرکتِ بزرگ دارد — چرا سادگی مهمتر از قابلیتهای زیاد است.",
|
|
||||||
"excerpt": "کسبوکارِ یک یا دونفره، نیازِ نرمافزاریِ متفاوتی از یک شرکتِ بزرگ دارد.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
«کسبوکار خرد» یعنی معمولا یک یا چند نفر، بدونِ بخشِ IT جدا، بدونِ
|
|
||||||
وقتِ زیاد برای یادگیریِ نرمافزارِ پیچیده. نیازهایش با یک شرکتِ
|
|
||||||
بزرگ خیلی فرق دارد.
|
|
||||||
</p>
|
|
||||||
<h2>چرا نرمافزارِ شرکتهای بزرگ جواب نمیدهد</h2>
|
|
||||||
<p>
|
|
||||||
نرمافزارهایی که برای تیمِ حسابداریِ چندنفره طراحی شدهاند، پر از
|
|
||||||
تنظیماتی هستند که یک مغازهدارِ تنها هیچوقت لازمشان ندارد — و همین
|
|
||||||
پیچیدگی باعث میشود اصلا استفاده نشود.
|
|
||||||
</p>
|
|
||||||
<h2>چه چیزی واقعا لازم است</h2>
|
|
||||||
<p>
|
|
||||||
چیزهایی مثل <a href="/articles/chera-fatoore-rasmi-mohem-ast/">فاکتورِ
|
|
||||||
ساده</a>، <a href="/articles/sabt-hazine-rozane-maghaze/">ثبتِ
|
|
||||||
هزینهی روزمره</a>، و <a href="/articles/ghaleb-payam-chist/">پیامِ
|
|
||||||
گروهیِ ساده</a> — نه گزارشهای پیچیدهای که هیچوقت باز نمیشوند.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "checklist-rahandazi-maghaze",
|
|
||||||
"title": "چکلیست راهاندازی یک مغازهی کوچک با موبایل",
|
|
||||||
"meta_description": "از ثبتِ اولین کالا تا صدور اولین فاکتور — چکلیستِ قدمبهقدمِ راهاندازیِ مدیریتِ یک مغازهی کوچک با شاپ شیپ.",
|
|
||||||
"excerpt": "از اولین کالا تا اولین فاکتور — یک چکلیستِ قدمبهقدم.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
شروعِ مدیریتِ یک مغازه با یک اپِ جدید، اگر قدمبهقدم نباشد، گیجکننده
|
|
||||||
میشود. این چکلیست، ترتیبِ پیشنهادی است.
|
|
||||||
</p>
|
|
||||||
<h2>قدم اول: انبار</h2>
|
|
||||||
<p>
|
|
||||||
چند کالای پرتکرارت را با
|
|
||||||
<a href="/articles/dastebandi-kala-anbar/">دستهبندی</a> و قیمتِ خرید
|
|
||||||
وارد کن — نیازی نیست همه را یکجا وارد کنی، میتوانی کمکم اضافه کنی.
|
|
||||||
</p>
|
|
||||||
<h2>قدم دوم: چند مشتریِ ثابت</h2>
|
|
||||||
<p>
|
|
||||||
اگر مشتریانِ ثابت داری، آنها را با
|
|
||||||
<a href="/articles/merge-mokhatebin-ganoshi/">واردکردن از
|
|
||||||
مخاطبینِ گوشی</a> اضافه کن.
|
|
||||||
</p>
|
|
||||||
<h2>قدم سوم: اولین فاکتور</h2>
|
|
||||||
<p>
|
|
||||||
برای یک فروشِ واقعی،
|
|
||||||
<a href="/articles/fatoore-chand-ghalam-kala/">اولین فاکتورت</a> را
|
|
||||||
بزن — از همینجا میبینی چطور فاکتور، انبار، و دفترت به هم وصل
|
|
||||||
میشوند.
|
|
||||||
</p>
|
|
||||||
<h2>قدم چهارم: اولین پیامِ گروهی</h2>
|
|
||||||
<p>
|
|
||||||
یک <a href="/articles/ghaleb-payam-chist/">قالبِ پیام</a> ساده بساز و
|
|
||||||
برای همان چند مشتری بفرست — تمام.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "shopship-vs-excel",
|
|
||||||
"title": "تفاوت شاپ شیپ با اکسل برای مدیریت کسبوکار",
|
|
||||||
"meta_description": "اکسل رایگان است ولی محاسبه، یادآوری، و اتصالِ خودکارِ بین فاکتور/انبار/دفتر را ندارد — مقایسهای صادق بین دو رویکرد.",
|
|
||||||
"excerpt": "اکسل رایگان است، ولی چیزهایی را خودکار نمیکند که هر روز وقتت را میگیرد.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
اکسل ابزارِ قدرتمندی است و خیلی از کسبوکارهای کوچک سالها با آن کار
|
|
||||||
کردهاند. سوال این نیست که اکسل بد است — سوال این است که چه چیزی
|
|
||||||
اکسل بهطورِ پیشفرض برایت انجام نمیدهد.
|
|
||||||
</p>
|
|
||||||
<h2>اتصالِ خودکار بین بخشها</h2>
|
|
||||||
<p>
|
|
||||||
در اکسل، وقتی فاکتور میزنی باید جداگانه هم
|
|
||||||
<a href="/articles/hoshdar-kambood-mojoodi/">موجودیِ انبار</a> را کم
|
|
||||||
کنی، هم <a href="/articles/tafavot-daramad-naghdi-talab/">دفترت</a>
|
|
||||||
را بهروز کنی — سه کار برای یک فروش. در شاپ شیپ اینها خودکار به هم
|
|
||||||
وصلاند.
|
|
||||||
</p>
|
|
||||||
<h2>یادآوری و پیام، بدونِ ابزارِ جدا</h2>
|
|
||||||
<p>
|
|
||||||
اکسل نمیتواند برایت پیامک بفرستد. برای
|
|
||||||
<a href="/articles/faal-sazi-mojadad-moshtarian/">یادآوریِ مشتری</a>
|
|
||||||
یا هشدارِ کمبودِ موجودی، باز باید ابزارِ جدا داشته باشی.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی اکسل هنوز کافی است</h2>
|
|
||||||
<p>
|
|
||||||
اگر فقط چند فروش در هفته داری و هیچوقت نیاز به پیام گروهی یا
|
|
||||||
پیگیریِ بدهی نداری، شاید اکسل هنوز جواب بدهد. اما همین که کسبوکارت
|
|
||||||
رشد کند، این محاسبات و یادآوریهای دستی، وقتِ زیادی میگیرند.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "yek-app-bejaye-chand-narmafzar",
|
|
||||||
"title": "چطور از یک اپ واحد بهجای چند نرمافزار جدا استفاده کنم؟",
|
|
||||||
"meta_description": "بهجای یک اپ برای فاکتور، یک اپ برای پیامک، و یک دفترچه برای حسابوکتاب، همه را در شاپ شیپ یکجا داشته باش.",
|
|
||||||
"excerpt": "فاکتور، پیام، انبار، دفتر — همه یکجا، و بههم وصل.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از کسبوکارهای کوچک بدونِ اینکه تصمیمِ آگاهانهای گرفته باشند،
|
|
||||||
سرِ آخر با پنج ابزارِ جدا کار میکنند: یک اپ برای فاکتور، یکی برای
|
|
||||||
پیامک، یک دفترچه برای حساب، و اکسلِ انبار.
|
|
||||||
</p>
|
|
||||||
<h2>مشکلِ اصلیِ ابزارهای جدا</h2>
|
|
||||||
<p>
|
|
||||||
هیچکدام از این ابزارها با هم حرف نمیزنند — یعنی هر فروش را باید سه
|
|
||||||
یا چهار بار جدا ثبت کنی. همانطور که در مقالهی
|
|
||||||
<a href="/articles/shopship-vs-excel/">تفاوتِ شاپ شیپ با اکسل</a>
|
|
||||||
گفتیم، همین دوبارهکاریهاست که وقتِ زیادی میگیرد.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی همهچیز یکجاست</h2>
|
|
||||||
<p>
|
|
||||||
یک <a href="/articles/vaziat-fatoore-pardakht/">فاکتور</a> که
|
|
||||||
پرداخت میشود، خودش انبار و دفتر را بهروز میکند. یک
|
|
||||||
<a href="/articles/parvande-moshtari-chist/">مشتری</a> که فاکتور
|
|
||||||
میگیرد، خودش پروندهاش ساخته میشود. کارِ کمتر، خطای کمتر.
|
|
||||||
</p>
|
|
||||||
<h2>شروعِ آسان</h2>
|
|
||||||
<p>
|
|
||||||
نیازی نیست همهچیز را یکجا شروع کنی — میتوانی از
|
|
||||||
<a href="/articles/checklist-rahandazi-maghaze/">چکلیستِ
|
|
||||||
راهاندازی</a> شروع کنی و کمکم بخشهای دیگر را اضافه کنی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
]
|
|
||||||
@@ -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": """
|
|
||||||
<p>
|
|
||||||
خیلی از کسبوکارهای کوچک اولین باری که «پیام گروهی» را امتحان میکنند،
|
|
||||||
یا از یک نرمافزار مشکوک استفاده میکنند که چند شماره را همزمان روی
|
|
||||||
چند گوشی میچرخاند، یا دستی و طاقتفرسا یکییکی کپیپیست میکنند. هر دو
|
|
||||||
راه یک مشکل مشترک دارند: یکی امن نیست، یکی زمانبر. شاپ شیپ یک راه سوم
|
|
||||||
را انتخاب کرده — دقیقا همان مسیری که خودِ پلتفرمها رسمی اعلام کردهاند.
|
|
||||||
</p>
|
|
||||||
<h2>چرا اکانتها مسدود میشوند</h2>
|
|
||||||
<p>
|
|
||||||
واتساپ، تلگرام، بله و ایتا همه یک سیستم ضدهرزنامه دارند که رفتار
|
|
||||||
غیرطبیعی (ارسال حجم بالا از یک شمارهی معمولی، چرخاندن چند دستگاه/سشن،
|
|
||||||
شبیهسازی رفتار انسانی برای فرار از تشخیص) را شناسایی و مسدود میکند.
|
|
||||||
این محدودیت تصادفی نیست — دقیقا همان مکانیزمی است که جلوی اسپم واقعی
|
|
||||||
را میگیرد، و هیچ «ترفند» پایداری برای دور زدنش وجود ندارد.
|
|
||||||
</p>
|
|
||||||
<h2>راه پایدار: مسیر رسمی هر پلتفرم</h2>
|
|
||||||
<p>
|
|
||||||
<strong>پیامک</strong> از طریق درگاههای بالک اپراتور، <strong>واتساپ</strong>
|
|
||||||
از طریق WhatsApp Business Platform (با Template ازپیشتاییدشده)، و
|
|
||||||
<strong>تلگرام/بله/ایتا</strong> از طریق Bot API — با یک محدودیت مهم:
|
|
||||||
بات فقط میتواند به کسانی پیام بدهد که خودشان قبلا چت را با بات شروع
|
|
||||||
کردهاند (opt-in). جزئیاتِ انتخاب بین این کانالها در مقالهی
|
|
||||||
<a href="/articles/sms-ya-whatsapp-business/">پیامک یا واتساپبیزینس؟</a>
|
|
||||||
آمده است.
|
|
||||||
</p>
|
|
||||||
<h2>شاپ شیپ چطور این را حل میکند</h2>
|
|
||||||
<p>
|
|
||||||
برای کانالهایی که opt-in لازم دارند و مخاطب هنوز چت را شروع نکرده،
|
|
||||||
شاپ شیپ بهجای اتوماسیونِ مخفی و پرخطر، یک صفِ ارسال دستیِ یکلمسی نشان
|
|
||||||
میدهد: قالب پیام را از قبل آماده کردهای، اپ مقصد را با متنِ آماده باز
|
|
||||||
میکند، خودت فقط دکمهی ارسال را میزنی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "field-sefareshi-dar-template",
|
|
||||||
"title": "فیلد اجرا در قالب پیام یعنی چه؟",
|
|
||||||
"meta_description": "قالب پیام در شاپ شیپ میتواند یک «فیلد اجرا» داشته باشد — مقداری که هر بار لحظهی ارسال پرسیده میشود، نه از قبل ثبت شده.",
|
|
||||||
"excerpt": "یک فیلد در قالب پیام که مقدارش از قبل معلوم نیست و هر بار لحظهی ارسال پرسیده میشود — مثلا کد رهگیری پستی.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
قالب پیام معمولی یک متن ثابت با چند جایخالیِ ازقبلمعلوم است، مثلا
|
|
||||||
«سلام {نام} عزیز». اما بعضی وقتها مقداری که باید در پیام بیاید اصلا
|
|
||||||
از قبل معلوم نیست — مثلا کد رهگیری پستیِ یک مرسوله که همان لحظهی
|
|
||||||
تحویل به پست تولید میشود. اینجاست که «فیلد اجرا» به کمک میآید.
|
|
||||||
</p>
|
|
||||||
<h2>فرقش با جایخالیهای معمولی چیست</h2>
|
|
||||||
<p>
|
|
||||||
جایخالیهای معمولیِ قالب (مثل نام یا شماره) از اطلاعاتِ همان مخاطب پر
|
|
||||||
میشوند — از قبل در اپ ثبت شدهاند. فیلد اجرا برعکس است: هیچ مقدار
|
|
||||||
ازپیشذخیرهشدهای ندارد؛ دقیقا همان لحظه که پیام دارد ارسال میشود،
|
|
||||||
اپ یکبار از تو میپرسدش.
|
|
||||||
</p>
|
|
||||||
<h2>کاربرد اول: پیگیری سفارش</h2>
|
|
||||||
<p>
|
|
||||||
وقتی برای یک مرحله از
|
|
||||||
<a href="/articles/peygiri-sefaresh-online-shop-etemad/">پیگیری سفارش</a>
|
|
||||||
قالبی انتخاب میکنی که فیلد اجرا دارد، همین که سفارشی به آن مرحله
|
|
||||||
میرسد، اپ همان لحظه مقدار را میپرسد — مثلا کد رهگیری — و بعد پیامک
|
|
||||||
را با همان مقدار برای مشتری میفرستد.
|
|
||||||
</p>
|
|
||||||
<h2>کاربرد دوم: ارسال گروهی</h2>
|
|
||||||
<p>
|
|
||||||
در ارسال گروهی هم اگر قالب انتخابی فیلد اجرا داشته باشد، اپ مخاطبین
|
|
||||||
را یکییکی نشان میدهد و مقدار را برای هرکدام میپرسد — و برای
|
|
||||||
راحتی، مقداری که برای نفرِ قبلی وارد کردی بهعنوان پیشفرضِ نفرِ بعدی
|
|
||||||
هم پیشنهاد میشود.
|
|
||||||
</p>
|
|
||||||
<h2>هر قالب فقط یک فیلد اجرا دارد</h2>
|
|
||||||
<p>
|
|
||||||
ساده نگه داشتنش عمدی است: هر قالب پیام حداکثر یک فیلد اجرا دارد، با
|
|
||||||
یک برچسبِ دلخواه (مثلا «کد رهگیری»). اگر قالبی این فیلد را نداشته
|
|
||||||
باشد، مثل قبل مستقیم ارسال میشود، بدون هیچ پرسش اضافهای.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "merge-mokhatebin-ganoshi",
|
|
||||||
"title": "وارد کردن مخاطبین گوشی به شاپ شیپ",
|
|
||||||
"meta_description": "سریعترین راه برای شروع: مخاطبین گوشیات را با یک تیک انتخابهمه، بدون تعیین گروه و بدون تایپ دستی، به مخاطبین یا مشتریانِ شاپ شیپ اضافه کن.",
|
|
||||||
"excerpt": "مخاطبین گوشیات را با یک تیک انتخابهمه وارد کن — بدون گروهبندی اضافه، بدون تایپ دستی.",
|
|
||||||
"read_minutes": 2,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
اگر لیست مشتری یا مخاطبت همین حالا در دفترچهتلفنِ گوشیات است، نیازی
|
|
||||||
به تایپ دوبارهی تکتک شمارهها نیست. شاپ شیپ میتواند مستقیم به
|
|
||||||
مخاطبین گوشیات دسترسی بگیرد (فقط با اجازهی خودت) و هرکدام را که
|
|
||||||
خواستی یکجا وارد اپ کند.
|
|
||||||
</p>
|
|
||||||
<h2>چطور کار میکند</h2>
|
|
||||||
<p>
|
|
||||||
از صفحهی «مخاطبین» یا «مشتریان»، گزینهی «وارد کردن از مخاطبین گوشی»
|
|
||||||
را بزن. اپ لیست مخاطبین گوشیات را نشان میدهد — با یک تیکِ
|
|
||||||
«انتخاب همه» بالای لیست برای وقتی که میخواهی همه را یکجا اضافه کنی،
|
|
||||||
یا تیک تکتک برای انتخاب دستی.
|
|
||||||
</p>
|
|
||||||
<h2>بدون مرحلهی گروهبندی</h2>
|
|
||||||
<p>
|
|
||||||
برخلاف نسخههای قدیمیتر، اینجا دیگر مجبور نیستی قبل از وارد کردن
|
|
||||||
یک «گروه مقصد» انتخاب کنی — همین یک مرحلهی کمتر یعنی وارد کردن دهها
|
|
||||||
مخاطب چند ثانیه بیشتر طول نمیکشد.
|
|
||||||
</p>
|
|
||||||
<h2>دوبارهکاری نمیشود</h2>
|
|
||||||
<p>
|
|
||||||
مخاطبینی که شمارهشان از قبل در اپ ثبت شده، خودشان از لیست واردکردن
|
|
||||||
کنار گذاشته میشوند — تطبیق بر اساس شمارهی نرمالشده انجام میشود، نه
|
|
||||||
اسم، پس اختلاف املایی اسم مشکلی ایجاد نمیکند.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "sms-ya-whatsapp-business",
|
|
||||||
"title": "پیامک یا واتساپبیزینس؟ کدام برای ارسال گروهی بهتر است",
|
|
||||||
"meta_description": "مقایسهی پیامک و واتساپبیزینس برای ارسال پیام گروهی در شاپ شیپ: هزینه، نرخ باز شدن، محدودیت Template، و کدام برای چه کسبوکاری مناسبتر است.",
|
|
||||||
"excerpt": "مقایسهی هزینه، نرخ باز شدن، و محدودیتهای هرکدام برای پیام گروهی کسبوکارها.",
|
|
||||||
"read_minutes": 5,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
هر دو کانال جای خودشان را دارند و در شاپ شیپ میتوانی همزمان از هردو
|
|
||||||
(بهعلاوهی تلگرام، بله و ایتا) استفاده کنی — یک
|
|
||||||
<a href="/articles/peyam-goruhi-ba-shomare/">قالب پیام</a> بنویسی،
|
|
||||||
برای هرکدام از کانالها بفرستی.
|
|
||||||
</p>
|
|
||||||
<h2>پیامک</h2>
|
|
||||||
<p>
|
|
||||||
به هر شمارهای میرسد، حتی بدون اینترنت یا اپ نصبشده — قابلاطمینانترین
|
|
||||||
کانال از نظر تحویل. هزینهی هر پیام مشخص و قابلپیشبینی است. محدودیت:
|
|
||||||
طول پیام (پیامکهای بلند چندقسمتی حساب میشوند) و نداشتن تصویر/دکمه.
|
|
||||||
</p>
|
|
||||||
<h2>واتساپبیزینس</h2>
|
|
||||||
<p>
|
|
||||||
نرخ باز شدن پیام معمولا بالاتر از پیامک است و امکاناتی مثل دکمه و
|
|
||||||
رسانه دارد. اما برای ارسال گروهی (business-initiated) باید از یک
|
|
||||||
Template ازپیشتاییدشده در Meta Business Manager استفاده کنی —
|
|
||||||
یعنی نمیتوانی هر متن دلخواهی را بیمحدودیت به هرکسی بفرستی.
|
|
||||||
</p>
|
|
||||||
<h2>تلگرام، بله، ایتا</h2>
|
|
||||||
<p>
|
|
||||||
اینها فقط به کسانی میرسند که قبلا با بات کسبوکارت چت را شروع
|
|
||||||
کردهاند. برای مخاطبینی که این کار را نکردهاند، شاپ شیپ یک صف
|
|
||||||
ارسال دستیِ یکلمسی نشان میدهد.
|
|
||||||
</p>
|
|
||||||
<h2>جمعبندی</h2>
|
|
||||||
<p>
|
|
||||||
اگر تحویل تضمینی برایت مهمتر است، پیامک را انتخاب کن. اگر مخاطبینت
|
|
||||||
بیشتر واتساپی هستند و Template تاییدشده داری، واتساپبیزینس گزینهی
|
|
||||||
بهتری است. بهترین نتیجه معمولا ترکیب چند کانال است.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "forosh-ghesti-hesabdari",
|
|
||||||
"title": "فروش قسطی چطور باید در دفترداری ثبت شود؟",
|
|
||||||
"meta_description": "فروش قسطی از نظر حسابداری یک نوع بدهکاری/طلب است، نه فقط یک عدد در لیست درآمد. دفتر درآمد و هزینهی شاپ شیپ این تفاوت را خودش تشخیص میدهد.",
|
|
||||||
"excerpt": "فروش قسطی فقط «درآمد» نیست — یک نوع طلب از مشتری است که باید جدا پیگیری شود.",
|
|
||||||
"read_minutes": 4,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
فرض کن یک کالا را ۵ میلیون تومان قسطی میفروشی: امروز ۲ میلیون
|
|
||||||
نقد میگیری، ۳ میلیون باقیمانده قرار است طی سه قسط پرداخت شود. سوال
|
|
||||||
اینجاست: همین امروز چقدر «درآمد» داشتهای؟
|
|
||||||
</p>
|
|
||||||
<h2>چرا فروش قسطی «فقط درآمد» نیست</h2>
|
|
||||||
<p>
|
|
||||||
از نظر حسابداریِ حرفهای، فروش قسطی یک نوع <strong>بدهکاریِ مشتری به
|
|
||||||
تو</strong> (طلب/receivable) ایجاد میکند، نه یک عدد ساده در لیست
|
|
||||||
درآمد. اگر همان ۵ میلیون را یکجا «درآمد» ثبت کنی، ترازِ نقدینگیات
|
|
||||||
اشتباه میشود — چون آن ۳ میلیونِ باقیمانده هنوز دستت نیامده.
|
|
||||||
</p>
|
|
||||||
<h2>دفتر درآمد و هزینهی شاپ شیپ چطور حلش میکند</h2>
|
|
||||||
<p>
|
|
||||||
وقتی دستهبندیِ «فروش قسطی» را برای یک ثبت انتخاب میکنی، اپ خودش
|
|
||||||
میفهمد این یک نوع بدهکاری است — و سه فیلد اضافه از تو میپرسد: مبلغِ
|
|
||||||
کل، نام و شمارهی طرفِ حساب. از همان لحظه، این ثبت یک «ماندهحساب»
|
|
||||||
دارد که هر قسطِ پرداختی از آن کم میشود.
|
|
||||||
</p>
|
|
||||||
<h2>ثبتِ ساده برای تو، حسابداریِ درست پشتِ صحنه</h2>
|
|
||||||
<p>
|
|
||||||
لازم نیست اصطلاحاتِ حسابداری بلد باشی. کافی است دستهی درستی که به
|
|
||||||
زبانِ خودت نزدیکتر است انتخاب کنی — بقیهاش (تشخیص طلب/بدهی، محاسبهی
|
|
||||||
مانده، تفکیک از درآمد نقدی) را اپ خودش پشتِ صحنه انجام میدهد.
|
|
||||||
</p>
|
|
||||||
<h2>اتصال به فاکتور و مشتری</h2>
|
|
||||||
<p>
|
|
||||||
اگر <a href="/articles/fatoore-rasmi-baraye-maghaze/">فاکتوری</a> را با
|
|
||||||
نوعِ پرداختِ «قسطی/نسیه» صادر کنی، شاپ شیپ همان لحظه یک ثبتِ طلب در
|
|
||||||
دفترت میسازد و آن را به پروندهی همان مشتری هم وصل میکند.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "fatoore-rasmi-baraye-maghaze",
|
|
||||||
"title": "فاکتور رسمی برای مغازه: چرا لازم است و چطور با انبار وصل میشود",
|
|
||||||
"meta_description": "فاکتور رسمی با لوگو، محاسبهی خودکار مالیات و تخفیف، و اتصال مستقیم به انبار — همین که پرداخت شد، موجودی خودش کم میشود.",
|
|
||||||
"excerpt": "فاکتوری که وقتی پرداخت میشود، موجودیِ انبار را هم خودش کم میکند — بدون ثبت دوباره.",
|
|
||||||
"read_minutes": 4,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از مغازهدارها فاکتور را یا اصلا صادر نمیکنند، یا دستی روی
|
|
||||||
یک برگه مینویسند — و بعد جداگانه همان کالا را از موجودیِ انبار کم
|
|
||||||
میکنند. این یعنی هر فروش، دوبار ثبت میشود.
|
|
||||||
</p>
|
|
||||||
<h2>یک فاکتور، هم برای مشتری هم برای انبار</h2>
|
|
||||||
<p>
|
|
||||||
در شاپ شیپ، وقتی برای یک فاکتور کالا اضافه میکنی، همان کالا را از
|
|
||||||
لیستِ محصولاتِ
|
|
||||||
<a href="/articles/kontorol-mojoodi-anbar-ba-mobile/">انبارت</a> انتخاب
|
|
||||||
میکنی — نه اینکه اسمش را دوباره تایپ کنی. همین که فاکتور ثبت شود،
|
|
||||||
تعداد همان کالا خودش از موجودیِ انبار کم میشود.
|
|
||||||
</p>
|
|
||||||
<h2>مالیات و تخفیف، خودکار</h2>
|
|
||||||
<p>
|
|
||||||
هر قلمِ کالا میتواند نرخِ مالیاتِ خودش و تخفیفِ خودش را داشته باشد —
|
|
||||||
جمعِ نهایی، مالیات، و مبلغِ تخفیفخورده خودکار محاسبه میشود.
|
|
||||||
</p>
|
|
||||||
<h2>پرداختشده یا نشده، همیشه مشخص</h2>
|
|
||||||
<p>
|
|
||||||
هر فاکتور یک وضعیتِ روشن دارد. برای فاکتورهای نقدی، دفترِ درآمد و
|
|
||||||
هزینهات را دستکاری نمیکند؛ اما اگر نوعِ پرداخت را «قسطی/نسیه»
|
|
||||||
بگذاری، همان لحظه یک ثبتِ طلب هم در دفترت ساخته میشود — جزئیاتش در
|
|
||||||
<a href="/articles/forosh-ghesti-hesabdari/">فروش قسطی چطور ثبت
|
|
||||||
میشود؟</a> آمده.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "kontorol-mojoodi-anbar-ba-mobile",
|
|
||||||
"title": "کنترل موجودی انبار با موبایل، بدون اکسل",
|
|
||||||
"meta_description": "دستهبندی کالا، کموزیاد کردن موجودی با یک ضربه، دیدنِ ارزش کلِ انبار، و هشدار وقتی چیزی کم میشود — همه از گوشی.",
|
|
||||||
"excerpt": "موجودی را با یک ضربه کموزیاد کن، ارزش کلِ انبارت را ببین، وقتی چیزی کم شد زودتر از مشتری بفهم.",
|
|
||||||
"read_minutes": 3,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
خیلی از کسبوکارهای کوچک موجودیِ انبار را در یک فایلِ اکسل نگه
|
|
||||||
میدارند که همیشه یک نسخهی «قدیمیتر از واقعیت» است.
|
|
||||||
</p>
|
|
||||||
<h2>هر کالا با واحد و قیمتِ خرید</h2>
|
|
||||||
<p>
|
|
||||||
برای هر کالا، واحدِ شمارشش (عدد، کیلوگرم، بسته و…) و قیمتِ خریدش را
|
|
||||||
ثبت میکنی. با همین دو عدد، اپ خودش <strong>ارزشِ کلِ انبارت</strong>
|
|
||||||
را محاسبه میکند — یعنی میدانی چقدر سرمایه روی طاقچههایت خوابیده.
|
|
||||||
</p>
|
|
||||||
<h2>کموزیاد با یک ضربه</h2>
|
|
||||||
<p>
|
|
||||||
موجودیِ هر کالا را میشود با یک ضربه کم یا زیاد کرد. اگر هم
|
|
||||||
<a href="/articles/fatoore-rasmi-baraye-maghaze/">فاکتور</a> برای همان
|
|
||||||
کالا صادر کنی، موجودی خودش کم میشود، بدون ثبتِ دستیِ دوباره.
|
|
||||||
</p>
|
|
||||||
<h2>هشدارِ کمبود</h2>
|
|
||||||
<p>
|
|
||||||
برای هر کالا میشود یک آستانهی «کم شد» تعریف کرد؛ وقتی موجودی به آن
|
|
||||||
آستانه برسد، پیش از اینکه مشتری بیاید و نداشته باشی، خودت متوجه
|
|
||||||
میشوی.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"slug": "peygiri-sefaresh-online-shop-etemad",
|
|
||||||
"title": "پیگیری سفارش آنلاینشاپ: اعتماد مشتری با یک پیامک خودکار",
|
|
||||||
"meta_description": "برای آنلاینشاپهای کوچک، شاپ شیپ مراحل سفارش را قابلتعریف میکند و با رسیدنِ سفارش به هر مرحله، خودکار از روی قالبِ دلخواهت به مشتری پیامک میرود.",
|
|
||||||
"excerpt": "مراحل کارت را تعریف کن — با رسیدنِ سفارش به هر مرحله، پیامکِ خودکار برای مشتری میرود.",
|
|
||||||
"read_minutes": 4,
|
|
||||||
"content_html": """
|
|
||||||
<p>
|
|
||||||
یکی از بزرگترین دلایلِ نگرانیِ مشتریِ آنلاینشاپهای کوچک همین است:
|
|
||||||
«سفارشم الان کجاست؟» وقتی جوابِ این سوال را با تاخیر یا اصلا نمیدهی،
|
|
||||||
اعتماد از دست میرود.
|
|
||||||
</p>
|
|
||||||
<h2>مراحلِ کارِ خودت را تعریف کن</h2>
|
|
||||||
<p>
|
|
||||||
در شاپ شیپ، مراحلِ سفارش را خودت تعریف میکنی — مثلا «آمادهسازی»،
|
|
||||||
«بستهبندی»، «ارسال»، «تحویلشد» — دقیقا همان جریانِ کاریِ واقعیِ
|
|
||||||
کسبوکارت. ترتیبِ مراحل را هم میشود با کشیدنورهاکردن جابهجا کرد.
|
|
||||||
</p>
|
|
||||||
<h2>هر مرحله، یک قالبِ پیام</h2>
|
|
||||||
<p>
|
|
||||||
هر مرحله را به یک قالبِ پیام وصل میکنی. همین که سفارشی به آن مرحله
|
|
||||||
میرسد، پیامک از روی همان قالب — خودکار — برای مشتری میرود.
|
|
||||||
</p>
|
|
||||||
<h2>وقتی یک مقدار متغیر لازم است</h2>
|
|
||||||
<p>
|
|
||||||
برای مرحلهی «ارسال» معمولا یک کد رهگیریِ پستی لازم است که از قبل
|
|
||||||
معلوم نیست. برای همین، قالبِ همین مرحله میتواند یک
|
|
||||||
<a href="/articles/field-sefareshi-dar-template/">فیلدِ اجرا</a>
|
|
||||||
داشته باشد.
|
|
||||||
</p>
|
|
||||||
""",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
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),
|
|
||||||
]
|
|
||||||
@@ -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')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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')],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user