Files
crmApi/apps/shop/admin_import.py
T
2026-10-08 16:37:45 +03:30

114 lines
6.9 KiB
Python

import json
from django import forms
from django.core.exceptions import PermissionDenied, ValidationError
from django.shortcuts import get_object_or_404
from django.template.response import TemplateResponse
from django.urls import path
from apps.shop.import_models import ProductImportBatch
from apps.shop.product_import import HEADERS, parse_workbook
from apps.shop.import_batches import preview_batch, confirm_batch
from apps.workspace.models import Workspace
from utils.admin_excel import excel_response
class ProductImportForm(forms.Form):
workspace = forms.ModelChoiceField(label='فضای کاری مقصد', queryset=Workspace.objects.all())
file = forms.FileField(label='فایل xlsx')
sheet = forms.CharField(label='نام برگه (خالی = اولین برگه)', required=False)
header_row = forms.IntegerField(label='شماره ردیف سرستون', min_value=1, max_value=100, initial=1)
mode = forms.ChoiceField(label='نوع ورود', choices=(('create', 'فقط ایجاد'), ('upsert', 'ایجاد و به‌روزرسانی'), ('update', 'فقط به‌روزرسانی')))
create_categories = forms.BooleanField(label='ساخت دسته‌های ناموجود', required=False, initial=True)
errors = forms.ChoiceField(label='مدیریت خطا', choices=(('atomic', 'در صورت هر خطا، هیچ ردیفی ثبت نشود'), ('skip', 'فقط ردیف‌های معتبر ثبت شوند')))
ordering = forms.ChoiceField(label='ترتیب نمایش محصولات', choices=(('preserve', 'مقادیر فایل؛ خالی = حفظ مقدار فعلی'), ('rows', 'ترتیب ردیف فایل برای مقادیر خالی'), ('append', 'محصول جدید در انتهای دسته')))
mapping = forms.CharField(label='نگاشت اختیاری ستون‌ها (JSON)', required=False, widget=forms.Textarea(attrs={'rows': 3}),
help_text='مثال: {"name": "عنوان کالا", "category": "گروه", "acc_code": "شناسه"}')
def clean_mapping(self):
try:
value = json.loads(self.cleaned_data['mapping'] or '{}')
if not isinstance(value, dict) or not all(isinstance(key, str) and isinstance(item, str) for key, item in value.items()):
raise ValueError
return value
except ValueError:
raise forms.ValidationError('یک شیء JSON با کلید و مقدار متنی وارد کنید.')
class ProductImportAdminMixin:
change_list_template = 'admin/shop/product/change_list.html'
def import_allowed(self, request):
return (self.has_add_permission(request) and self.has_change_permission(request)
and request.user.has_perm('shop.view_productcategory'))
def get_urls(self):
return [path('import-excel/', self.admin_site.admin_view(self.import_view), name='shop_product_import'),
path('import-template/', self.admin_site.admin_view(self.import_template), name='shop_product_import_template'),
path('import-report/<int:batch_id>/', self.admin_site.admin_view(self.import_report), name='shop_product_import_report')] + super().get_urls()
def changelist_view(self, request, extra_context=None):
return super().changelist_view(request, dict(extra_context or {}, can_import=self.import_allowed(request)))
def import_template(self, request):
if not self.import_allowed(request):
raise PermissionDenied
return excel_response(HEADERS.keys(), [('محصول نمونه', 'دسته اصلی/زیردسته', 'CODE-001', 1000, 1, 1, 1, 1, False, '{}', 'عدد')], 'product-import-template.xlsx')
def import_report(self, request, batch_id):
if not self.import_allowed(request):
raise PermissionDenied
batch = get_object_or_404(ProductImportBatch, pk=batch_id, created_by=request.user)
rows = ((row['row'], *(row['values'].get(key) for key in HEADERS), row['status'], row['errors'])
for row in batch.result.get('rows', []))
return excel_response(('ردیف', *HEADERS, 'وضعیت', 'خطاها'), rows, f'import-report-{batch.pk}.xlsx')
def import_view(self, request):
if not self.import_allowed(request):
raise PermissionDenied
form = ProductImportForm(request.POST or None, request.FILES or None)
batch, problem = None, None
if request.method == 'POST':
if request.POST.get('confirm'):
try:
batch = self.confirm_import(request)
except ValidationError as error:
problem = '؛ '.join(error.messages)
elif form.is_valid():
try:
batch = self.preview_import(request, form.cleaned_data)
except ValidationError as error:
form.add_error(None, error)
context = dict(self.admin_site.each_context(request), opts=self.model._meta, title='ورود محصولات از اکسل',
form=form, batch=batch, problem=problem,
result=batch.result if batch else None, can_confirm=batch and not batch.confirmed_at
and (batch.payload['options']['errors'] == 'skip' or not batch.result['errors']))
return TemplateResponse(request, 'admin/shop/product/import.html', context)
def preview_import(self, request, data):
if data['create_categories'] and not request.user.has_perm('shop.add_productcategory'):
raise PermissionDenied
# Category-order permission is checked against the parsed file before preview writes.
rows = parse_workbook(data['file'], data['sheet'], data['header_row'], data['mapping'])
if any(row['values'].get(key) for row in rows for key in ('order_cat', 'category_excel_order')):
if not request.user.has_perm('shop.change_productcategory'):
raise PermissionDenied
data['file'].seek(0)
options = {key: data[key] for key in ('mode', 'create_categories', 'errors', 'ordering')}
return preview_batch(request.user, data['workspace'], data['file'], options,
data['sheet'], data['header_row'], data['mapping'])
def confirm_import(self, request):
try:
batch_id = int(request.POST['batch_id'])
except (KeyError, ValueError, TypeError):
raise ValidationError('شناسه پیش‌نمایش معتبر نیست.')
batch = get_object_or_404(ProductImportBatch, pk=batch_id, created_by=request.user)
options = batch.payload['options']
if options['create_categories'] and not request.user.has_perm('shop.add_productcategory'):
raise PermissionDenied
if any(row['values'].get(key) for row in batch.payload['rows'] for key in ('order_cat', 'category_excel_order')):
if not request.user.has_perm('shop.change_productcategory'):
raise PermissionDenied
return confirm_batch(batch_id, request.user)