add retry sms manually
This commit is contained in:
1 parent
33930e972c
commit
cb92abbb41
42 files changed
+1628
-1484
No files matched your search
@@ -1,78 +1,78 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
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='OTPChallenge',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mobile', models.CharField(db_index=True, max_length=16)),
|
||||
('purpose', models.CharField(choices=[('LOGIN', 'ورود')], default='LOGIN', max_length=20)),
|
||||
('code_hash', models.CharField(max_length=64)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('attempt_count', models.PositiveSmallIntegerField(default=0)),
|
||||
('max_attempts', models.PositiveSmallIntegerField(default=5)),
|
||||
('verified_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('request_ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.CharField(blank=True, max_length=300)),
|
||||
],
|
||||
),
|
||||
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')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('mobile', models.CharField(blank=True, max_length=16, null=True, unique=True)),
|
||||
('account_type', models.CharField(choices=[('CUSTOMER', 'مشتری'), ('STAFF', 'کارشناس')], default='CUSTOMER', max_length=10)),
|
||||
('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')),
|
||||
('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={
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketMagicLink',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token_hash', models.CharField(max_length=64, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('used_at', models.DateTimeField(blank=True, null=True)),
|
||||
('revoked_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_reason', models.CharField(blank=True, max_length=100)),
|
||||
('last_used_ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
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='OTPChallenge',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('mobile', models.CharField(db_index=True, max_length=16)),
|
||||
('purpose', models.CharField(choices=[('LOGIN', 'ورود')], default='LOGIN', max_length=20)),
|
||||
('code_hash', models.CharField(max_length=64)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('attempt_count', models.PositiveSmallIntegerField(default=0)),
|
||||
('max_attempts', models.PositiveSmallIntegerField(default=5)),
|
||||
('verified_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('request_ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.CharField(blank=True, max_length=300)),
|
||||
],
|
||||
),
|
||||
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')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('mobile', models.CharField(blank=True, max_length=16, null=True, unique=True)),
|
||||
('account_type', models.CharField(choices=[('CUSTOMER', 'مشتری'), ('STAFF', 'کارشناس')], default='CUSTOMER', max_length=10)),
|
||||
('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')),
|
||||
('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={
|
||||
'abstract': False,
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketMagicLink',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('token_hash', models.CharField(max_length=64, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('expires_at', models.DateTimeField()),
|
||||
('used_at', models.DateTimeField(blank=True, null=True)),
|
||||
('revoked_at', models.DateTimeField(blank=True, null=True)),
|
||||
('created_reason', models.CharField(blank=True, max_length=100)),
|
||||
('last_used_ip', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,22 +1,22 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
('tickets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='ticket',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='magic_links', to='tickets.ticket'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
('tickets', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='ticket',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='magic_links', to='tickets.ticket'),
|
||||
),
|
||||
]
|
||||
+38
-38
@@ -1,38 +1,38 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MagicLinkAccessLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('opened_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.CharField(blank=True, max_length=500)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'بازدید لینک جادویی',
|
||||
'verbose_name_plural': 'بازدیدهای لینک جادویی',
|
||||
'ordering': ['-opened_at'],
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='ticketmagiclink',
|
||||
options={'permissions': [('view_all_magic_links', 'Can audit magic links across all units')]},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='created_magic_links', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MagicLinkAccessLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('opened_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('ip_address', models.GenericIPAddressField(blank=True, null=True)),
|
||||
('user_agent', models.CharField(blank=True, max_length=500)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'بازدید لینک جادویی',
|
||||
'verbose_name_plural': 'بازدیدهای لینک جادویی',
|
||||
'ordering': ['-opened_at'],
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='ticketmagiclink',
|
||||
options={'permissions': [('view_all_magic_links', 'Can audit magic links across all units')]},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='created_magic_links', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -1,45 +1,45 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0003_magiclinkaccesslog_alter_ticketmagiclink_options_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='created_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='magic_links', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='last_user_agent',
|
||||
field=models.CharField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='recipient_mobile',
|
||||
field=models.CharField(blank=True, max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='recipient_name',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='view_count',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='magiclinkaccesslog',
|
||||
name='magic_link',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_logs', to='accounts.ticketmagiclink'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0003_magiclinkaccesslog_alter_ticketmagiclink_options_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='created_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='magic_links', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='last_user_agent',
|
||||
field=models.CharField(blank=True, max_length=500),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='recipient_mobile',
|
||||
field=models.CharField(blank=True, max_length=16),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='recipient_name',
|
||||
field=models.CharField(blank=True, max_length=200),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketmagiclink',
|
||||
name='view_count',
|
||||
field=models.PositiveIntegerField(default=0),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='magiclinkaccesslog',
|
||||
name='magic_link',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_logs', to='accounts.ticketmagiclink'),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +1,27 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0004_ticketmagiclink_created_unit_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
('tickets', '0006_assign_legacy_contacts_to_sales'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='magiclinkaccesslog',
|
||||
index=models.Index(fields=['magic_link', 'opened_at'], name='magic_opened_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticketmagiclink',
|
||||
index=models.Index(fields=['created_unit', 'created_at'], name='magic_unit_created_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticketmagiclink',
|
||||
index=models.Index(fields=['expires_at', 'revoked_at'], name='magic_validity_idx'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 15:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0004_ticketmagiclink_created_unit_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
('tickets', '0006_assign_legacy_contacts_to_sales'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='magiclinkaccesslog',
|
||||
index=models.Index(fields=['magic_link', 'opened_at'], name='magic_opened_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticketmagiclink',
|
||||
index=models.Index(fields=['created_unit', 'created_at'], name='magic_unit_created_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticketmagiclink',
|
||||
index=models.Index(fields=['expires_at', 'revoked_at'], name='magic_validity_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,49 +1,49 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import apps.attachments.models
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('file', models.FileField(upload_to=apps.attachments.models.attachment_path)),
|
||||
('original_name', models.CharField(max_length=255)),
|
||||
('stored_name', models.CharField(max_length=255)),
|
||||
('relative_path', models.CharField(max_length=500)),
|
||||
('attachment_type', models.CharField(choices=[('IMAGE', 'تصویر'), ('VIDEO', 'ویدئو'), ('VOICE', 'صدا'), ('PDF', 'PDF'), ('WORD', 'Word'), ('EXCEL', 'Excel'), ('ZIP', 'ZIP'), ('RAR', 'RAR'), ('OTHER', 'سایر')], max_length=20)),
|
||||
('mime_type', models.CharField(max_length=150)),
|
||||
('extension', models.CharField(max_length=20)),
|
||||
('file_size', models.PositiveBigIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'permissions': [('download_attachment', 'Can download attachment'), ('manage_attachment_policy', 'Can manage attachment policy')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AttachmentPolicy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('attachment_type', models.CharField(max_length=20, unique=True)),
|
||||
('is_enabled', models.BooleanField(default=True)),
|
||||
('max_file_size', models.PositiveBigIntegerField()),
|
||||
('max_files_per_message', models.PositiveSmallIntegerField(default=10)),
|
||||
('allowed_extensions', models.JSONField(default=list)),
|
||||
('allowed_mime_types', models.JSONField(default=list)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import apps.attachments.models
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Attachment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('file', models.FileField(upload_to=apps.attachments.models.attachment_path)),
|
||||
('original_name', models.CharField(max_length=255)),
|
||||
('stored_name', models.CharField(max_length=255)),
|
||||
('relative_path', models.CharField(max_length=500)),
|
||||
('attachment_type', models.CharField(choices=[('IMAGE', 'تصویر'), ('VIDEO', 'ویدئو'), ('VOICE', 'صدا'), ('PDF', 'PDF'), ('WORD', 'Word'), ('EXCEL', 'Excel'), ('ZIP', 'ZIP'), ('RAR', 'RAR'), ('OTHER', 'سایر')], max_length=20)),
|
||||
('mime_type', models.CharField(max_length=150)),
|
||||
('extension', models.CharField(max_length=20)),
|
||||
('file_size', models.PositiveBigIntegerField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
options={
|
||||
'permissions': [('download_attachment', 'Can download attachment'), ('manage_attachment_policy', 'Can manage attachment policy')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='AttachmentPolicy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('attachment_type', models.CharField(max_length=20, unique=True)),
|
||||
('is_enabled', models.BooleanField(default=True)),
|
||||
('max_file_size', models.PositiveBigIntegerField()),
|
||||
('max_files_per_message', models.PositiveSmallIntegerField(default=10)),
|
||||
('allowed_extensions', models.JSONField(default=list)),
|
||||
('allowed_mime_types', models.JSONField(default=list)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,29 +1,29 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0001_initial'),
|
||||
('messaging', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='attachment',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='attachments', to='messaging.message'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='attachment',
|
||||
name='uploaded_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0001_initial'),
|
||||
('messaging', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='attachment',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='attachments', to='messaging.message'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='attachment',
|
||||
name='uploaded_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +1,19 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 08:31
|
||||
|
||||
import apps.attachments.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='file',
|
||||
field=models.FileField(max_length=500, upload_to=apps.attachments.models.attachment_path),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 08:31
|
||||
|
||||
import apps.attachments.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('attachments', '0002_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='attachment',
|
||||
name='file',
|
||||
field=models.FileField(max_length=500, upload_to=apps.attachments.models.attachment_path),
|
||||
),
|
||||
]
|
||||
@@ -1,47 +1,47 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 03:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PageTitle',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('route_name', models.CharField(help_text='مانند accounts:login', max_length=160, unique=True)),
|
||||
('title', models.CharField(max_length=240)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'عنوان صفحه',
|
||||
'verbose_name_plural': 'عنوان صفحات',
|
||||
'ordering': ('route_name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SiteBranding',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('site_title', models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160)),
|
||||
('header_title', models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160)),
|
||||
('header_subtitle', models.CharField(default='درخواست روشن، پیگیری مطمئن', max_length=200)),
|
||||
('staff_brand_title', models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160)),
|
||||
('staff_brand_subtitle', models.CharField(default='مرکز ارتباط با مشتری', max_length=200)),
|
||||
('staff_panel_label', models.CharField(default='پنل کارشناسان', max_length=120)),
|
||||
('pwa_title', models.CharField(default='واحد فروش', max_length=80)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'تنظیمات عنوان و برند',
|
||||
'verbose_name_plural': 'تنظیمات عنوان و برند',
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 03:38
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PageTitle',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('route_name', models.CharField(help_text='مانند accounts:login', max_length=160, unique=True)),
|
||||
('title', models.CharField(max_length=240)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'عنوان صفحه',
|
||||
'verbose_name_plural': 'عنوان صفحات',
|
||||
'ordering': ('route_name',),
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='SiteBranding',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('site_title', models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160)),
|
||||
('header_title', models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160)),
|
||||
('header_subtitle', models.CharField(default='درخواست روشن، پیگیری مطمئن', max_length=200)),
|
||||
('staff_brand_title', models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160)),
|
||||
('staff_brand_subtitle', models.CharField(default='مرکز ارتباط با مشتری', max_length=200)),
|
||||
('staff_panel_label', models.CharField(default='پنل کارشناسان', max_length=120)),
|
||||
('pwa_title', models.CharField(default='واحد فروش', max_length=80)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'تنظیمات عنوان و برند',
|
||||
'verbose_name_plural': 'تنظیمات عنوان و برند',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,53 +1,53 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 04:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_seed_branding_and_page_titles'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='developer_credit_html',
|
||||
field=models.TextField(blank=True, help_text='فقط برای HTML مورد اعتماد؛ لینک، آیکون و style خطی مجاز است.', verbose_name='HTML سفارشی امضای توسعه\u200cدهنده'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='developer_credit_text',
|
||||
field=models.CharField(default='برنامه نویسی و توسعه: محمد جواد فصاحت | 09202202129', max_length=500, verbose_name='متن امضای توسعه\u200cدهنده'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_body',
|
||||
field=models.TextField(default='درخواست خود را ثبت کنید، فایل و پیام صوتی بفرستید و پاسخ کارشناس را در یک فضای امن دنبال کنید.', verbose_name='متن معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='نمایش بخش معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_eyebrow',
|
||||
field=models.CharField(default='ارتباط مستقیم با تیم فروش', max_length=200, verbose_name='بالانویس معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_heading',
|
||||
field=models.TextField(default='پاسخ روشن،\nپیگیری ساده.', verbose_name='عنوان معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_icon',
|
||||
field=models.CharField(blank=True, default='✦', max_length=32, verbose_name='آیکون معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='use_developer_credit_html',
|
||||
field=models.BooleanField(default=False, help_text='با فعال\u200cسازی، HTML بدون escape نمایش داده می\u200cشود؛ فقط مدیر ارشد باید آن را ویرایش کند.', verbose_name='استفاده از HTML سفارشی'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 04:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0002_seed_branding_and_page_titles'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='developer_credit_html',
|
||||
field=models.TextField(blank=True, help_text='فقط برای HTML مورد اعتماد؛ لینک، آیکون و style خطی مجاز است.', verbose_name='HTML سفارشی امضای توسعه\u200cدهنده'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='developer_credit_text',
|
||||
field=models.CharField(default='برنامه نویسی و توسعه: محمد جواد فصاحت | 09202202129', max_length=500, verbose_name='متن امضای توسعه\u200cدهنده'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_body',
|
||||
field=models.TextField(default='درخواست خود را ثبت کنید، فایل و پیام صوتی بفرستید و پاسخ کارشناس را در یک فضای امن دنبال کنید.', verbose_name='متن معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='نمایش بخش معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_eyebrow',
|
||||
field=models.CharField(default='ارتباط مستقیم با تیم فروش', max_length=200, verbose_name='بالانویس معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_heading',
|
||||
field=models.TextField(default='پاسخ روشن،\nپیگیری ساده.', verbose_name='عنوان معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='login_intro_icon',
|
||||
field=models.CharField(blank=True, default='✦', max_length=32, verbose_name='آیکون معرفی ورود'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='use_developer_credit_html',
|
||||
field=models.BooleanField(default=False, help_text='با فعال\u200cسازی، HTML بدون escape نمایش داده می\u200cشود؛ فقط مدیر ارشد باید آن را ویرایش کند.', verbose_name='استفاده از HTML سفارشی'),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +1,28 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_sitebranding_developer_credit_html_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_sidebar_credit_html',
|
||||
field=models.TextField(blank=True, help_text='مستقل از امضای عمومی؛ فقط برای HTML مورد اعتماد.', verbose_name='HTML سفارشی امضای sidebar کارکنان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_sidebar_credit_text',
|
||||
field=models.CharField(default='برنامه نویسی و توسعه: محمد جواد فصاحت', max_length=500, verbose_name='متن امضای پایین sidebar کارکنان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='use_staff_sidebar_credit_html',
|
||||
field=models.BooleanField(default=False, verbose_name='استفاده از HTML سفارشی در sidebar'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:04
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0003_sitebranding_developer_credit_html_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_sidebar_credit_html',
|
||||
field=models.TextField(blank=True, help_text='مستقل از امضای عمومی؛ فقط برای HTML مورد اعتماد.', verbose_name='HTML سفارشی امضای sidebar کارکنان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_sidebar_credit_text',
|
||||
field=models.CharField(default='برنامه نویسی و توسعه: محمد جواد فصاحت', max_length=500, verbose_name='متن امضای پایین sidebar کارکنان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='use_staff_sidebar_credit_html',
|
||||
field=models.BooleanField(default=False, verbose_name='استفاده از HTML سفارشی در sidebar'),
|
||||
),
|
||||
]
|
||||
@@ -1,93 +1,93 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_sitebranding_staff_sidebar_credit_html_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='company_logo',
|
||||
field=models.ImageField(blank=True, upload_to='branding/logo/', verbose_name='لوگوی اصلی'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_body',
|
||||
field=models.TextField(default='موضوع جدیدی دارید؟ آن را جدا ثبت کنید تا تیم فروش دقیق\u200cتر و سریع\u200cتر پیگیری کند.', verbose_name='متن صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='نمایش معرفی صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_eyebrow',
|
||||
field=models.CharField(default='مرکز ارتباط با فروش', max_length=200, verbose_name='بالانویس صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_heading',
|
||||
field=models.TextField(default='هر درخواست، یک مسیر روشن\nتا رسیدن به پاسخ.', verbose_name='عنوان صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='favicon',
|
||||
field=models.ImageField(blank=True, upload_to='branding/favicon/', verbose_name='Favicon'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_description',
|
||||
field=models.CharField(default='ثبت و پیگیری درخواست\u200cهای فروش لوله گستر گلپایگان', max_length=300, verbose_name='توضیح PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_icon',
|
||||
field=models.ImageField(blank=True, upload_to='branding/pwa/', verbose_name='آیکون PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_install_help',
|
||||
field=models.CharField(default='برای دسترسی سریع، برنامه را به صفحه اصلی گوشی اضافه کنید.', max_length=300, verbose_name='راهنمای نصب PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_install_label',
|
||||
field=models.CharField(default='نصب گفت\u200cوگو با واحد فروش', max_length=160, verbose_name='عنوان پیشنهاد نصب PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_name',
|
||||
field=models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160, verbose_name='نام کامل PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_body',
|
||||
field=models.TextField(default='صف درخواست\u200cها، SLA، اولویت\u200cها و سوابق مشتری را از یک میز کار متمرکز مدیریت کنید.', verbose_name='متن ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='نمایش معرفی ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_eyebrow',
|
||||
field=models.CharField(default='پنل کارشناسان فروش', max_length=200, verbose_name='بالانویس ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_heading',
|
||||
field=models.TextField(default='پاسخ\u200cهای بهتر،\nفروش هوشمندتر.', verbose_name='عنوان ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_icon',
|
||||
field=models.CharField(blank=True, default='◈', max_length=32, verbose_name='آیکون معرفی ورود کارشناسان'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:18
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('core', '0004_sitebranding_staff_sidebar_credit_html_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='company_logo',
|
||||
field=models.ImageField(blank=True, upload_to='branding/logo/', verbose_name='لوگوی اصلی'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_body',
|
||||
field=models.TextField(default='موضوع جدیدی دارید؟ آن را جدا ثبت کنید تا تیم فروش دقیق\u200cتر و سریع\u200cتر پیگیری کند.', verbose_name='متن صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='نمایش معرفی صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_eyebrow',
|
||||
field=models.CharField(default='مرکز ارتباط با فروش', max_length=200, verbose_name='بالانویس صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='customer_tickets_hero_heading',
|
||||
field=models.TextField(default='هر درخواست، یک مسیر روشن\nتا رسیدن به پاسخ.', verbose_name='عنوان صفحه تیکت\u200cهای مشتری'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='favicon',
|
||||
field=models.ImageField(blank=True, upload_to='branding/favicon/', verbose_name='Favicon'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_description',
|
||||
field=models.CharField(default='ثبت و پیگیری درخواست\u200cهای فروش لوله گستر گلپایگان', max_length=300, verbose_name='توضیح PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_icon',
|
||||
field=models.ImageField(blank=True, upload_to='branding/pwa/', verbose_name='آیکون PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_install_help',
|
||||
field=models.CharField(default='برای دسترسی سریع، برنامه را به صفحه اصلی گوشی اضافه کنید.', max_length=300, verbose_name='راهنمای نصب PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_install_label',
|
||||
field=models.CharField(default='نصب گفت\u200cوگو با واحد فروش', max_length=160, verbose_name='عنوان پیشنهاد نصب PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='pwa_name',
|
||||
field=models.CharField(default='گفت\u200cوگو با واحد فروش', max_length=160, verbose_name='نام کامل PWA'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_body',
|
||||
field=models.TextField(default='صف درخواست\u200cها، SLA، اولویت\u200cها و سوابق مشتری را از یک میز کار متمرکز مدیریت کنید.', verbose_name='متن ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_enabled',
|
||||
field=models.BooleanField(default=True, verbose_name='نمایش معرفی ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_eyebrow',
|
||||
field=models.CharField(default='پنل کارشناسان فروش', max_length=200, verbose_name='بالانویس ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_heading',
|
||||
field=models.TextField(default='پاسخ\u200cهای بهتر،\nفروش هوشمندتر.', verbose_name='عنوان ورود کارشناسان'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='sitebranding',
|
||||
name='staff_login_intro_icon',
|
||||
field=models.CharField(blank=True, default='◈', max_length=32, verbose_name='آیکون معرفی ورود کارشناسان'),
|
||||
),
|
||||
]
|
||||
@@ -1,32 +1,32 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
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='CustomerProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('first_name', models.CharField(blank=True, max_length=100)),
|
||||
('last_name', models.CharField(blank=True, max_length=100)),
|
||||
('customer_type', models.CharField(blank=True, choices=[('PERSON', 'شخص'), ('CONSUMER', 'مصرف\u200cکننده نهایی'), ('SHOP', 'فروشگاه'), ('REPRESENTATIVE', 'نمایندگی'), ('COMPANY', 'شرکت'), ('OTHER', 'سایر')], max_length=20)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
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='CustomerProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('first_name', models.CharField(blank=True, max_length=100)),
|
||||
('last_name', models.CharField(blank=True, max_length=100)),
|
||||
('customer_type', models.CharField(blank=True, choices=[('PERSON', 'شخص'), ('CONSUMER', 'مصرف\u200cکننده نهایی'), ('SHOP', 'فروشگاه'), ('REPRESENTATIVE', 'نمایندگی'), ('COMPANY', 'شرکت'), ('OTHER', 'سایر')], max_length=20)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='customer_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
||||
+41
-41
@@ -1,41 +1,41 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import apps.core.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0002_customerprofile_city_customerprofile_province'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContactGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=150)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'گروه مخاطبان',
|
||||
'verbose_name_plural': 'گروه\u200cهای مخاطبان',
|
||||
'ordering': ['unit__name', 'name'],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customerprofile',
|
||||
name='organization_name',
|
||||
field=models.CharField(blank=True, max_length=180),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customerprofile',
|
||||
name='stakeholder_type',
|
||||
field=models.CharField(choices=[('CUSTOMER', 'مشتری'), ('SUPPLIER', 'تامین\u200cکننده'), ('CARRIER', 'باربری'), ('OTHER', 'سایر')], db_index=True, default='CUSTOMER', max_length=20),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import apps.core.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0002_customerprofile_city_customerprofile_province'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ContactGroup',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=150)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'گروه مخاطبان',
|
||||
'verbose_name_plural': 'گروه\u200cهای مخاطبان',
|
||||
'ordering': ['unit__name', 'name'],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customerprofile',
|
||||
name='organization_name',
|
||||
field=models.CharField(blank=True, max_length=180),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customerprofile',
|
||||
name='stakeholder_type',
|
||||
field=models.CharField(choices=[('CUSTOMER', 'مشتری'), ('SUPPLIER', 'تامین\u200cکننده'), ('CARRIER', 'باربری'), ('OTHER', 'سایر')], db_index=True, default='CUSTOMER', max_length=20),
|
||||
),
|
||||
]
|
||||
+29
-29
@@ -1,29 +1,29 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0003_contactgroup_customerprofile_organization_name_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contactgroup',
|
||||
name='unit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_groups', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customerprofile',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(blank=True, related_name='contacts', to='customers.contactgroup'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='contactgroup',
|
||||
constraint=models.UniqueConstraint(fields=('unit', 'name'), name='unique_contact_group_name_per_unit'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0003_contactgroup_customerprofile_organization_name_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='contactgroup',
|
||||
name='unit',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='contact_groups', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='customerprofile',
|
||||
name='groups',
|
||||
field=models.ManyToManyField(blank=True, related_name='contacts', to='customers.contactgroup'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='contactgroup',
|
||||
constraint=models.UniqueConstraint(fields=('unit', 'name'), name='unique_contact_group_name_per_unit'),
|
||||
),
|
||||
]
|
||||
@@ -1,53 +1,53 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
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='MessageMention',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageRevision',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('body', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('body', models.TextField(blank=True)),
|
||||
('message_type', models.CharField(choices=[('PUBLIC', 'عمومی'), ('INTERNAL_NOTE', 'یادداشت داخلی')], default='PUBLIC', max_length=20)),
|
||||
('is_internal', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('edited_at', models.DateTimeField(blank=True, null=True)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='authored_messages', to=settings.AUTH_USER_MODEL)),
|
||||
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='deleted_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at', 'id'],
|
||||
'permissions': [('add_internal_note', 'Can add internal note'), ('edit_own_message', 'Can edit own message'), ('delete_own_message', 'Can delete own message'), ('view_deleted_message_content', 'Can view deleted content'), ('view_message_revisions', 'Can view revisions')],
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
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='MessageMention',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MessageRevision',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('body', models.TextField()),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Message',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('body', models.TextField(blank=True)),
|
||||
('message_type', models.CharField(choices=[('PUBLIC', 'عمومی'), ('INTERNAL_NOTE', 'یادداشت داخلی')], default='PUBLIC', max_length=20)),
|
||||
('is_internal', models.BooleanField(default=False)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('edited_at', models.DateTimeField(blank=True, null=True)),
|
||||
('deleted_at', models.DateTimeField(blank=True, null=True)),
|
||||
('author', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='authored_messages', to=settings.AUTH_USER_MODEL)),
|
||||
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='deleted_messages', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['created_at', 'id'],
|
||||
'permissions': [('add_internal_note', 'Can add internal note'), ('edit_own_message', 'Can edit own message'), ('delete_own_message', 'Can delete own message'), ('view_deleted_message_content', 'Can view deleted content'), ('view_message_revisions', 'Can view revisions')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,48 +1,48 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('messaging', '0001_initial'),
|
||||
('tickets', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='ticket',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ticket_messages', to='tickets.ticket'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagemention',
|
||||
name='mentioned_user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagemention',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentions', to='messaging.message'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagerevision',
|
||||
name='edited_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagerevision',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='messaging.message'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='messagemention',
|
||||
constraint=models.UniqueConstraint(fields=('message', 'mentioned_user'), name='unique_message_mention'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('messaging', '0001_initial'),
|
||||
('tickets', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='message',
|
||||
name='ticket',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='ticket_messages', to='tickets.ticket'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagemention',
|
||||
name='mentioned_user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagemention',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='mentions', to='messaging.message'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagerevision',
|
||||
name='edited_by',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='messagerevision',
|
||||
name='message',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='revisions', to='messaging.message'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='messagemention',
|
||||
constraint=models.UniqueConstraint(fields=('message', 'mentioned_user'), name='unique_message_mention'),
|
||||
),
|
||||
]
|
||||
@@ -1,29 +1,29 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-06 15:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('messaging', '0002_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MessageReadReceipt',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('read_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_receipts', to='messaging.message')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='message_read_receipts', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['read_at', 'id'],
|
||||
'constraints': [models.UniqueConstraint(fields=('message', 'user'), name='unique_message_read_receipt')],
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-06 15:45
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('messaging', '0002_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='MessageReadReceipt',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('read_at', models.DateTimeField(auto_now_add=True, db_index=True)),
|
||||
('message', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='read_receipts', to='messaging.message')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='message_read_receipts', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['read_at', 'id'],
|
||||
'constraints': [models.UniqueConstraint(fields=('message', 'user'), name='unique_message_read_receipt')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,37 +1,37 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NotificationJob',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('channel', models.CharField(choices=[('SMS', 'پیامک'), ('BALE', 'بله')], max_length=10)),
|
||||
('event_type', models.CharField(max_length=50)),
|
||||
('payload', models.JSONField(default=dict)),
|
||||
('status', models.CharField(choices=[('PENDING', 'در صف'), ('PROCESSING', 'در حال ارسال'), ('SENT', 'ارسال\u200cشده'), ('FAILED', 'ناموفق')], db_index=True, default='PENDING', max_length=15)),
|
||||
('attempts', models.PositiveSmallIntegerField(default=0)),
|
||||
('next_attempt_at', models.DateTimeField(db_index=True)),
|
||||
('last_error', models.TextField(blank=True)),
|
||||
('dedupe_key', models.CharField(max_length=200, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('sent_at', models.DateTimeField(blank=True, null=True)),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_notifications', to=settings.AUTH_USER_MODEL)),
|
||||
('staff_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='staff_notifications', to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tickets.ticket')),
|
||||
],
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NotificationJob',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('channel', models.CharField(choices=[('SMS', 'پیامک'), ('BALE', 'بله')], max_length=10)),
|
||||
('event_type', models.CharField(max_length=50)),
|
||||
('payload', models.JSONField(default=dict)),
|
||||
('status', models.CharField(choices=[('PENDING', 'در صف'), ('PROCESSING', 'در حال ارسال'), ('SENT', 'ارسال\u200cشده'), ('FAILED', 'ناموفق')], db_index=True, default='PENDING', max_length=15)),
|
||||
('attempts', models.PositiveSmallIntegerField(default=0)),
|
||||
('next_attempt_at', models.DateTimeField(db_index=True)),
|
||||
('last_error', models.TextField(blank=True)),
|
||||
('dedupe_key', models.CharField(max_length=200, unique=True)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('sent_at', models.DateTimeField(blank=True, null=True)),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='customer_notifications', to=settings.AUTH_USER_MODEL)),
|
||||
('staff_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='staff_notifications', to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='tickets.ticket')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-07 04:56
|
||||
|
||||
import apps.core.models
|
||||
# Generated by Django 5.1.1 on 2026-09-07 04:56
|
||||
|
||||
import apps.core.models
|
||||
from django.db import migrations, models
|
||||
|
||||
TAGS=["magic_link","first_name","last_name","ticket_number","subject","province","city","expert_name"]
|
||||
@@ -14,26 +14,26 @@ def seed_sms_templates(apps,schema_editor):
|
||||
for event,(title,body) in DEFAULTS.items(): model.objects.get_or_create(event_type=event,defaults={"title":title,"body":body,"allowed_tags":TAGS})
|
||||
def remove_sms_templates(apps,schema_editor):
|
||||
apps.get_model("notifications","SMSTemplate").objects.filter(event_type__in=DEFAULTS).delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SMSTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event_type', models.CharField(choices=[('CUSTOMER_TICKET_CREATED', 'ثبت تیکت توسط مشتری'), ('STAFF_TICKET_CREATED', 'ثبت تیکت توسط کارشناس'), ('STAFF_REPLY', 'پاسخ کارشناس')], max_length=50, unique=True)),
|
||||
('title', models.CharField(max_length=120)),
|
||||
('body', models.TextField(help_text='تگ\u200cها را مانند {magic_link} در متن قرار دهید.')),
|
||||
('allowed_tags', models.JSONField(blank=True, default=list)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
name='SMSTemplate',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event_type', models.CharField(choices=[('CUSTOMER_TICKET_CREATED', 'ثبت تیکت توسط مشتری'), ('STAFF_TICKET_CREATED', 'ثبت تیکت توسط کارشناس'), ('STAFF_REPLY', 'پاسخ کارشناس')], max_length=50, unique=True)),
|
||||
('title', models.CharField(max_length=120)),
|
||||
('body', models.TextField(help_text='تگ\u200cها را مانند {magic_link} در متن قرار دهید.')),
|
||||
('allowed_tags', models.JSONField(blank=True, default=list)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.RunPython(seed_sms_templates,remove_sms_templates),
|
||||
|
||||
+33
-33
@@ -1,33 +1,33 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0004_customer_reply_bale_policy'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='smstemplate',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, help_text='خالی یعنی الگوی عمومی و fallback.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sms_templates', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='smstemplate',
|
||||
name='event_type',
|
||||
field=models.CharField(choices=[('CUSTOMER_TICKET_CREATED', 'ثبت تیکت توسط مشتری'), ('STAFF_TICKET_CREATED', 'ثبت تیکت توسط کارشناس'), ('STAFF_REPLY', 'پاسخ کارشناس')], max_length=50),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='smstemplate',
|
||||
constraint=models.UniqueConstraint(fields=('unit', 'event_type'), name='unique_sms_template_per_unit'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='smstemplate',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('unit__isnull', True)), fields=('event_type',), name='unique_global_sms_template_event'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0004_customer_reply_bale_policy'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='smstemplate',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, help_text='خالی یعنی الگوی عمومی و fallback.', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='sms_templates', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='smstemplate',
|
||||
name='event_type',
|
||||
field=models.CharField(choices=[('CUSTOMER_TICKET_CREATED', 'ثبت تیکت توسط مشتری'), ('STAFF_TICKET_CREATED', 'ثبت تیکت توسط کارشناس'), ('STAFF_REPLY', 'پاسخ کارشناس')], max_length=50),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='smstemplate',
|
||||
constraint=models.UniqueConstraint(fields=('unit', 'event_type'), name='unique_sms_template_per_unit'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='smstemplate',
|
||||
constraint=models.UniqueConstraint(condition=models.Q(('unit__isnull', True)), fields=('event_type',), name='unique_global_sms_template_event'),
|
||||
),
|
||||
]
|
||||
@@ -1,6 +1,6 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 10:05
|
||||
|
||||
import apps.notifications.models
|
||||
# Generated by Django 5.1.1 on 2026-09-08 10:05
|
||||
|
||||
import apps.notifications.models
|
||||
from django.db import migrations, models
|
||||
|
||||
TAGS = ["magic_link", "first_name", "last_name", "recipient_name", "unit_name", "company_name", "ticket_number", "subject", "province", "city", "expert_name"]
|
||||
@@ -14,15 +14,15 @@ def add_supported_tags(apps, schema_editor):
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0005_smstemplate_unit_alter_smstemplate_event_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0005_smstemplate_unit_alter_smstemplate_event_type_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='smstemplate',
|
||||
name='allowed_tags',
|
||||
model_name='smstemplate',
|
||||
name='allowed_tags',
|
||||
field=models.JSONField(blank=True, default=apps.notifications.models.sms_template_tags),
|
||||
),
|
||||
migrations.RunPython(add_supported_tags, migrations.RunPython.noop),
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 15:32
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0006_alter_smstemplate_allowed_tags'),
|
||||
('tickets', '0006_assign_legacy_contacts_to_sales'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='notificationjob',
|
||||
index=models.Index(fields=['status', 'next_attempt_at'], name='notif_due_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notificationjob',
|
||||
index=models.Index(fields=['ticket', 'channel', 'status'], name='notif_ticket_chan_idx'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 15:32
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0006_alter_smstemplate_allowed_tags'),
|
||||
('tickets', '0006_assign_legacy_contacts_to_sales'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddIndex(
|
||||
model_name='notificationjob',
|
||||
index=models.Index(fields=['status', 'next_attempt_at'], name='notif_due_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='notificationjob',
|
||||
index=models.Index(fields=['ticket', 'channel', 'status'], name='notif_ticket_chan_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 15:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0007_notificationjob_notif_due_idx_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='notificationjob',
|
||||
name='provider_message_id',
|
||||
field=models.CharField(blank=True, max_length=500),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 15:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('notifications', '0007_notificationjob_notif_due_idx_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='notificationjob',
|
||||
name='provider_message_id',
|
||||
field=models.CharField(blank=True, max_length=500),
|
||||
),
|
||||
]
|
||||
@@ -1,37 +1,37 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
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='StaffProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('display_name', models.CharField(max_length=150)),
|
||||
('avatar', models.ImageField(blank=True, upload_to='staff/avatars/')),
|
||||
('mobile', models.CharField(blank=True, max_length=16)),
|
||||
('bale_chat_id', models.CharField(blank=True, max_length=100)),
|
||||
('bale_notifications_enabled', models.BooleanField(default=True)),
|
||||
('is_customer_selectable', models.BooleanField(default=True)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['sort_order', 'display_name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
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='StaffProfile',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('display_name', models.CharField(max_length=150)),
|
||||
('avatar', models.ImageField(blank=True, upload_to='staff/avatars/')),
|
||||
('mobile', models.CharField(blank=True, max_length=16)),
|
||||
('bale_chat_id', models.CharField(blank=True, max_length=100)),
|
||||
('bale_notifications_enabled', models.BooleanField(default=True)),
|
||||
('is_customer_selectable', models.BooleanField(default=True)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='staff_profile', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['sort_order', 'display_name'],
|
||||
},
|
||||
),
|
||||
]
|
||||
+28
-28
@@ -1,28 +1,28 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-06 10:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='first_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='job_title',
|
||||
field=models.CharField(blank=True, default='کارشناس فروش', max_length=150),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='last_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-06 10:20
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='first_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='job_title',
|
||||
field=models.CharField(blank=True, default='کارشناس فروش', max_length=150),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='last_name',
|
||||
field=models.CharField(blank=True, max_length=100),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-07 11:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0002_staffprofile_first_name_staffprofile_job_title_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('SALES_EXPERT', 'کارشناس فروش'), ('SALES_SUPERVISOR', 'سرپرست فروش'), ('SALES_MANAGER', 'مدیر فروش'), ('CEO', 'مدیرعامل'), ('GENERAL_ADMIN', 'مدیرکل Admin')], db_index=True, default='SALES_EXPERT', max_length=24),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-07 11:02
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0002_staffprofile_first_name_staffprofile_job_title_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='role',
|
||||
field=models.CharField(choices=[('SALES_EXPERT', 'کارشناس فروش'), ('SALES_SUPERVISOR', 'سرپرست فروش'), ('SALES_MANAGER', 'مدیر فروش'), ('CEO', 'مدیرعامل'), ('GENERAL_ADMIN', 'مدیرکل Admin')], db_index=True, default='SALES_EXPERT', max_length=24),
|
||||
),
|
||||
]
|
||||
+61
-61
@@ -1,61 +1,61 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import apps.core.models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('staff', '0003_staffprofile_role'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrganizationalUnit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=120, unique=True)),
|
||||
('slug', models.SlugField(max_length=80, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('permissions', models.ManyToManyField(blank=True, help_text='مجوزهایی که به تمام اعضای فعال این واحد داده می\u200cشود.', related_name='organizational_units', to='auth.permission')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'واحد سازمانی',
|
||||
'verbose_name_plural': 'واحدهای سازمانی',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='primary_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='primary_staff', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UnitMembership',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_primary', models.BooleanField(default=False)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('permissions', models.ManyToManyField(blank=True, help_text='مجوزهای اضافه مخصوص این عضو در همین واحد.', related_name='unit_memberships', to='auth.permission')),
|
||||
('unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='staff.organizationalunit')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_memberships', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'عضویت واحد',
|
||||
'verbose_name_plural': 'عضویت\u200cهای واحد',
|
||||
'ordering': ['unit__name', 'user__username'],
|
||||
'constraints': [models.UniqueConstraint(fields=('user', 'unit'), name='unique_unit_membership')],
|
||||
},
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import apps.core.models
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
('staff', '0003_staffprofile_role'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='OrganizationalUnit',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=120, unique=True)),
|
||||
('slug', models.SlugField(max_length=80, unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('permissions', models.ManyToManyField(blank=True, help_text='مجوزهایی که به تمام اعضای فعال این واحد داده می\u200cشود.', related_name='organizational_units', to='auth.permission')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'واحد سازمانی',
|
||||
'verbose_name_plural': 'واحدهای سازمانی',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='staffprofile',
|
||||
name='primary_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='primary_staff', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UnitMembership',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('is_primary', models.BooleanField(default=False)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
('permissions', models.ManyToManyField(blank=True, help_text='مجوزهای اضافه مخصوص این عضو در همین واحد.', related_name='unit_memberships', to='auth.permission')),
|
||||
('unit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='memberships', to='staff.organizationalunit')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='unit_memberships', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'عضویت واحد',
|
||||
'verbose_name_plural': 'عضویت\u200cهای واحد',
|
||||
'ordering': ['unit__name', 'user__username'],
|
||||
'constraints': [models.UniqueConstraint(fields=('user', 'unit'), name='unique_unit_membership')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='accepts_external_tickets',
|
||||
field=models.BooleanField(default=True, help_text='اگر غیرفعال باشد، مشتریان نمی\u200cتوانند مستقیماً برای این واحد تیکت ثبت کنند.', verbose_name='قابل انتخاب برای تیکت مشتری'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='accepts_external_tickets',
|
||||
field=models.BooleanField(default=True, help_text='اگر غیرفعال باشد، مشتریان نمی\u200cتوانند مستقیماً برای این واحد تیکت ثبت کنند.', verbose_name='قابل انتخاب برای تیکت مشتری'),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +1,27 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0005_organizationalunit_accepts_external_tickets'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='organizationalunit',
|
||||
options={'ordering': ['sort_order', 'name', 'pk'], 'verbose_name': 'واحد سازمانی', 'verbose_name_plural': 'واحدهای سازمانی'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='search_keywords',
|
||||
field=models.TextField(blank=True, help_text='واژه\u200cها و عبارت\u200cهای مترادف را با فاصله یا ویرگول وارد کنید.', verbose_name='کلیدواژه\u200cهای جستجو'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='ترتیب نمایش'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0005_organizationalunit_accepts_external_tickets'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='organizationalunit',
|
||||
options={'ordering': ['sort_order', 'name', 'pk'], 'verbose_name': 'واحد سازمانی', 'verbose_name_plural': 'واحدهای سازمانی'},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='search_keywords',
|
||||
field=models.TextField(blank=True, help_text='واژه\u200cها و عبارت\u200cهای مترادف را با فاصله یا ویرگول وارد کنید.', verbose_name='کلیدواژه\u200cهای جستجو'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='ترتیب نمایش'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0006_alter_organizationalunit_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='show_in_customer_form',
|
||||
field=models.BooleanField(default=True, help_text='در صورت خاموش بودن، واحد در فرم ثبت تیکت و جستجوی مشتری نمایش داده نمی\u200cشود.', verbose_name='نمایش در فرم مشتری'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0006_alter_organizationalunit_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='organizationalunit',
|
||||
name='show_in_customer_form',
|
||||
field=models.BooleanField(default=True, help_text='در صورت خاموش بودن، واحد در فرم ثبت تیکت و جستجوی مشتری نمایش داده نمی\u200cشود.', verbose_name='نمایش در فرم مشتری'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-10 05:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0008_unitmembership_ticket_scope_and_roles'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='staffprofile',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, help_text='عدد کمتر، کارشناس را زودتر در فهرست همان واحد نمایش می\u200cدهد.', verbose_name='ترتیب نمایش در انتخاب کارشناس'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-10 05:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0008_unitmembership_ticket_scope_and_roles'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='staffprofile',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, help_text='عدد کمتر، کارشناس را زودتر در فهرست همان واحد نمایش می\u200cدهد.', verbose_name='ترتیب نمایش در انتخاب کارشناس'),
|
||||
),
|
||||
]
|
||||
@@ -1,130 +1,130 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('messaging', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SLAConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('priority', models.CharField(max_length=15, unique=True)),
|
||||
('first_response_minutes', models.PositiveIntegerField()),
|
||||
('resolution_minutes', models.PositiveIntegerField()),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('icon', models.CharField(blank=True, max_length=50)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['sort_order', 'title'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ticket',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('ticket_number', models.CharField(editable=False, max_length=30, unique=True)),
|
||||
('subject', models.CharField(max_length=200, validators=[django.core.validators.MinLengthValidator(5)])),
|
||||
('status', models.CharField(choices=[('NEW', 'جدید'), ('OPEN', 'باز'), ('WAITING_CUSTOMER', 'در انتظار مشتری'), ('WAITING_STAFF', 'در انتظار کارشناس'), ('RESOLVED', 'حل\u200cشده'), ('CLOSED', 'بسته'), ('REOPENED', 'بازگشایی\u200cشده')], db_index=True, default='NEW', max_length=25)),
|
||||
('priority', models.CharField(choices=[('NORMAL', 'عادی'), ('IMPORTANT', 'مهم'), ('URGENT', 'فوری')], db_index=True, default='NORMAL', max_length=15)),
|
||||
('origin_visibility', models.CharField(choices=[('PUBLIC', 'عمومی'), ('SELECTED', 'کارشناس انتخابی')], default='PUBLIC', max_length=15)),
|
||||
('last_activity_at', models.DateTimeField(db_index=True)),
|
||||
('first_response_at', models.DateTimeField(blank=True, null=True)),
|
||||
('last_customer_message_at', models.DateTimeField(blank=True, null=True)),
|
||||
('last_staff_message_at', models.DateTimeField(blank=True, null=True)),
|
||||
('first_response_due_at', models.DateTimeField(blank=True, null=True)),
|
||||
('resolution_due_at', models.DateTimeField(blank=True, null=True)),
|
||||
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
||||
('closed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('reopened_at', models.DateTimeField(blank=True, null=True)),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='customer_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
('selected_expert', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='selected_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tickets', to='tickets.ticketcategory')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-last_activity_at'],
|
||||
'permissions': [('view_all_tickets', 'Can view all tickets'), ('claim_ticket', 'Can claim ticket'), ('transfer_ticket', 'Can transfer ticket'), ('share_ticket', 'Can share ticket'), ('change_ticket_priority', 'Can change ticket priority'), ('view_customer_history', 'Can view customer history')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketEvent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event_type', models.CharField(choices=[('CREATED', 'ایجاد'), ('CLAIMED', 'برداشت'), ('TRANSFERRED', 'انتقال'), ('SHARED', 'اشتراک'), ('ACCESS_REVOKED', 'لغو دسترسی'), ('STATUS_CHANGED', 'تغییر وضعیت'), ('PRIORITY_CHANGED', 'تغییر اولویت'), ('REOPENED', 'بازگشایی'), ('RESOLVED', 'حل'), ('CLOSED', 'بستن')], max_length=30)),
|
||||
('metadata', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('actor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='tickets.ticket')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketReadState',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_read_at', models.DateTimeField()),
|
||||
('last_read_message', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='messaging.message')),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.ticket')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketAccessGrant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('permission_level', models.CharField(choices=[('VIEW_ONLY', 'فقط مشاهده'), ('REPLY', 'پاسخ'), ('FULL', 'کامل')], default='FULL', max_length=15)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('revoked_at', models.DateTimeField(blank=True, null=True)),
|
||||
('granted_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='granted_ticket_access', to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_grants', to='tickets.ticket')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_grants', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'constraints': [models.UniqueConstraint(condition=models.Q(('revoked_at__isnull', True)), fields=('ticket', 'user'), name='unique_active_ticket_grant')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['status', 'last_activity_at'], name='tickets_tic_status_dbef56_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['owner', 'status'], name='tickets_tic_owner_i_70fd59_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['selected_expert', 'status'], name='tickets_tic_selecte_71a761_idx'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='ticketreadstate',
|
||||
constraint=models.UniqueConstraint(fields=('ticket', 'user'), name='unique_ticket_read_state'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-05 07:03
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('messaging', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SLAConfig',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('priority', models.CharField(max_length=15, unique=True)),
|
||||
('first_response_minutes', models.PositiveIntegerField()),
|
||||
('resolution_minutes', models.PositiveIntegerField()),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketCategory',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('title', models.CharField(max_length=100)),
|
||||
('slug', models.SlugField(unique=True)),
|
||||
('description', models.TextField(blank=True)),
|
||||
('icon', models.CharField(blank=True, max_length=50)),
|
||||
('sort_order', models.PositiveIntegerField(default=0)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'ordering': ['sort_order', 'title'],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Ticket',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('ticket_number', models.CharField(editable=False, max_length=30, unique=True)),
|
||||
('subject', models.CharField(max_length=200, validators=[django.core.validators.MinLengthValidator(5)])),
|
||||
('status', models.CharField(choices=[('NEW', 'جدید'), ('OPEN', 'باز'), ('WAITING_CUSTOMER', 'در انتظار مشتری'), ('WAITING_STAFF', 'در انتظار کارشناس'), ('RESOLVED', 'حل\u200cشده'), ('CLOSED', 'بسته'), ('REOPENED', 'بازگشایی\u200cشده')], db_index=True, default='NEW', max_length=25)),
|
||||
('priority', models.CharField(choices=[('NORMAL', 'عادی'), ('IMPORTANT', 'مهم'), ('URGENT', 'فوری')], db_index=True, default='NORMAL', max_length=15)),
|
||||
('origin_visibility', models.CharField(choices=[('PUBLIC', 'عمومی'), ('SELECTED', 'کارشناس انتخابی')], default='PUBLIC', max_length=15)),
|
||||
('last_activity_at', models.DateTimeField(db_index=True)),
|
||||
('first_response_at', models.DateTimeField(blank=True, null=True)),
|
||||
('last_customer_message_at', models.DateTimeField(blank=True, null=True)),
|
||||
('last_staff_message_at', models.DateTimeField(blank=True, null=True)),
|
||||
('first_response_due_at', models.DateTimeField(blank=True, null=True)),
|
||||
('resolution_due_at', models.DateTimeField(blank=True, null=True)),
|
||||
('resolved_at', models.DateTimeField(blank=True, null=True)),
|
||||
('closed_at', models.DateTimeField(blank=True, null=True)),
|
||||
('reopened_at', models.DateTimeField(blank=True, null=True)),
|
||||
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='customer_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
('owner', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
('selected_expert', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='selected_tickets', to=settings.AUTH_USER_MODEL)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='tickets', to='tickets.ticketcategory')),
|
||||
],
|
||||
options={
|
||||
'ordering': ['-last_activity_at'],
|
||||
'permissions': [('view_all_tickets', 'Can view all tickets'), ('claim_ticket', 'Can claim ticket'), ('transfer_ticket', 'Can transfer ticket'), ('share_ticket', 'Can share ticket'), ('change_ticket_priority', 'Can change ticket priority'), ('view_customer_history', 'Can view customer history')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketEvent',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('event_type', models.CharField(choices=[('CREATED', 'ایجاد'), ('CLAIMED', 'برداشت'), ('TRANSFERRED', 'انتقال'), ('SHARED', 'اشتراک'), ('ACCESS_REVOKED', 'لغو دسترسی'), ('STATUS_CHANGED', 'تغییر وضعیت'), ('PRIORITY_CHANGED', 'تغییر اولویت'), ('REOPENED', 'بازگشایی'), ('RESOLVED', 'حل'), ('CLOSED', 'بستن')], max_length=30)),
|
||||
('metadata', models.JSONField(blank=True, default=dict)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('actor', models.ForeignKey(null=True, on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='events', to='tickets.ticket')),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketReadState',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('last_read_at', models.DateTimeField()),
|
||||
('last_read_message', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='messaging.message')),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='tickets.ticket')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TicketAccessGrant',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('permission_level', models.CharField(choices=[('VIEW_ONLY', 'فقط مشاهده'), ('REPLY', 'پاسخ'), ('FULL', 'کامل')], default='FULL', max_length=15)),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('revoked_at', models.DateTimeField(blank=True, null=True)),
|
||||
('granted_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='granted_ticket_access', to=settings.AUTH_USER_MODEL)),
|
||||
('ticket', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='access_grants', to='tickets.ticket')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ticket_grants', to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'constraints': [models.UniqueConstraint(condition=models.Q(('revoked_at__isnull', True)), fields=('ticket', 'user'), name='unique_active_ticket_grant')],
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['status', 'last_activity_at'], name='tickets_tic_status_dbef56_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['owner', 'status'], name='tickets_tic_owner_i_70fd59_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['selected_expert', 'status'], name='tickets_tic_selecte_71a761_idx'),
|
||||
),
|
||||
migrations.AddConstraint(
|
||||
model_name='ticketreadstate',
|
||||
constraint=models.UniqueConstraint(fields=('ticket', 'user'), name='unique_ticket_read_state'),
|
||||
),
|
||||
]
|
||||
+102
-102
@@ -1,102 +1,102 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import apps.core.models
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0003_contactgroup_customerprofile_organization_name_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
('tickets', '0003_ticket_city_ticket_province'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TicketTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=80, unique=True)),
|
||||
('slug', models.SlugField(max_length=80, unique=True)),
|
||||
('color', models.CharField(default='#52677d', max_length=7)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'برچسب تیکت',
|
||||
'verbose_name_plural': 'برچسب\u200cهای تیکت',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='assigned_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assigned_tickets', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='created_tickets', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='delivery_type',
|
||||
field=models.CharField(choices=[('INDIVIDUAL', 'فردی'), ('GROUP', 'ارسال گروهی')], db_index=True, default='INDIVIDUAL', max_length=12),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='owning_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_tickets', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupDispatch',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('subject', models.CharField(max_length=200, validators=[django.core.validators.MinLengthValidator(5)])),
|
||||
('body', models.TextField()),
|
||||
('status', models.CharField(choices=[('DRAFT', 'پیش\u200cنویس'), ('QUEUED', 'در صف ارسال'), ('SENT', 'ارسال\u200cشده'), ('PARTIAL', 'ارسال ناقص'), ('CANCELLED', 'لغوشده')], db_index=True, default='DRAFT', max_length=12)),
|
||||
('recipient_count', models.PositiveIntegerField(default=0)),
|
||||
('dispatched_at', models.DateTimeField(blank=True, null=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tickets.ticketcategory')),
|
||||
('contact_group', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='dispatches', to='customers.contactgroup')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='created_group_dispatches', to=settings.AUTH_USER_MODEL)),
|
||||
('unit', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='group_dispatches', to='staff.organizationalunit')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ارسال گروهی',
|
||||
'verbose_name_plural': 'ارسال\u200cهای گروهی',
|
||||
'ordering': ['-created_at'],
|
||||
'permissions': [('dispatch_group_tickets', 'Can dispatch isolated tickets to a contact group')],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='group_dispatch',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tickets', to='tickets.groupdispatch'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, related_name='tickets', to='tickets.tickettag'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['owning_unit', 'status'], name='tickets_tic_owning__e629e3_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['delivery_type', 'last_activity_at'], name='tickets_tic_deliver_c53084_idx'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-08 09:57
|
||||
|
||||
import apps.core.models
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('customers', '0003_contactgroup_customerprofile_organization_name_and_more'),
|
||||
('staff', '0004_organizationalunit_staffprofile_primary_unit_and_more'),
|
||||
('tickets', '0003_ticket_city_ticket_province'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='TicketTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('name', models.CharField(max_length=80, unique=True)),
|
||||
('slug', models.SlugField(max_length=80, unique=True)),
|
||||
('color', models.CharField(default='#52677d', max_length=7)),
|
||||
('is_active', models.BooleanField(default=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'برچسب تیکت',
|
||||
'verbose_name_plural': 'برچسب\u200cهای تیکت',
|
||||
'ordering': ['name'],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='assigned_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='assigned_tickets', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='created_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='created_tickets', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='delivery_type',
|
||||
field=models.CharField(choices=[('INDIVIDUAL', 'فردی'), ('GROUP', 'ارسال گروهی')], db_index=True, default='INDIVIDUAL', max_length=12),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='owning_unit',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='owned_tickets', to='staff.organizationalunit'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='GroupDispatch',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
('public_id', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||
('subject', models.CharField(max_length=200, validators=[django.core.validators.MinLengthValidator(5)])),
|
||||
('body', models.TextField()),
|
||||
('status', models.CharField(choices=[('DRAFT', 'پیش\u200cنویس'), ('QUEUED', 'در صف ارسال'), ('SENT', 'ارسال\u200cشده'), ('PARTIAL', 'ارسال ناقص'), ('CANCELLED', 'لغوشده')], db_index=True, default='DRAFT', max_length=12)),
|
||||
('recipient_count', models.PositiveIntegerField(default=0)),
|
||||
('dispatched_at', models.DateTimeField(blank=True, null=True)),
|
||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='tickets.ticketcategory')),
|
||||
('contact_group', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='dispatches', to='customers.contactgroup')),
|
||||
('created_by', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='created_group_dispatches', to=settings.AUTH_USER_MODEL)),
|
||||
('unit', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='group_dispatches', to='staff.organizationalunit')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'ارسال گروهی',
|
||||
'verbose_name_plural': 'ارسال\u200cهای گروهی',
|
||||
'ordering': ['-created_at'],
|
||||
'permissions': [('dispatch_group_tickets', 'Can dispatch isolated tickets to a contact group')],
|
||||
},
|
||||
bases=(apps.core.models.PersianTextNormalizationMixin, models.Model),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='group_dispatch',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='tickets', to='tickets.groupdispatch'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticket',
|
||||
name='tags',
|
||||
field=models.ManyToManyField(blank=True, related_name='tickets', to='tickets.tickettag'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['owning_unit', 'status'], name='tickets_tic_owning__e629e3_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='ticket',
|
||||
index=models.Index(fields=['delivery_type', 'last_activity_at'], name='tickets_tic_deliver_c53084_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +1,24 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0005_organizationalunit_accepts_external_tickets'),
|
||||
('tickets', '0006_assign_legacy_contacts_to_sales'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ticketcategory',
|
||||
options={'ordering': ['unit__name', 'sort_order', 'title']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketcategory',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, help_text='برای دسته\u200cهای قدیمی عمومی می\u200cتواند خالی باشد؛ دسته\u200cهای جدید باید واحد داشته باشند.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ticket_categories', to='staff.organizationalunit', verbose_name='واحد سازمانی'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 05:43
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('staff', '0005_organizationalunit_accepts_external_tickets'),
|
||||
('tickets', '0006_assign_legacy_contacts_to_sales'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ticketcategory',
|
||||
options={'ordering': ['unit__name', 'sort_order', 'title']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketcategory',
|
||||
name='unit',
|
||||
field=models.ForeignKey(blank=True, help_text='برای دسته\u200cهای قدیمی عمومی می\u200cتواند خالی باشد؛ دسته\u200cهای جدید باید واحد داشته باشند.', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='ticket_categories', to='staff.organizationalunit', verbose_name='واحد سازمانی'),
|
||||
),
|
||||
]
|
||||
@@ -1,28 +1,28 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:31
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0009_seed_customer_facing_units_and_categories'),
|
||||
('staff', '0006_alter_organizationalunit_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ticketcategory',
|
||||
options={'ordering': ['unit__sort_order', 'unit__name', 'sort_order', 'title', 'pk']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketcategory',
|
||||
name='search_keywords',
|
||||
field=models.TextField(blank=True, help_text='مترادف\u200cها و عبارت\u200cهای رایج مشتری را وارد کنید.', verbose_name='کلیدواژه\u200cهای جستجو'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcategory',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='ترتیب نمایش'),
|
||||
),
|
||||
]
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='ticketcategory',
|
||||
options={'ordering': ['unit__sort_order', 'unit__name', 'sort_order', 'title', 'pk']},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ticketcategory',
|
||||
name='search_keywords',
|
||||
field=models.TextField(blank=True, help_text='مترادف\u200cها و عبارت\u200cهای رایج مشتری را وارد کنید.', verbose_name='کلیدواژه\u200cهای جستجو'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ticketcategory',
|
||||
name='sort_order',
|
||||
field=models.PositiveIntegerField(db_index=True, default=0, verbose_name='ترتیب نمایش'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +1,18 @@
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0010_alter_ticketcategory_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticketcategory',
|
||||
name='show_in_customer_form',
|
||||
field=models.BooleanField(default=True, help_text='در صورت خاموش بودن، این دسته در انتخاب و جستجوی مشتری دیده نمی\u200cشود.', verbose_name='نمایش در فرم مشتری'),
|
||||
),
|
||||
]
|
||||
# Generated by Django 5.1.1 on 2026-09-09 09:40
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('tickets', '0010_alter_ticketcategory_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ticketcategory',
|
||||
name='show_in_customer_form',
|
||||
field=models.BooleanField(default=True, help_text='در صورت خاموش بودن، این دسته در انتخاب و جستجوی مشتری دیده نمی\u200cشود.', verbose_name='نمایش در فرم مشتری'),
|
||||
),
|
||||
]
|
||||
@@ -1,4 +1,4 @@
|
||||
from django.urls import path
|
||||
from . import views
|
||||
app_name="staff_tickets"
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("dispatches/<uuid:public_id>/resolve/",views.staff_dispatch_resolve,name="dispatch_resolve"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
urlpatterns=[path("",views.staff_list,name="list"),path("customers/",views.staff_customers,name="customers"),path("customers/search/",views.staff_customer_search,name="customer_search"),path("contacts/",views.staff_contacts,name="contacts"),path("contacts/new/",views.staff_contact_create,name="contact_create"),path("contacts/<uuid:public_id>/edit/",views.staff_contact_edit,name="contact_edit"),path("contact-groups/",views.staff_groups,name="groups"),path("contact-groups/<int:group_id>/",views.staff_groups,name="group_edit"),path("magic-links/",views.staff_magic_audit,name="magic_audit"),path("magic-links/<int:link_id>/revoke/",views.staff_magic_revoke,name="magic_revoke"),path("sms-templates/",views.staff_sms_templates,name="sms_templates"),path("sms-templates/<int:template_id>/",views.staff_sms_templates,name="sms_template_edit"),path("dispatches/",views.staff_dispatch_create,name="dispatch_create"),path("dispatches/<uuid:public_id>/",views.staff_dispatch_detail,name="dispatch_detail"),path("dispatches/<uuid:public_id>/retry/",views.staff_dispatch_retry,name="dispatch_retry"),path("dispatches/<uuid:public_id>/resolve/",views.staff_dispatch_resolve,name="dispatch_resolve"),path("experts/<uuid:public_id>/avatar/",views.expert_avatar,name="expert_avatar"),path("tickets/",views.staff_list,name="tickets"),path("tickets/new/",views.staff_create,name="create"),path("tickets/<uuid:public_id>/messages/older/",views.staff_messages,name="messages"),path("tickets/<uuid:public_id>/messages/poll/",views.staff_poll,name="poll"),path("tickets/<uuid:public_id>/sms/<int:job_id>/retry/",views.staff_sms_retry,name="sms_retry"),path("tickets/<uuid:public_id>/",views.staff_detail,name="detail"),path("tickets/<uuid:public_id>/claim/",views.staff_claim,name="claim"),path("tickets/<uuid:public_id>/reply/",views.staff_reply,name="reply"),path("tickets/<uuid:public_id>/note/",lambda r,public_id:views.staff_reply(r,public_id,note=True),name="note"),path("tickets/<uuid:public_id>/transfer/",views.staff_transfer,name="transfer"),path("tickets/<uuid:public_id>/share/",views.staff_share,name="share"),path("tickets/<uuid:public_id>/revoke/<uuid:user_id>/",views.staff_revoke,name="revoke"),path("tickets/<uuid:public_id>/status/",views.staff_status,name="status"),path("tickets/<uuid:public_id>/priority/",views.staff_priority,name="priority"),path("tickets/<uuid:public_id>/delete/",views.staff_delete,name="delete")]
|
||||
+62
-9
@@ -33,6 +33,53 @@ from .policies import DispatchPolicy, TicketPolicy
|
||||
from .selectors import customer_tickets,dashboard_counts,filter_staff_tickets,staff_ticket_rows
|
||||
from .services import change_priority,change_status,claim_ticket,create_ticket,dispatch_group_tickets,refresh_group_dispatch_status,reply_ticket,resolve_group_dispatch_tickets,revoke_access,share_ticket,transfer_ticket
|
||||
MESSAGE_PAGE_SIZE=50
|
||||
SMS_RETRYABLE_DELIVERY_STATUSES=(
|
||||
NotificationJob.DeliveryStatus.UNDELIVERED,
|
||||
NotificationJob.DeliveryStatus.EXPIRED,
|
||||
NotificationJob.DeliveryStatus.BLOCKED,
|
||||
NotificationJob.DeliveryStatus.UNKNOWN,
|
||||
NotificationJob.DeliveryStatus.ERROR,
|
||||
)
|
||||
|
||||
|
||||
def _retryable_sms_jobs(jobs):
|
||||
return jobs.filter(
|
||||
Q(status=NotificationJob.Status.FAILED)
|
||||
| Q(
|
||||
status=NotificationJob.Status.SENT,
|
||||
delivery_status__in=SMS_RETRYABLE_DELIVERY_STATUSES,
|
||||
delivery_next_check_at__isnull=True,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _sms_job_is_retryable(job):
|
||||
return job.status == NotificationJob.Status.FAILED or (
|
||||
job.status == NotificationJob.Status.SENT
|
||||
and job.delivery_status in SMS_RETRYABLE_DELIVERY_STATUSES
|
||||
and job.delivery_next_check_at is None
|
||||
)
|
||||
|
||||
|
||||
def _requeue_retryable_sms_jobs(jobs):
|
||||
return _retryable_sms_jobs(jobs).update(
|
||||
status=NotificationJob.Status.PENDING,
|
||||
attempts=0,
|
||||
next_attempt_at=timezone.now(),
|
||||
last_error="",
|
||||
provider_message_id="",
|
||||
provider_status_code="",
|
||||
sent_at=None,
|
||||
delivery_status="",
|
||||
delivery_status_code="",
|
||||
delivery_description="",
|
||||
delivery_checked_at=None,
|
||||
delivery_check_attempts=0,
|
||||
delivery_next_check_at=None,
|
||||
delivery_is_final=False,
|
||||
)
|
||||
|
||||
|
||||
def conversation_page(queryset,before=None):
|
||||
queryset=queryset.order_by("-created_at","-id")
|
||||
if before:
|
||||
@@ -395,13 +442,25 @@ def staff_detail(request,public_id):
|
||||
mark_messages_read(conversation,request.user)
|
||||
last=conversation[-1] if conversation else None; TicketReadState.objects.update_or_create(ticket=ticket,user=request.user,defaults={"last_read_at":timezone.now(),"last_read_message":last})
|
||||
history=Ticket.objects.filter(customer=ticket.customer).exclude(pk=ticket.pk).select_related("owner__staff_profile","selected_expert__staff_profile").order_by("-last_activity_at")[:5]
|
||||
sms_jobs=ticket.notificationjob_set.filter(channel=NotificationJob.Channel.SMS).exclude(event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("-created_at")
|
||||
sms_jobs=list(ticket.notificationjob_set.filter(channel=NotificationJob.Channel.SMS).exclude(event_type="UNIT_SUPERVISOR_NEW_TICKET").order_by("-created_at"))
|
||||
can_retry_sms=TicketPolicy.can_reply(request.user,ticket)
|
||||
for sms_job in sms_jobs:
|
||||
sms_job.sms_retryable=can_retry_sms and _sms_job_is_retryable(sms_job)
|
||||
latest=conversation[-1] if conversation else None
|
||||
group_dispatch = ticket.group_dispatch
|
||||
can_resolve_dispatch = bool(group_dispatch and DispatchPolicy.can_view(request.user, group_dispatch) and (request.user.has_perm("tickets.change_ticket") or TicketPolicy.has_unit_permission(request.user, group_dispatch.unit, "tickets.change_ticket")))
|
||||
group_open_count = group_dispatch.tickets.exclude(status__in=(Ticket.Status.RESOLVED, Ticket.Status.CLOSED)).count() if group_dispatch else 0
|
||||
return render(request,"tickets/staff_detail.html",{"ticket":ticket,"conversation":conversation,"has_older":has_older,"older_cursor":older_cursor,"form":StaffReplyForm(),"transfer_form":TransferForm(),"share_form":ShareForm(),"can_claim":TicketPolicy.can_claim(request.user,ticket),"can_transfer":TicketPolicy.can_transfer(request.user,ticket),"can_share":TicketPolicy.can_share(request.user,ticket),"can_delete":TicketPolicy.can_delete(request.user,ticket),"can_resolve_dispatch":can_resolve_dispatch,"group_open_count":group_open_count,"statuses":Ticket.Status.choices,"priorities":Ticket.Priority.choices,"grants":ticket.access_grants.filter(revoked_at__isnull=True).select_related("user__staff_profile"),"customer_history":history,"sms_jobs":sms_jobs,"viewer_user":request.user,"poll_version":conversation_version(ticket,latest),"poll_cursor":latest.public_id if latest else ""})
|
||||
@staff_required
|
||||
@require_POST
|
||||
def staff_sms_retry(request,public_id,job_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
if not TicketPolicy.can_reply(request.user,ticket): raise PermissionDenied
|
||||
jobs=ticket.notificationjob_set.filter(pk=job_id,channel=NotificationJob.Channel.SMS).exclude(event_type="UNIT_SUPERVISOR_NEW_TICKET")
|
||||
count=_requeue_retryable_sms_jobs(jobs)
|
||||
messages.success(request,"پیامک ناموفق دوباره در صف ارسال قرار گرفت." if count else "این پیامک در وضعیت قابل تلاش مجدد نیست.")
|
||||
return redirect("staff_tickets:detail",public_id=public_id)
|
||||
@staff_required
|
||||
def staff_messages(request,public_id):
|
||||
ticket=get_object_or_404(Ticket,public_id=public_id)
|
||||
if not TicketPolicy.can_view(request.user,ticket): raise PermissionDenied
|
||||
@@ -640,20 +699,14 @@ def staff_dispatch_resolve(request, public_id):
|
||||
def staff_dispatch_retry(request, public_id):
|
||||
dispatch=get_object_or_404(GroupDispatch,public_id=public_id)
|
||||
if not DispatchPolicy.can_view(request.user,dispatch) or not DispatchPolicy.can_create(request.user,dispatch.unit,dispatch.operation_type): raise PermissionDenied
|
||||
retryable_delivery = [NotificationJob.DeliveryStatus.UNDELIVERED, NotificationJob.DeliveryStatus.EXPIRED, NotificationJob.DeliveryStatus.BLOCKED, NotificationJob.DeliveryStatus.UNKNOWN, NotificationJob.DeliveryStatus.ERROR]
|
||||
jobs=NotificationJob.objects.filter(ticket__group_dispatch=dispatch,channel=NotificationJob.Channel.SMS,event_type=SMSTemplate.Event.STAFF_TICKET_CREATED).filter(Q(status=NotificationJob.Status.FAILED) | Q(status=NotificationJob.Status.SENT,delivery_status__in=retryable_delivery,delivery_next_check_at__isnull=True))
|
||||
jobs=NotificationJob.objects.filter(ticket__group_dispatch=dispatch,channel=NotificationJob.Channel.SMS,event_type=SMSTemplate.Event.STAFF_TICKET_CREATED)
|
||||
job_id=request.POST.get("job_id","").strip()
|
||||
if job_id:
|
||||
if not job_id.isdigit():
|
||||
messages.error(request,"شناسه پیامک برای تلاش مجدد معتبر نیست.")
|
||||
return redirect("staff_tickets:dispatch_detail",public_id=public_id)
|
||||
jobs=jobs.filter(pk=int(job_id))
|
||||
count=jobs.update(
|
||||
status=NotificationJob.Status.PENDING, attempts=0, next_attempt_at=timezone.now(),
|
||||
last_error="", provider_message_id="", provider_status_code="", sent_at=None,
|
||||
delivery_status="", delivery_status_code="", delivery_description="",
|
||||
delivery_checked_at=None, delivery_check_attempts=0, delivery_next_check_at=None,
|
||||
)
|
||||
count=_requeue_retryable_sms_jobs(jobs)
|
||||
refresh_group_dispatch_status(dispatch.pk)
|
||||
messages.success(request,f"{count} ارسال ناموفق دوباره در صف قرار گرفت." if count else "ارسال ناموفقی برای تلاش مجدد وجود ندارد.")
|
||||
return redirect("staff_tickets:dispatch_detail",public_id=public_id)
|
||||
|
||||
Reference in new issue
Block a user