From 2a93cd8a8fc4518d882a93a5a2875d14dfa6dfd6 Mon Sep 17 00:00:00 2001 From: NikDizell Date: Mon, 13 Jul 2026 22:37:41 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8=D0=BB=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B9=D1=81=20=D1=81=D1=82=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=86=D1=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- OneCprogsite/settings.py | 5 ++ OneCprogsite/urls.py | 1 + prices/__init__.py | 0 prices/admin.py | 35 ++++++++++++ prices/apps.py | 6 ++ prices/migrations/__init__.py | 0 prices/models.py | 15 +++++ prices/templates/prices/price_list.html | 54 ++++++++++++++++++ prices/tests.py | 3 + prices/urls.py | 9 +++ prices/views.py | 76 +++++++++++++++++++++++++ programmer/mixins.py | 1 + requirements.txt | 3 +- 13 files changed, 207 insertions(+), 1 deletion(-) create mode 100644 prices/__init__.py create mode 100644 prices/admin.py create mode 100644 prices/apps.py create mode 100644 prices/migrations/__init__.py create mode 100644 prices/models.py create mode 100644 prices/templates/prices/price_list.html create mode 100644 prices/tests.py create mode 100644 prices/urls.py create mode 100644 prices/views.py diff --git a/OneCprogsite/settings.py b/OneCprogsite/settings.py index a501465..70f21e1 100644 --- a/OneCprogsite/settings.py +++ b/OneCprogsite/settings.py @@ -50,6 +50,10 @@ EXCLUDED_IPS = os.getenv('EXCLUDED_IPS', 'localhost,127.0.0.1').split(',') SITE_URL = os.getenv('SITE_URL', 'https://nikdizell.ru') +PRICE_API_URL = os.getenv('PRICE_API_URL', 'http://localhost:81/BP/hs/webexchange/price') +PRICE_API_USER = os.getenv('PRICE_API_USER', '') +PRICE_API_PASSWORD = os.getenv('PRICE_API_PASSWORD', '') + # Важно для работы за прокси if not DEBUG: SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') @@ -113,6 +117,7 @@ INSTALLED_APPS = [ 'products', 'background_task', 'django_celery_results', + 'prices', ] MIDDLEWARE = [ diff --git a/OneCprogsite/urls.py b/OneCprogsite/urls.py index df8c438..db6fd3e 100644 --- a/OneCprogsite/urls.py +++ b/OneCprogsite/urls.py @@ -27,6 +27,7 @@ urlpatterns = [ path('ckeditor5/', include('django_ckeditor_5.urls')), path('captcha/', include('captcha.urls')), path('products/', include('products.urls', namespace='products')), + path('prices/', include('prices.urls', namespace='prices')), # path('', index, name='home'), # path('about/', about, name='about'), # path('solution/', solution, name='solution'), diff --git a/prices/__init__.py b/prices/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/prices/admin.py b/prices/admin.py new file mode 100644 index 0000000..d0fa277 --- /dev/null +++ b/prices/admin.py @@ -0,0 +1,35 @@ +from django.contrib import admin, messages +from django.shortcuts import redirect +from django.urls import path +from .models import PriceItem +from .views import update_prices_from_api + +@admin.register(PriceItem) +class PriceItemAdmin(admin.ModelAdmin): + list_display = ('name', 'price', 'updated_at') + search_fields = ('name',) + actions = ['update_prices_action'] + + def update_prices_action(self, request, queryset): + """Admin action для обновления прайса.""" + update_prices_from_api(request) + update_prices_action.short_description = "Обновить прайс из API" + + def get_urls(self): + """Добавляем кастомный URL для кнопки в changelist.""" + urls = super().get_urls() + custom_urls = [ + path('update-prices/', self.admin_site.admin_view(self.update_prices_view), name='update-prices'), + ] + return custom_urls + urls + + def update_prices_view(self, request): + """View для кнопки обновления.""" + update_prices_from_api(request) + return redirect('admin:prices_priceitem_changelist') + + def changelist_view(self, request, extra_context=None): + """Передаём в контекст наличие кнопки.""" + extra_context = extra_context or {} + extra_context['show_update_button'] = True + return super().changelist_view(request, extra_context=extra_context) \ No newline at end of file diff --git a/prices/apps.py b/prices/apps.py new file mode 100644 index 0000000..821b0fa --- /dev/null +++ b/prices/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class PricesConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'prices' diff --git a/prices/migrations/__init__.py b/prices/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/prices/models.py b/prices/models.py new file mode 100644 index 0000000..c56f0c4 --- /dev/null +++ b/prices/models.py @@ -0,0 +1,15 @@ +from django.db import models +from django.urls import reverse + +class PriceItem(models.Model): + name = models.CharField(max_length=255, verbose_name='Наименование услуги') + price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='Цена (₽)') + updated_at = models.DateTimeField(auto_now=True, verbose_name='Дата обновления') + + class Meta: + verbose_name = 'Услуга' + verbose_name_plural = 'Прайс-лист' + ordering = ['name'] + + def __str__(self): + return f"{self.name} – {self.price} ₽" \ No newline at end of file diff --git a/prices/templates/prices/price_list.html b/prices/templates/prices/price_list.html new file mode 100644 index 0000000..c5bb97b --- /dev/null +++ b/prices/templates/prices/price_list.html @@ -0,0 +1,54 @@ +{% extends "programmer/base.html" %} +{% load static %} + +{% block content %} + + +
+ + + +
+ + + + + + + + + + {% for item in price_items %} + + + + + + {% empty %} + + + + {% endfor %} + +
Наименование услугиЦена, ₽/ч
{{ forloop.counter }}{{ item.name }}{{ item.price|floatformat:0 }}
Прайс-лист временно недоступен
+
+
+ Обновлено: {% if price_items %}{{ price_items.0.updated_at|date:"d.m.Y H:i" }}{% else %}нет данных{% endif %} +
+ {% if user.is_staff %} + + {% endif %} +
+{% endblock %} \ No newline at end of file diff --git a/prices/tests.py b/prices/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/prices/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/prices/urls.py b/prices/urls.py new file mode 100644 index 0000000..92df12c --- /dev/null +++ b/prices/urls.py @@ -0,0 +1,9 @@ +from django.urls import path +from .views import PriceListView, update_prices_redirect + +app_name = 'prices' + +urlpatterns = [ + path('', PriceListView.as_view(), name='price_list'), + path('admin/update-prices/', update_prices_redirect, name='admin_update_prices'), +] \ No newline at end of file diff --git a/prices/views.py b/prices/views.py new file mode 100644 index 0000000..5f223d6 --- /dev/null +++ b/prices/views.py @@ -0,0 +1,76 @@ +import json +import base64 +from urllib.request import urlopen, Request +from urllib.error import URLError +from django.contrib import messages +from django.conf import settings +from django.contrib.admin.views.decorators import staff_member_required +from django.shortcuts import redirect +from django.views.generic import ListView +from programmer.mixins import MenuContextMixin, BreadcrumbMixin +from .models import PriceItem + +def update_prices_from_api(request): + """Загружает прайс из API с Basic Auth и обновляет таблицу PriceItem.""" + api_url = getattr(settings, 'PRICE_API_URL', 'http://localhost:81/BP/hs/webexchange/price') + username = getattr(settings, 'PRICE_API_USER', '') + password = getattr(settings, 'PRICE_API_PASSWORD', '') + + # Формируем заголовок Authorization + credentials = f"{username}:{password}" + encoded = base64.b64encode(credentials.encode('utf-8')).decode('utf-8') + headers = { + 'Authorization': f'Basic {encoded}', + 'Accept': 'application/json', + } + + try: + req = Request(api_url, headers=headers) + with urlopen(req, timeout=10) as response: + data = json.loads(response.read().decode('utf-8')) + except URLError as e: + messages.error(request, f'Ошибка соединения с API: {e}') + return False + except json.JSONDecodeError as e: + messages.error(request, f'Ошибка парсинга JSON: {e}') + return False + + if data.get('error', True): + messages.error(request, 'API вернул ошибку') + return False + + items = data.get('result', []) + if not items: + messages.warning(request, 'API вернул пустой список') + return False + + # Обновляем + PriceItem.objects.all().delete() + objs = [PriceItem(name=item['name'], price=item['price']) for item in items] + PriceItem.objects.bulk_create(objs) + + messages.success(request, f'Прайс обновлён: загружено {len(objs)} услуг') + return True + +@staff_member_required +def update_prices_redirect(request): + update_prices_from_api(request) + return redirect('admin:prices_priceitem_changelist') + +class PriceListView(MenuContextMixin, BreadcrumbMixin, ListView): + model = PriceItem + template_name = 'prices/price_list.html' + context_object_name = 'price_items' + paginate_by = 20 + + def get_breadcrumbs(self): + return [{'title': 'Прайс-лист', 'url_name': None}] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context.update({ + 'title': 'Прайс-лист на услуги 1С | С.Н.А. Технологии', + 'meta_description': 'Актуальные цены на услуги программиста 1С: аудит, обновление, разработка, интеграция и другие.', + 'meta_keywords': 'прайс 1С, цены на услуги 1С, стоимость разработки 1С', + }) + return context \ No newline at end of file diff --git a/programmer/mixins.py b/programmer/mixins.py index a3031db..d08dc89 100644 --- a/programmer/mixins.py +++ b/programmer/mixins.py @@ -21,6 +21,7 @@ class MenuContextMixin(ContextMixin): {'title': "Кейсы", 'url_name': 'solution'}, {'title': "Статьи", 'url_name': 'blog:article_list'}, {'title': "Продукты", 'url_name': 'products:product_list'}, + {'title': "Прайс на услуги", 'url_name': 'prices:price_list'}, ] }, {'title': "Отзывы", 'url_name': 'recall'}, diff --git a/requirements.txt b/requirements.txt index 3d5ee78..51ab010 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,4 +15,5 @@ celery django-background-tasks celery>=5.3.0 redis>=5.0.0 -django-celery-results>=2.5.0 \ No newline at end of file +django-celery-results>=2.5.0 +requests \ No newline at end of file