- Реализован импорт Product из CSV/XLSX через Celery с прогресс-баром - Параллельная загрузка фото товаров с внешних URL (масштабируемость до 500+ товаров) - Добавлена модель ProductImportJob для отслеживания статуса импорта - Создан таск download_product_photo_async для загрузки фото в фоне - Интеграция с существующим ImageProcessor (синхронная обработка через use_async=False) - Добавлены view и template для импорта с real-time обновлением через AJAX FIX: Исправлен баг со счётчиком SKU - инкремент только после успешного сохранения - Добавлен SKUCounter.peek_next_value() - возвращает следующий номер БЕЗ инкремента - Добавлен SKUCounter.increment_counter() - инкрементирует счётчик - generate_product_sku() использует peek_next_value() вместо get_next_value() - Добавлен post_save сигнал increment_sku_counter_after_save() для инкремента после создания - Предотвращает пропуски номеров при ошибках валидации (например cost_price NULL) FIX: Исправлена ошибка с is_main в ProductPhoto - ProductPhoto не имеет поля is_main, используется только order - Первое фото (order=0) автоматически считается главным - Удалён параметр is_main из download_product_photo_async и _collect_photo_tasks Изменены файлы: - products/models/base.py - методы для управления счётчиком SKU - products/models/import_job.py - модель для отслеживания импорта - products/services/import_export.py - сервис импорта с поддержкой Celery - products/tasks.py - таски для асинхронного импорта и загрузки фото - products/signals.py - сигнал для инкремента счётчика после сохранения - products/utils/sku_generator.py - использование peek_next_value() - products/views/product_import_views.py - view для импорта - products/templates/products/product_import*.html - UI для импорта - docker/entrypoint.sh - настройка Celery worker (concurrency=4) - requirements.txt - добавлен requests для загрузки фото
127 lines
9.1 KiB
Python
127 lines
9.1 KiB
Python
from django.urls import path
|
||
from . import views
|
||
from .views import api_views
|
||
from .views import photo_status_api
|
||
|
||
app_name = 'products'
|
||
|
||
urlpatterns = [
|
||
# Main unified list for products and kits (default view)
|
||
path('', views.CombinedProductListView.as_view(), name='products-list'),
|
||
|
||
# Каталог с drag-n-drop
|
||
path('catalog/', views.CatalogView.as_view(), name='catalog'),
|
||
|
||
# Legacy URLs for backward compatibility
|
||
path('all/', views.CombinedProductListView.as_view(), name='all-products'),
|
||
path('products/', views.ProductListView.as_view(), name='product-list-legacy'),
|
||
path('kits/', views.ProductKitListView.as_view(), name='productkit-list'),
|
||
# CRUD URLs for Product
|
||
path('product/create/', views.ProductCreateView.as_view(), name='product-create'),
|
||
path('product/<int:pk>/', views.ProductDetailView.as_view(), name='product-detail'),
|
||
path('product/<int:pk>/update/', views.ProductUpdateView.as_view(), name='product-update'),
|
||
path('product/<int:pk>/delete/', views.ProductDeleteView.as_view(), name='product-delete'),
|
||
|
||
# Import/Export
|
||
path('import/', views.product_import_view, name='product-import'),
|
||
path('import/status/<int:job_id>/', views.product_import_status_view, name='product-import-status'),
|
||
path('import/status/<int:job_id>/api/', views.product_import_status_api, name='product-import-status-api'),
|
||
path('import/errors/download/', views.download_import_errors, name='product-import-errors-download'),
|
||
|
||
# Photo management for Product
|
||
path('product/photo/<int:pk>/delete/', views.product_photo_delete, name='product-photo-delete'),
|
||
path('product/photo/<int:pk>/set-main/', views.product_photo_set_main, name='product-photo-set-main'),
|
||
path('product/photo/<int:pk>/move-up/', views.product_photo_move_up, name='product-photo-move-up'),
|
||
path('product/photo/<int:pk>/move-down/', views.product_photo_move_down, name='product-photo-move-down'),
|
||
path('product/photos/delete-bulk/', views.product_photos_delete_bulk, name='product-photos-delete-bulk'),
|
||
|
||
# CRUD URLs for ProductKit (комплекты/букеты)
|
||
path('kit/create/', views.ProductKitCreateView.as_view(), name='productkit-create'),
|
||
path('kit/<int:pk>/', views.ProductKitDetailView.as_view(), name='productkit-detail'),
|
||
path('kit/<int:pk>/update/', views.ProductKitUpdateView.as_view(), name='productkit-update'),
|
||
path('kit/<int:pk>/delete/', views.ProductKitDeleteView.as_view(), name='productkit-delete'),
|
||
path('kit/<int:pk>/make-permanent/', views.ProductKitMakePermanentView.as_view(), name='productkit-make-permanent'),
|
||
|
||
# Photo management for ProductKit
|
||
path('kit/photo/<int:pk>/delete/', views.productkit_photo_delete, name='productkit-photo-delete'),
|
||
path('kit/photo/<int:pk>/set-main/', views.productkit_photo_set_main, name='productkit-photo-set-main'),
|
||
path('kit/photo/<int:pk>/move-up/', views.productkit_photo_move_up, name='productkit-photo-move-up'),
|
||
path('kit/photo/<int:pk>/move-down/', views.productkit_photo_move_down, name='productkit-photo-move-down'),
|
||
|
||
# API endpoints
|
||
path('api/search-products-variants/', views.search_products_and_variants, name='api-search-products-variants'),
|
||
path('api/products/<int:product_id>/sales-units/', api_views.get_product_sales_units_api, name='api-product-sales-units'),
|
||
path('api/kits/temporary/create/', views.create_temporary_kit_api, name='api-temporary-kit-create'),
|
||
path('api/tags/create/', api_views.create_tag_api, name='api-tag-create'),
|
||
path('api/tags/<int:pk>/toggle/', api_views.toggle_tag_status_api, name='api-tag-toggle'),
|
||
path('api/categories/create/', api_views.create_category_api, name='api-category-create'),
|
||
path('api/categories/<int:pk>/rename/', api_views.rename_category_api, name='api-category-rename'),
|
||
path('api/products/<int:pk>/update-price/', api_views.update_product_price_api, name='api-update-product-price'),
|
||
path('api/payment-methods/', api_views.get_payment_methods, name='api-payment-methods'),
|
||
|
||
# Photo processing status API (for AJAX polling)
|
||
path('api/photos/status/<str:task_id>/', photo_status_api.photo_processing_status, name='api-photo-status'),
|
||
path('api/photos/batch-status/', photo_status_api.batch_photo_status, name='api-batch-photo-status'),
|
||
|
||
# CRUD URLs for ProductVariantGroup (Варианты товаров)
|
||
path('variant-groups/', views.ProductVariantGroupListView.as_view(), name='variantgroup-list'),
|
||
path('variant-groups/create/', views.ProductVariantGroupCreateView.as_view(), name='variantgroup-create'),
|
||
path('variant-groups/<int:pk>/', views.ProductVariantGroupDetailView.as_view(), name='variantgroup-detail'),
|
||
path('variant-groups/<int:pk>/update/', views.ProductVariantGroupUpdateView.as_view(), name='variantgroup-update'),
|
||
path('variant-groups/<int:pk>/delete/', views.ProductVariantGroupDeleteView.as_view(), name='variantgroup-delete'),
|
||
|
||
# AJAX endpoints for ProductVariantGroup item management
|
||
path('variant-groups/item/<int:item_id>/move/<str:direction>/', views.product_variant_group_item_move, name='variantgroup-item-move'),
|
||
|
||
# CRUD URLs for ProductCategory
|
||
path('categories/', views.ProductCategoryListView.as_view(), name='category-list'),
|
||
path('categories/create/', views.ProductCategoryCreateView.as_view(), name='category-create'),
|
||
path('categories/<int:pk>/', views.ProductCategoryDetailView.as_view(), name='category-detail'),
|
||
path('categories/<int:pk>/update/', views.ProductCategoryUpdateView.as_view(), name='category-update'),
|
||
path('categories/<int:pk>/delete/', views.ProductCategoryDeleteView.as_view(), name='category-delete'),
|
||
|
||
# Category photo management
|
||
path('categories/photo/<int:pk>/delete/', views.category_photo_delete, name='category-photo-delete'),
|
||
path('categories/photo/<int:pk>/set-main/', views.category_photo_set_main, name='category-photo-set-main'),
|
||
path('categories/photo/<int:pk>/move-up/', views.category_photo_move_up, name='category-photo-move-up'),
|
||
path('categories/photo/<int:pk>/move-down/', views.category_photo_move_down, name='category-photo-move-down'),
|
||
|
||
# CRUD URLs for ProductTag
|
||
path('tags/', views.ProductTagListView.as_view(), name='tag-list'),
|
||
path('tags/create/', views.ProductTagCreateView.as_view(), name='tag-create'),
|
||
path('tags/<int:pk>/', views.ProductTagDetailView.as_view(), name='tag-detail'),
|
||
path('tags/<int:pk>/update/', views.ProductTagUpdateView.as_view(), name='tag-update'),
|
||
path('tags/<int:pk>/delete/', views.ProductTagDeleteView.as_view(), name='tag-delete'),
|
||
|
||
# CRUD URLs for ProductAttribute (справочник атрибутов)
|
||
path('attributes/', views.ProductAttributeListView.as_view(), name='attribute-list'),
|
||
path('attributes/create/', views.ProductAttributeCreateView.as_view(), name='attribute-create'),
|
||
path('attributes/<int:pk>/', views.ProductAttributeDetailView.as_view(), name='attribute-detail'),
|
||
path('attributes/<int:pk>/update/', views.ProductAttributeUpdateView.as_view(), name='attribute-update'),
|
||
path('attributes/<int:pk>/delete/', views.ProductAttributeDeleteView.as_view(), name='attribute-delete'),
|
||
|
||
# API для атрибутов
|
||
path('api/attributes/', views.get_attributes_list_api, name='api-attributes-list'),
|
||
path('api/attributes/create/', views.create_attribute_api, name='api-attribute-create'),
|
||
path('api/attributes/<int:pk>/values/add/', views.add_attribute_value_api, name='attribute-add-value'),
|
||
path('api/attributes/<int:pk>/values/<int:value_id>/delete/', views.delete_attribute_value_api, name='attribute-delete-value'),
|
||
|
||
# CRUD URLs for ConfigurableProduct
|
||
path('configurable/', views.ConfigurableProductListView.as_view(), name='configurableproduct-list'),
|
||
path('configurable/create/', views.ConfigurableProductCreateView.as_view(), name='configurableproduct-create'),
|
||
path('configurable/<int:pk>/', views.ConfigurableProductDetailView.as_view(), name='configurableproduct-detail'),
|
||
path('configurable/<int:pk>/update/', views.ConfigurableProductUpdateView.as_view(), name='configurableproduct-update'),
|
||
path('configurable/<int:pk>/delete/', views.ConfigurableProductDeleteView.as_view(), name='configurableproduct-delete'),
|
||
|
||
# API для управления вариантами ConfigurableProduct
|
||
path('configurable/<int:pk>/options/add/', views.add_option_to_configurable, name='configurableproduct-add-option'),
|
||
path('configurable/<int:pk>/options/<int:option_id>/remove/', views.remove_option_from_configurable, name='configurableproduct-remove-option'),
|
||
path('configurable/<int:pk>/options/<int:option_id>/set-default/', views.set_option_as_default, name='configurableproduct-set-default-option'),
|
||
|
||
# Управление единицами измерения
|
||
path('units/', views.unit_of_measure_list, name='unit-list'),
|
||
path('sales-units/', views.product_sales_unit_list, name='sales-unit-list'),
|
||
path('sales-units/create/', views.product_sales_unit_create, name='sales-unit-create'),
|
||
path('sales-units/<int:pk>/edit/', views.product_sales_unit_update, name='sales-unit-update'),
|
||
path('sales-units/<int:pk>/delete/', views.product_sales_unit_delete, name='sales-unit-delete'),
|
||
] |