This commit is contained in:
javad committed 2026-09-10 11:34:19 +03:30
commit fa9d154db9
260 files changed
+17643

No files matched your search

+130
View File
@@ -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)),
]
@@ -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)]
View File
Whitespace-only changes.