first
This commit is contained in:
commit
fa9d154db9
260 files changed
+17643
No files matched your search
@@ -0,0 +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'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,6 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0001_initial")]
|
||||
operations = [migrations.AlterField(model_name="ticketreadstate", name="ticket", field=models.ForeignKey(on_delete=models.deletion.CASCADE, related_name="read_states", to="tickets.ticket"))]
|
||||
@@ -0,0 +1,9 @@
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0002_alter_ticketreadstate_ticket")]
|
||||
operations = [
|
||||
migrations.AddField(model_name="ticket", name="city", field=models.CharField(blank=True, max_length=100)),
|
||||
migrations.AddField(model_name="ticket", name="province", field=models.CharField(blank=True, max_length=100)),
|
||||
]
|
||||
+102
@@ -0,0 +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'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,53 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
BASE_UNIT_PERMISSIONS = {
|
||||
"tickets": {"view_ticket", "add_ticket", "change_ticket", "claim_ticket", "transfer_ticket", "share_ticket", "change_ticket_priority", "dispatch_group_tickets"},
|
||||
"messaging": {"add_message", "view_message"},
|
||||
"customers": {"view_customerprofile", "add_customerprofile", "change_customerprofile", "view_contactgroup", "add_contactgroup", "change_contactgroup"},
|
||||
"notifications": {"view_smstemplate", "add_smstemplate", "change_smstemplate"},
|
||||
}
|
||||
AUDIT_ROLES = {"SALES_MANAGER", "CEO", "GENERAL_ADMIN"}
|
||||
|
||||
|
||||
def bootstrap_legacy_staff(apps, schema_editor):
|
||||
"""Give pre-RBAC staff a safe editable default unit during an in-place upgrade."""
|
||||
StaffProfile = apps.get_model("staff", "StaffProfile")
|
||||
if not StaffProfile.objects.exists():
|
||||
return
|
||||
OrganizationalUnit = apps.get_model("staff", "OrganizationalUnit")
|
||||
UnitMembership = apps.get_model("staff", "UnitMembership")
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
unit, _ = OrganizationalUnit.objects.get_or_create(
|
||||
slug="sales",
|
||||
defaults={"name": "فروش", "description": "واحد پیشفرض برای کارشناسان موجود پیش از راهاندازی RBAC"},
|
||||
)
|
||||
query = None
|
||||
from django.db.models import Q
|
||||
for app_label, codenames in BASE_UNIT_PERMISSIONS.items():
|
||||
part = Q(content_type__app_label=app_label, codename__in=codenames)
|
||||
query = part if query is None else query | part
|
||||
unit.permissions.add(*Permission.objects.filter(query))
|
||||
audit_permission = Permission.objects.filter(content_type__app_label="accounts", codename="view_ticketmagiclink").first()
|
||||
for profile in StaffProfile.objects.select_related("user"):
|
||||
if profile.primary_unit_id is None:
|
||||
profile.primary_unit_id = unit.pk
|
||||
profile.save(update_fields=["primary_unit"])
|
||||
membership, _ = UnitMembership.objects.get_or_create(user_id=profile.user_id, unit=unit, defaults={"is_primary": True, "is_active": True})
|
||||
changed = False
|
||||
if not membership.is_active: membership.is_active = True; changed = True
|
||||
if profile.primary_unit_id == unit.pk and not membership.is_primary: membership.is_primary = True; changed = True
|
||||
if changed: membership.save(update_fields=["is_active", "is_primary", "updated_at"])
|
||||
if audit_permission and (profile.role in AUDIT_ROLES or profile.user.is_superuser):
|
||||
membership.permissions.add(audit_permission)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0004_ticketmagiclink_created_unit_and_more"),
|
||||
("customers", "0004_contactgroup_unit_customerprofile_groups_and_more"),
|
||||
("notifications", "0006_alter_smstemplate_allowed_tags"),
|
||||
("staff", "0004_organizationalunit_staffprofile_primary_unit_and_more"),
|
||||
("tickets", "0004_tickettag_ticket_assigned_unit_ticket_created_by_and_more"),
|
||||
]
|
||||
operations = [migrations.RunPython(bootstrap_legacy_staff, migrations.RunPython.noop)]
|
||||
@@ -0,0 +1,21 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def assign_legacy_contacts(apps, schema_editor):
|
||||
OrganizationalUnit = apps.get_model("staff", "OrganizationalUnit")
|
||||
ContactGroup = apps.get_model("customers", "ContactGroup")
|
||||
CustomerProfile = apps.get_model("customers", "CustomerProfile")
|
||||
sales = OrganizationalUnit.objects.filter(slug="sales").first()
|
||||
if sales is None or not CustomerProfile.objects.exists():
|
||||
return
|
||||
group, _ = ContactGroup.objects.get_or_create(
|
||||
unit=sales,
|
||||
name="مخاطبان فروش",
|
||||
defaults={"description": "گروه پیشفرض مخاطبان موجود پیش از راهاندازی دفترچه سازمانی"},
|
||||
)
|
||||
group.contacts.add(*CustomerProfile.objects.filter(groups__isnull=True))
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0005_bootstrap_legacy_staff_unit")]
|
||||
operations = [migrations.RunPython(assign_legacy_contacts, migrations.RunPython.noop)]
|
||||
@@ -0,0 +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='واحد سازمانی'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,14 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def assign_categories(apps, schema_editor):
|
||||
Unit = apps.get_model("staff", "OrganizationalUnit")
|
||||
Category = apps.get_model("tickets", "TicketCategory")
|
||||
unit = Unit.objects.filter(is_active=True, slug="sales").first() or Unit.objects.filter(is_active=True).order_by("id").first()
|
||||
if unit:
|
||||
Category.objects.filter(unit__isnull=True).update(unit=unit)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0007_alter_ticketcategory_options_ticketcategory_unit")]
|
||||
operations = [migrations.RunPython(assign_categories, migrations.RunPython.noop)]
|
||||
@@ -0,0 +1,37 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
DEFAULT_UNITS = (
|
||||
("accounting", "حسابداری", (("financial-documents", "اسناد مالی"), ("payment-receipt", "فیش و رسید پرداخت"), ("invoice", "صورتحساب و فاکتور"))),
|
||||
("logistics", "بارگیری و لجستیک", (("shipping-quote", "استعلام حمل"), ("loading-status", "وضعیت بارگیری"), ("delivery-coordination", "هماهنگی تحویل"))),
|
||||
("warehouse", "انبار", (("inventory-request", "استعلام موجودی انبار"), ("warehouse-release", "مجوز خروج کالا"), ("warehouse-documents", "اسناد انبار"))),
|
||||
)
|
||||
|
||||
|
||||
def seed_units_and_categories(apps, schema_editor):
|
||||
Unit = apps.get_model("staff", "OrganizationalUnit")
|
||||
Category = apps.get_model("tickets", "TicketCategory")
|
||||
for unit_slug, unit_name, categories in DEFAULT_UNITS:
|
||||
unit, _ = Unit.objects.get_or_create(
|
||||
slug=unit_slug,
|
||||
defaults={"name": unit_name, "is_active": True, "accepts_external_tickets": True},
|
||||
)
|
||||
changed = []
|
||||
if not unit.is_active:
|
||||
unit.is_active = True
|
||||
changed.append("is_active")
|
||||
if not unit.accepts_external_tickets:
|
||||
unit.accepts_external_tickets = True
|
||||
changed.append("accepts_external_tickets")
|
||||
if changed:
|
||||
unit.save(update_fields=changed)
|
||||
for sort_order, (slug, title) in enumerate(categories, start=10):
|
||||
Category.objects.get_or_create(
|
||||
slug=slug,
|
||||
defaults={"unit": unit, "title": title, "sort_order": sort_order, "is_active": True},
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0008_assign_categories_to_default_unit")]
|
||||
operations = [migrations.RunPython(seed_units_and_categories, migrations.RunPython.noop)]
|
||||
@@ -0,0 +1,28 @@
|
||||
# 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='ترتیب نمایش'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +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='نمایش در فرم مشتری'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,124 @@
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
PEOPLE = [
|
||||
("kimiya_ghaffari", "کیمیا", "غفاری", "کیمیا غفاری", "SALES_EXPERT", "sales", "کارشناس فروش", "SELF"),
|
||||
("zahra_jamali", "زهرا", "جمالی", "زهرا جمالی", "SALES_EXPERT", "sales", "کارشناس فروش", "SELF"),
|
||||
("maryam_fotovat", "مریم", "فتوت", "مریم فتوت", "SALES_SUPERVISOR", "sales", "سرپرست فروش", "UNIT"),
|
||||
("morteza_sokhaeifard", "مرتضی", "سخائی فرد", "مرتضی سخائی فرد", "SALES_MANAGER", "sales", "مدیر فروش", "UNIT"),
|
||||
("mostafa_sokhaeifard", "مصطفی", "سخائی فرد", "مصطفی سخائی فرد", "CEO", "executive", "مدیرعامل", "UNIT"),
|
||||
("ms_sabzali", "", "سبزعلی", "خانم سبزعلی", "WAREHOUSE_EXPERT", "warehouse-loading", "کارشناس بارگیری انبار", "UNIT"),
|
||||
("mehdi_azami", "مهدی", "عظمی", "آقای مهدی عظمی", "WAREHOUSE_EXPERT", "warehouse-loading", "کارشناس بارگیری انبار", "UNIT"),
|
||||
("fereshteh_yavari", "فرشته", "یاوری", "خانم فرشته یاوری", "FINANCE_EXPERT", "finance", "کارشناس مالی / حسابداری", "UNIT"),
|
||||
("fatemeh_sokhaei", "فاطمه", "سخایی", "خانم فاطمه سخایی", "FINANCE_EXPERT", "finance", "کارشناس مالی / حسابداری", "UNIT"),
|
||||
("rana_nobakht", "رعنا", "نوبخت", "خانم رعنا نوبخت", "FINANCE_EXPERT", "finance", "کارشناس مالی / حسابداری", "UNIT"),
|
||||
("mehdi_liaqat", "مهدی", "لیاقت", "آقای مهدی لیاقت", "FINANCE_EXPERT", "finance", "کارشناس مالی / حسابداری", "UNIT"),
|
||||
("aliasghar_sokhaeifard", "علی اصغر", "سخائی فرد", "علی اصغر سخائی فرد", "FINANCE_EXPERT", "finance", "کارشناس مالی / حسابداری", "UNIT"),
|
||||
]
|
||||
|
||||
|
||||
def seed_organization(apps, schema_editor):
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
Group = apps.get_model("auth", "Group")
|
||||
User = apps.get_model("accounts", "User")
|
||||
Unit = apps.get_model("staff", "OrganizationalUnit")
|
||||
Membership = apps.get_model("staff", "UnitMembership")
|
||||
Profile = apps.get_model("staff", "StaffProfile")
|
||||
Category = apps.get_model("tickets", "TicketCategory")
|
||||
ContactGroup = apps.get_model("customers", "ContactGroup")
|
||||
|
||||
unit_specs = {
|
||||
"sales": ("فروش", "امور فروش و ارتباط با مشتری", True),
|
||||
"warehouse-loading": ("بارگیری انبار", "بارگیری، انبار و ارسال", True),
|
||||
"finance": ("مالی / حسابداری", "واحد مالی و حسابداری (یک مفهوم سازمانی)", True),
|
||||
"executive": ("مدیریت ارشد", "مدیریت عامل", False),
|
||||
}
|
||||
units = {}
|
||||
for order, (slug, (name, description, external)) in enumerate(unit_specs.items(), 10):
|
||||
unit, _ = Unit.objects.update_or_create(slug=slug, defaults={"name": name, "description": description, "is_active": True, "accepts_external_tickets": external, "show_in_customer_form": external, "sort_order": order})
|
||||
units[slug] = unit
|
||||
|
||||
category_specs = [("sales", "عمومی فروش", "sales-general"), ("warehouse-loading", "بارگیری و ارسال", "warehouse-loading-general"), ("finance", "مالی و حسابداری", "finance-general")]
|
||||
for slug, title, category_slug in category_specs:
|
||||
Category.objects.update_or_create(slug=category_slug, defaults={"title": title, "unit": units[slug], "is_active": True, "show_in_customer_form": True})
|
||||
ContactGroup.objects.get_or_create(unit=units[slug], name=f"مخاطبان {units[slug].name}")
|
||||
|
||||
permission_specs = {
|
||||
("tickets", "groupdispatch"): [
|
||||
("create_bulk_dispatch", "Can create bulk ticket dispatches"),
|
||||
("view_all_bulk_dispatches", "Can view all bulk ticket dispatches"),
|
||||
("create_inquiry", "Can create inquiries"),
|
||||
("view_all_inquiries", "Can view all inquiries"),
|
||||
],
|
||||
}
|
||||
for (app_label, model), rows in permission_specs.items():
|
||||
content_type, _ = ContentType.objects.get_or_create(app_label=app_label, model=model)
|
||||
for codename, name in rows:
|
||||
Permission.objects.get_or_create(content_type=content_type, codename=codename, defaults={"name": name})
|
||||
|
||||
permission_models = {
|
||||
"tickets": {"view_ticket": "ticket", "add_ticket": "ticket", "claim_ticket": "ticket", "view_all_tickets": "ticket", "create_bulk_dispatch": "groupdispatch", "view_all_bulk_dispatches": "groupdispatch", "create_inquiry": "groupdispatch", "view_all_inquiries": "groupdispatch"},
|
||||
"messaging": {"add_message": "message"},
|
||||
"customers": {"view_customerprofile": "customerprofile", "add_customerprofile": "customerprofile", "change_customerprofile": "customerprofile", "view_contactgroup": "contactgroup", "add_contactgroup": "contactgroup", "change_contactgroup": "contactgroup"},
|
||||
}
|
||||
|
||||
def permission(app_label, codename):
|
||||
content_type, _ = ContentType.objects.get_or_create(app_label=app_label, model=permission_models[app_label][codename])
|
||||
item, _ = Permission.objects.get_or_create(content_type=content_type, codename=codename, defaults={"name": codename.replace("_", " ").title()})
|
||||
return item
|
||||
|
||||
unit_permissions = [
|
||||
("tickets", "view_ticket"), ("tickets", "add_ticket"), ("tickets", "claim_ticket"),
|
||||
("messaging", "add_message"),
|
||||
("customers", "view_customerprofile"), ("customers", "add_customerprofile"), ("customers", "change_customerprofile"),
|
||||
("customers", "view_contactgroup"), ("customers", "add_contactgroup"), ("customers", "change_contactgroup"),
|
||||
]
|
||||
for slug in ("sales", "warehouse-loading", "finance"):
|
||||
units[slug].permissions.add(*[permission(*item) for item in unit_permissions])
|
||||
|
||||
role_groups = {
|
||||
"SALES_EXPERT": "Sales Expert", "SALES_SUPERVISOR": "Sales Supervisor", "SALES_MANAGER": "Sales Manager",
|
||||
"WAREHOUSE_EXPERT": "Warehouse Expert", "FINANCE_EXPERT": "Finance Expert", "CEO": "CEO",
|
||||
}
|
||||
for username, first_name, last_name, display_name, role, unit_slug, job_title, scope in PEOPLE:
|
||||
user, _ = User.objects.update_or_create(username=username, defaults={"first_name": first_name, "last_name": last_name, "account_type": "STAFF", "is_active": True, "password": "!"})
|
||||
profile, _ = Profile.objects.update_or_create(user=user, defaults={"primary_unit": units[unit_slug], "role": role, "display_name": display_name, "first_name": first_name, "last_name": last_name, "job_title": job_title, "is_active": True})
|
||||
membership, _ = Membership.objects.update_or_create(user=user, unit=units[unit_slug], defaults={"is_primary": True, "is_active": True, "ticket_scope": scope})
|
||||
Membership.objects.filter(user=user).exclude(pk=membership.pk).delete()
|
||||
group, _ = Group.objects.get_or_create(name=role_groups[role])
|
||||
user.groups.add(group)
|
||||
|
||||
sabzali = User.objects.get(username="ms_sabzali")
|
||||
sabzali_membership = Membership.objects.get(user=sabzali, unit=units["warehouse-loading"])
|
||||
sabzali_membership.permissions.add(permission("tickets", "create_bulk_dispatch"), permission("tickets", "create_inquiry"))
|
||||
|
||||
ceo = User.objects.get(username="mostafa_sokhaeifard")
|
||||
ceo.user_permissions.add(
|
||||
permission("tickets", "view_all_tickets"), permission("tickets", "add_ticket"),
|
||||
permission("tickets", "create_bulk_dispatch"), permission("tickets", "view_all_bulk_dispatches"),
|
||||
permission("tickets", "create_inquiry"), permission("tickets", "view_all_inquiries"),
|
||||
)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("accounts", "0005_magiclinkaccesslog_magic_opened_idx_and_more"),
|
||||
("customers", "0004_contactgroup_unit_customerprofile_groups_and_more"),
|
||||
("messaging", "0003_messagereadreceipt"),
|
||||
("staff", "0008_unitmembership_ticket_scope_and_roles"),
|
||||
("tickets", "0011_ticketcategory_show_in_customer_form"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="groupdispatch",
|
||||
name="operation_type",
|
||||
field=models.CharField(choices=[("BULK", "ارسال گروهی تیکت"), ("INQUIRY", "استعلام")], db_index=True, default="BULK", max_length=12),
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name="groupdispatch",
|
||||
options={"ordering": ["-created_at"], "permissions": [("dispatch_group_tickets", "Can dispatch isolated tickets to a contact group"), ("create_bulk_dispatch", "Can create bulk ticket dispatches"), ("view_all_bulk_dispatches", "Can view all bulk ticket dispatches"), ("create_inquiry", "Can create inquiries"), ("view_all_inquiries", "Can view all inquiries")], "verbose_name": "ارسال گروهی", "verbose_name_plural": "ارسالهای گروهی"},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,75 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def merge_legacy_accounting_unit(apps, schema_editor):
|
||||
Unit = apps.get_model("staff", "OrganizationalUnit")
|
||||
Membership = apps.get_model("staff", "UnitMembership")
|
||||
StaffProfile = apps.get_model("staff", "StaffProfile")
|
||||
TicketCategory = apps.get_model("tickets", "TicketCategory")
|
||||
Ticket = apps.get_model("tickets", "Ticket")
|
||||
GroupDispatch = apps.get_model("tickets", "GroupDispatch")
|
||||
ContactGroup = apps.get_model("customers", "ContactGroup")
|
||||
CustomerProfile = apps.get_model("customers", "CustomerProfile")
|
||||
SMSTemplate = apps.get_model("notifications", "SMSTemplate")
|
||||
TicketMagicLink = apps.get_model("accounts", "TicketMagicLink")
|
||||
|
||||
legacy = Unit.objects.filter(slug="accounting").first()
|
||||
canonical = Unit.objects.filter(slug="finance").first()
|
||||
if legacy is None:
|
||||
return
|
||||
if canonical is None:
|
||||
legacy.slug = "finance"
|
||||
legacy.name = "مالی / حسابداری"
|
||||
legacy.description = "واحد مالی و حسابداری (یک مفهوم سازمانی)"
|
||||
legacy.is_active = True
|
||||
legacy.accepts_external_tickets = True
|
||||
legacy.show_in_customer_form = True
|
||||
legacy.save()
|
||||
return
|
||||
|
||||
canonical.permissions.add(*legacy.permissions.all())
|
||||
TicketCategory.objects.filter(unit=legacy).update(unit=canonical)
|
||||
Ticket.objects.filter(owning_unit=legacy).update(owning_unit=canonical)
|
||||
Ticket.objects.filter(assigned_unit=legacy).update(assigned_unit=canonical)
|
||||
GroupDispatch.objects.filter(unit=legacy).update(unit=canonical)
|
||||
TicketMagicLink.objects.filter(created_unit=legacy).update(created_unit=canonical)
|
||||
StaffProfile.objects.filter(primary_unit=legacy).update(primary_unit=canonical)
|
||||
|
||||
for membership in Membership.objects.filter(unit=legacy):
|
||||
existing = Membership.objects.filter(user_id=membership.user_id, unit=canonical).first()
|
||||
if existing is None:
|
||||
membership.unit = canonical
|
||||
membership.save(update_fields=["unit", "updated_at"])
|
||||
continue
|
||||
existing.permissions.add(*membership.permissions.all())
|
||||
existing.is_active = existing.is_active or membership.is_active
|
||||
existing.is_primary = existing.is_primary or membership.is_primary
|
||||
if membership.ticket_scope == "UNIT":
|
||||
existing.ticket_scope = "UNIT"
|
||||
existing.save(update_fields=["is_active", "is_primary", "ticket_scope", "updated_at"])
|
||||
membership.delete()
|
||||
|
||||
for group in ContactGroup.objects.filter(unit=legacy):
|
||||
existing = ContactGroup.objects.filter(unit=canonical, name=group.name).first()
|
||||
if existing is None:
|
||||
group.unit = canonical
|
||||
group.save(update_fields=["unit", "updated_at"])
|
||||
continue
|
||||
for profile in CustomerProfile.objects.filter(groups=group):
|
||||
profile.groups.add(existing)
|
||||
GroupDispatch.objects.filter(contact_group=group).update(contact_group=existing)
|
||||
group.delete()
|
||||
|
||||
for template in SMSTemplate.objects.filter(unit=legacy):
|
||||
if SMSTemplate.objects.filter(unit=canonical, event_type=template.event_type).exists():
|
||||
template.delete()
|
||||
else:
|
||||
template.unit = canonical
|
||||
template.save(update_fields=["unit", "updated_at"])
|
||||
|
||||
legacy.delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0012_groupdispatch_operation_permissions_and_seed_org")]
|
||||
operations = [migrations.RunPython(merge_legacy_accounting_unit, migrations.RunPython.noop)]
|
||||
@@ -0,0 +1,76 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def merge_unit(apps, legacy_slug, canonical_slug, canonical_name, canonical_description):
|
||||
Unit = apps.get_model("staff", "OrganizationalUnit")
|
||||
Membership = apps.get_model("staff", "UnitMembership")
|
||||
StaffProfile = apps.get_model("staff", "StaffProfile")
|
||||
TicketCategory = apps.get_model("tickets", "TicketCategory")
|
||||
Ticket = apps.get_model("tickets", "Ticket")
|
||||
GroupDispatch = apps.get_model("tickets", "GroupDispatch")
|
||||
ContactGroup = apps.get_model("customers", "ContactGroup")
|
||||
CustomerProfile = apps.get_model("customers", "CustomerProfile")
|
||||
SMSTemplate = apps.get_model("notifications", "SMSTemplate")
|
||||
TicketMagicLink = apps.get_model("accounts", "TicketMagicLink")
|
||||
|
||||
legacy = Unit.objects.filter(slug=legacy_slug).first()
|
||||
canonical = Unit.objects.filter(slug=canonical_slug).first()
|
||||
if legacy is None:
|
||||
return
|
||||
if canonical is None:
|
||||
legacy.slug = canonical_slug
|
||||
legacy.name = canonical_name
|
||||
legacy.description = canonical_description
|
||||
legacy.is_active = True
|
||||
legacy.accepts_external_tickets = True
|
||||
legacy.show_in_customer_form = True
|
||||
legacy.save()
|
||||
return
|
||||
|
||||
canonical.permissions.add(*legacy.permissions.all())
|
||||
TicketCategory.objects.filter(unit=legacy).update(unit=canonical)
|
||||
Ticket.objects.filter(owning_unit=legacy).update(owning_unit=canonical)
|
||||
Ticket.objects.filter(assigned_unit=legacy).update(assigned_unit=canonical)
|
||||
GroupDispatch.objects.filter(unit=legacy).update(unit=canonical)
|
||||
TicketMagicLink.objects.filter(created_unit=legacy).update(created_unit=canonical)
|
||||
StaffProfile.objects.filter(primary_unit=legacy).update(primary_unit=canonical)
|
||||
for membership in Membership.objects.filter(unit=legacy):
|
||||
existing = Membership.objects.filter(user_id=membership.user_id, unit=canonical).first()
|
||||
if existing is None:
|
||||
membership.unit = canonical
|
||||
membership.save(update_fields=["unit", "updated_at"])
|
||||
continue
|
||||
existing.permissions.add(*membership.permissions.all())
|
||||
existing.is_active = existing.is_active or membership.is_active
|
||||
existing.is_primary = existing.is_primary or membership.is_primary
|
||||
if membership.ticket_scope == "UNIT":
|
||||
existing.ticket_scope = "UNIT"
|
||||
existing.save(update_fields=["is_active", "is_primary", "ticket_scope", "updated_at"])
|
||||
membership.delete()
|
||||
for group in ContactGroup.objects.filter(unit=legacy):
|
||||
existing = ContactGroup.objects.filter(unit=canonical, name=group.name).first()
|
||||
if existing is None:
|
||||
group.unit = canonical
|
||||
group.save(update_fields=["unit", "updated_at"])
|
||||
continue
|
||||
for profile in CustomerProfile.objects.filter(groups=group):
|
||||
profile.groups.add(existing)
|
||||
GroupDispatch.objects.filter(contact_group=group).update(contact_group=existing)
|
||||
group.delete()
|
||||
for template in SMSTemplate.objects.filter(unit=legacy):
|
||||
if SMSTemplate.objects.filter(unit=canonical, event_type=template.event_type).exists():
|
||||
template.delete()
|
||||
else:
|
||||
template.unit = canonical
|
||||
template.save(update_fields=["unit", "updated_at"])
|
||||
legacy.delete()
|
||||
|
||||
|
||||
def merge_legacy_warehouse_units(apps, schema_editor):
|
||||
merge_unit(apps, "logistics", "warehouse-loading", "بارگیری انبار", "بارگیری، انبار و ارسال")
|
||||
merge_unit(apps, "warehouse", "warehouse-loading", "بارگیری انبار", "بارگیری، انبار و ارسال")
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0013_merge_legacy_accounting_unit")]
|
||||
operations = [migrations.RunPython(merge_legacy_warehouse_units, migrations.RunPython.noop)]
|
||||
@@ -0,0 +1,69 @@
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
UNIT_CONTENT = {
|
||||
"sales": ("خرید، فروش، موجودی کالا، سفارش، بارگیری، ارسال، تحویل و خدمات مشتریان", "فروش خرید سفارش کالا محصول قیمت پیش فاکتور موجودی بار بارگیری ارسال حمل تحویل نمایندگی شکایت مرجوعی"),
|
||||
"finance": ("پرداخت، فاکتور، صورتحساب، مانده حساب، چک، مالیات و تسویه حساب", "مالی حسابداری پرداخت واریز فیش رسید فاکتور صورتحساب مانده بدهی بستانکاری چک مالیات تسویه"),
|
||||
}
|
||||
|
||||
CATEGORIES = {
|
||||
"sales": (
|
||||
(10, "price", "قیمت، پیشفاکتور و خرید", "دریافت قیمت روز، پیشفاکتور یا راهنمایی برای خرید محصول.", "قیمت نرخ بها خرید سفارش پیش فاکتور پیشفاکتور quotation استعلام قیمت لیست قیمت"),
|
||||
(20, "discount", "تخفیف و پیشنهاد فروش", "پرسش درباره تخفیف، جشنواره، پیشنهاد ویژه یا قیمت همکاری.", "تخفیف آفر جشنواره پیشنهاد ویژه قیمت همکاری درصد تخفیف فروش عمده کد تخفیف"),
|
||||
(30, "sales-terms", "شرایط فروش و ثبت سفارش", "اطلاع از شرایط فروش و ثبت، تغییر یا تأیید سفارش.", "شرایط فروش ثبت سفارش خرید سفارش جدید تغییر سفارش تایید سفارش تأیید سفارش لغو سفارش قرارداد فروش"),
|
||||
(40, "inventory", "موجودی و مشخصات کالا", "استعلام موجودی، زمان تأمین، مشخصات، مدل، رنگ یا ابعاد کالا.", "موجودی کالا ناموجود تامین تأمین محصول مشخصات فنی مدل رنگ سایز اندازه ابعاد کاتالوگ جایگزین"),
|
||||
(50, "order-follow-up", "پیگیری سفارش", "پیگیری وضعیت سفارش از زمان ثبت تا آمادهسازی و ارسال.", "پیگیری سفارش وضعیت سفارش کد سفارش شماره سفارش آماده سازی آمادهسازی پردازش سفارش کی آماده میشه چه زمانی"),
|
||||
(60, "loading-shipping-tracking", "پیگیری بارگیری و ارسال", "هماهنگی و پیگیری بارگیری، ارسال، حمل، راننده و رسیدن بار.", "پیگیری بار ارسال بار بارگیری حمل محموله راننده کامیون تریلی بارنامه کد رهگیری رهگیری ارسال وضعیت بارگیری زمان ارسال ارسال نشده کی میرسه کجاست"),
|
||||
(70, "delivery-issue", "تحویل، کسری یا آسیب کالا", "اعلام تأخیر، عدم تحویل، کسری، مغایرت یا آسیبدیدگی کالای تحویلی.", "تحویل بار تاخیر تأخیر نرسیده کسری کمبود مغایرت اشتباه خرابی آسیب شکسته ناقص مفقودی بسته بندی بستهبندی"),
|
||||
(80, "consulting", "مشاوره انتخاب محصول", "دریافت راهنمایی برای انتخاب محصول مناسب و مقایسه گزینهها.", "مشاوره راهنمای خرید انتخاب محصول مقایسه پیشنهاد مناسب کاربرد چه محصولی کدام مدل"),
|
||||
(90, "agency", "نمایندگی و همکاری تجاری", "درخواست نمایندگی، عاملیت فروش یا شروع همکاری تجاری.", "نمایندگی عاملیت همکاری تجاری شراکت فروشنده همکار پخش کننده پخشکننده قرارداد همکاری"),
|
||||
(100, "complaint", "مرجوعی، تعویض و شکایت", "درخواست مرجوعی یا تعویض کالا و ثبت شکایت از فرایند فروش یا تحویل.", "مرجوعی برگشت کالا تعویض شکایت اعتراض نارضایتی خدمات بد پیگیری شکایت خسارت"),
|
||||
(110, "delivery", "شرایط و هزینه ارسال", "پرسش پیش از خرید درباره روش، هزینه، محدوده و زمان ارسال.", "شرایط ارسال هزینه ارسال کرایه حمل هزینه حمل روش ارسال زمان تحویل ارسال شهرستان پست باربری تیپاکس"),
|
||||
(900, "sales-general", "عمومی فروش", "موضوعات فروش، سفارش و خدمات مشتریان که در دستههای دیگر قرار نمیگیرند.", "عمومی فروش سایر متفرقه سوال سؤال درخواست مشتری راهنمایی ارتباط با فروش"),
|
||||
),
|
||||
"finance": (
|
||||
(10, "invoice", "فاکتور و صورتحساب", "دریافت یا پیگیری فاکتور رسمی، صورتحساب و رونوشت اسناد فروش.", "فاکتور رسمی صورتحساب صورت حساب پیش فاکتور کپی فاکتور رونوشت شماره فاکتور صدور فاکتور اصلاح فاکتور"),
|
||||
(20, "payment-receipt", "ثبت و تأیید پرداخت", "ارسال فیش یا رسید واریز و پیگیری ثبت و تأیید پرداخت.", "پرداخت واریز فیش واریزی رسید تراکنش کارت به کارت حواله بانکی تایید پرداخت تأیید پرداخت ثبت پرداخت پول واریز کردم"),
|
||||
(30, "account-balance", "مانده حساب و گردش حساب", "دریافت مانده بدهکار یا بستانکار، گردش حساب و تطبیق حسابها.", "مانده حساب گردش حساب بدهی بدهکار بستانکار طلب صورت حساب صورتحساب حساب مشتری مغایرت حساب تطبیق حساب"),
|
||||
(40, "financial-documents", "اصلاح و تکمیل اسناد مالی", "ارسال، تکمیل یا اصلاح مدارک و اسناد موردنیاز واحد مالی.", "اسناد مالی مدارک مالی اصلاح سند سند حسابداری کد اقتصادی شناسه ملی اطلاعات شرکت پرونده مالی پیوست سند"),
|
||||
(50, "cheque-guarantee", "چک، ضمانت و تعهدات", "پیگیری تحویل، وصول، برگشت یا تعویض چک و اسناد ضمانتی.", "چک صیادی ثبت چک وصول چک برگشتی برگشت چک تعویض چک ضمانت ضمانتنامه ضمانتنامه سفته سررسید"),
|
||||
(60, "payment-refund", "استرداد وجه", "درخواست یا پیگیری بازگشت وجه پرداختشده.", "استرداد وجه عودت وجه برگشت پول بازپرداخت پس گرفتن پول واریز اشتباه پرداخت اضافه refund"),
|
||||
(70, "tax-vat", "مالیات و ارزش افزوده", "پرسش درباره مالیات، ارزش افزوده، سامانه مؤدیان و صورتحساب الکترونیکی.", "مالیات ارزش افزوده vat سامانه مودیان مؤدیان صورتحساب الکترونیکی شناسه یکتا گواهی مالیاتی فصل معاملات"),
|
||||
(80, "supplier-settlement", "تسویه تأمینکنندگان", "پیگیری صورتحساب، پرداخت و تسویه حساب فروشندگان و تأمینکنندگان.", "تامین کننده تأمین کننده تامینکننده تأمینکننده فروشنده طرف حساب تسویه تسویه حساب پرداخت تامین کننده صورت وضعیت صورتوضعیت"),
|
||||
(900, "finance-general", "سایر امور مالی و حسابداری", "موضوعات مالی و حسابداری که در دستههای دیگر قرار نمیگیرند.", "عمومی مالی حسابداری سایر متفرقه امور مالی سوال سؤال درخواست پیگیری مالی"),
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def seed_catalog(apps, schema_editor):
|
||||
Unit = apps.get_model("staff", "OrganizationalUnit")
|
||||
Category = apps.get_model("tickets", "TicketCategory")
|
||||
units = {}
|
||||
for slug, (description, keywords) in UNIT_CONTENT.items():
|
||||
name = "فروش" if slug == "sales" else "مالی / حسابداری"
|
||||
order = 10 if slug == "sales" else 30
|
||||
unit, _ = Unit.objects.update_or_create(
|
||||
slug=slug,
|
||||
defaults={"name": name, "description": description, "search_keywords": keywords, "sort_order": order, "is_active": True, "accepts_external_tickets": True, "show_in_customer_form": True},
|
||||
)
|
||||
units[slug] = unit
|
||||
Unit.objects.filter(slug="warehouse-loading").update(show_in_customer_form=False)
|
||||
for unit_slug, rows in CATEGORIES.items():
|
||||
for sort_order, slug, title, description, keywords in rows:
|
||||
Category.objects.update_or_create(
|
||||
slug=slug,
|
||||
defaults={"unit": units[unit_slug], "title": title, "description": description, "search_keywords": keywords, "sort_order": sort_order, "is_active": True, "show_in_customer_form": True},
|
||||
)
|
||||
warehouse = Unit.objects.filter(slug="warehouse-loading").first()
|
||||
if warehouse is not None:
|
||||
Category.objects.update_or_create(
|
||||
slug="warehouse-loading-general",
|
||||
defaults={"unit": warehouse, "title": "عملیات بارگیری انبار", "description": "هماهنگی داخلی عملیات انبار و بارگیری.", "search_keywords": "انبار بارگیری عملیات داخلی", "sort_order": 10, "is_active": True, "show_in_customer_form": False},
|
||||
)
|
||||
Category.objects.filter(slug="other").update(show_in_customer_form=False)
|
||||
Category.objects.filter(slug="e2e-sms").update(show_in_customer_form=False)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("tickets", "0014_merge_legacy_warehouse_units")]
|
||||
operations = [migrations.RunPython(seed_catalog, migrations.RunPython.noop)]
|
||||
Whitespace-only changes.
Reference in new issue
Block a user