add product

This commit is contained in:
javad committed 2026-10-08 16:37:45 +03:30
1 parent 423d4cf40e
commit c8d45dd07c
58 files changed
+8309 -674

No files matched your search

+109
View File
@@ -0,0 +1,109 @@
"""Common admin ergonomics without changing application API contracts."""
from django import forms
from django.contrib import admin
from django.db import models
from django.core.exceptions import ValidationError
from utils.admin_excel import excel_response
def workspace_id(obj):
if obj is None:
return None
if hasattr(obj, 'sys_ws_id'):
return obj.sys_ws_id
for relation in ('order', 'form', 'ticket_comment'):
if hasattr(obj, relation):
return workspace_id(getattr(obj, relation))
return None
class RelatedWorkspaceForm(forms.ModelForm):
def clean(self):
cleaned = super().clean()
workspace = cleaned.get('sys_ws')
scope = workspace.pk if workspace else None
if scope is None:
for key in ('order', 'form', 'ticket_comment'):
scope = workspace_id(cleaned.get(key))
if scope is not None:
break
if scope is not None:
for field in self._meta.model._meta.fields:
if field.is_relation and field.name != 'sys_ws':
value = cleaned.get(field.name)
related_scope = workspace_id(value)
if related_scope is not None and related_scope != scope:
self.add_error(field.name, 'این رابطه متعلق به فضای کاری دیگری است.')
self.check_category_parent(cleaned)
self.check_domain_values(cleaned)
return cleaned
def check_domain_values(self, cleaned):
label = self._meta.model._meta.label_lower
if label == 'cuser.cuserrole' and cleaned.get('mobile') is not None:
from utils.validations.mobile import normalize_mobile
try:
cleaned['mobile'] = normalize_mobile(cleaned['mobile'])
except ValidationError as error:
self.add_error('mobile', error.messages)
user = cleaned.get('cuser')
if user and user.mobile != cleaned.get('mobile'):
self.add_error('mobile', 'شماره نقش باید با شماره کاربر یکسان باشد.')
if label in ('shop.cart', 'shop.orderitem'):
key = 'quantity' if label == 'shop.cart' else 'amount'
if cleaned.get(key) is not None and cleaned[key] < 1:
self.add_error(key, 'تعداد باید حداقل یک باشد.')
if label == 'shop.orderitem' and cleaned.get('price') is not None and cleaned['price'] < 0:
self.add_error('price', 'قیمت نباید منفی باشد.')
def check_category_parent(self, cleaned):
if self._meta.model._meta.label_lower != 'shop.productcategory':
return
parent = cleaned.get('parent')
visited = set()
while parent is not None:
if parent.pk == self.instance.pk or parent.pk in visited:
self.add_error('parent', 'دسته والد نباید یک چرخه ایجاد کند.')
return
visited.add(parent.pk)
parent = parent.parent
class CRMModelAdmin(admin.ModelAdmin):
form = RelatedWorkspaceForm
list_per_page = 50
show_full_result_count = False
save_on_top = True
actions = ('export_selected',)
sensitive_fields = ()
def __init__(self, model, admin_site):
super().__init__(model, admin_site)
fields = model._meta.fields
names = {field.name for field in fields}
if self.list_display == ('__str__',):
candidates = ('id', 'name', 'name_family', 'title', 'mobile', 'customer', 'lead', 'form',
'product', 'order', 'cuser_role', 'role_type', 'sys_ws', 'status', 'type',
'is_active', 'expire', 'datetime', 'created_at')
self.list_display = tuple(name for name in candidates if name in names)[:9]
if not self.search_fields:
self.search_fields = tuple(field.name for field in fields if isinstance(field, (models.CharField, models.TextField))
and field.name not in self.sensitive_fields)
if not self.list_filter:
self.list_filter = tuple(field.name for field in fields if field.name == 'sys_ws' or field.choices
or isinstance(field, models.BooleanField))
self.readonly_fields = tuple(dict.fromkeys((*self.readonly_fields,
*(name for name in ('created_at', 'updated_at', 'uuid') if name in names))))
self.raw_id_fields = tuple(field.name for field in fields if field.many_to_one)
self.list_select_related = tuple(field.name for field in fields if field.many_to_one)
if 'created_at' in names:
self.date_hierarchy = 'created_at'
if not self.ordering and not model._meta.ordering:
self.ordering = ('-pk',)
@admin.action(description='خروجی اکسل ردیف‌های انتخاب‌شده', permissions=['view'])
def export_selected(self, request, queryset):
fields = [field for field in self.model._meta.fields if field.name not in self.sensitive_fields]
rows = queryset.values_list(*(field.attname for field in fields)).iterator()
return excel_response([str(field.verbose_name) for field in fields], rows, f'{self.model._meta.model_name}.xlsx')
+39
View File
@@ -0,0 +1,39 @@
from datetime import datetime
from io import BytesIO
from itertools import chain
import json
from django.http import HttpResponse
from openpyxl import Workbook
from openpyxl.cell import WriteOnlyCell
from openpyxl.cell.cell import ILLEGAL_CHARACTERS_RE
from openpyxl.utils import get_column_letter
def excel_response(headers, rows, filename):
workbook = Workbook(write_only=True)
sheet = workbook.create_sheet('Products')
sheet.sheet_view.rightToLeft = True
sheet.freeze_panes = 'A2'
for index in range(1, len(headers) + 1):
sheet.column_dimensions[get_column_letter(index)].width = 24
for number, row in enumerate(chain((headers,), rows), 1):
cells = []
for value in row:
if isinstance(value, datetime) and value.tzinfo:
value = value.isoformat()
if isinstance(value, (dict, list)):
value = json.dumps(value, ensure_ascii=False)
if isinstance(value, str):
value = ILLEGAL_CHARACTERS_RE.sub('', value)
cell = WriteOnlyCell(sheet, value=value)
if isinstance(value, str):
cell.data_type = 's'
cells.append(cell)
sheet.append(cells)
sheet.auto_filter.ref = f'A1:{get_column_letter(len(headers))}{number}'
content = BytesIO()
workbook.save(content)
response = HttpResponse(content.getvalue(), content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
+8 -5
View File
@@ -1,5 +1,6 @@
from abc import ABC, abstractmethod
import requests
from django.conf import settings
class SmsProviderABC(ABC):
@@ -23,10 +24,12 @@ class DosefrNavadHashtSMS(SmsProviderABC):
'TO': int(mobile),
'TEXT': f'{message}'
}
r = requests.get('http://www.0098sms.com/sendsmslink.aspx', params=payload)
if r.status_code == 200:
return True
else:
try:
r = requests.get('http://www.0098sms.com/sendsmslink.aspx', params=payload, timeout=10)
r.raise_for_status()
except requests.RequestException:
return False
# An HTTP 200 alone does not mean the SMS provider accepted the message.
success_codes = getattr(settings, 'SMS_SUCCESS_CODES', ('0',))
return r.text.strip() in success_codes
+14
View File
@@ -0,0 +1,14 @@
from django.core.exceptions import ValidationError
def normalize_mobile(value):
"""Keep the integer storage format while accepting Iranian mobile formats."""
mobile = str(value).strip().translate(str.maketrans("۰۱۲۳۴۵۶۷۸۹٠١٢٣٤٥٦٧٨٩", "01234567890123456789"))
mobile = mobile.replace(" ", "").replace("-", "")
for prefix in ("+98", "0098", "98", "0"):
if mobile.startswith(prefix) and len(mobile) == 10 + len(prefix):
mobile = mobile[len(prefix):]
break
if len(mobile) != 10 or not mobile.isascii() or not mobile.isdigit() or not mobile.startswith("9"):
raise ValidationError({"mobile": "شماره موبایل معتبر وارد کنید."})
return int(mobile)