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//', 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)