114 lines
6.9 KiB
Python
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)
|