Добавил прайс страницу

This commit is contained in:
NikDizell 2026-07-13 22:37:41 +03:00
parent 8ae6e4c4f8
commit 2a93cd8a8f
13 changed files with 207 additions and 1 deletions

View File

@ -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 = [

View File

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

0
prices/__init__.py Normal file
View File

35
prices/admin.py Normal file
View File

@ -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)

6
prices/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PricesConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'prices'

View File

15
prices/models.py Normal file
View File

@ -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}"

View File

@ -0,0 +1,54 @@
{% extends "programmer/base.html" %}
{% load static %}
{% block content %}
<div class="page-header">
<h1 class="page-title">Прайс-лист на услуги</h1>
<p class="page-subtitle">Актуальные цены на услуги программиста 1С</p>
</div>
<div class="content-card">
<!-- Информационный блок о ценах -->
<div class="alert alert-info mb-4" role="alert">
<strong> Обратите внимание!</strong> В прайс-листе указаны <strong>минимальные цены (ставка часа)</strong> на услуги.
Итоговая стоимость рассчитывается индивидуально в зависимости от объёма и сложности работ.
Для разовых заказов цена может отличаться — пожалуйста, <a href="#" onclick="CallbackModal.open(); return false;" class="alert-link">уточняйте точную стоимость</a> при обращении.
При долгосрочном сотрудничестве мы предлагаем гибкие условия и скидки.
</div>
<div class="table-responsive">
<table class="table table-striped">
<thead>
<tr>
<th></th>
<th>Наименование услуги</th>
<th>Цена, ₽/ч</th>
</tr>
</thead>
<tbody>
{% for item in price_items %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ item.name }}</td>
<td>{{ item.price|floatformat:0 }}</td>
</tr>
{% empty %}
<tr>
<td colspan="3" class="text-center">Прайс-лист временно недоступен</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="text-muted mt-3">
<small>Обновлено: {% if price_items %}{{ price_items.0.updated_at|date:"d.m.Y H:i" }}{% else %}нет данных{% endif %}</small>
</div>
{% if user.is_staff %}
<div class="mt-3">
<a href="{% url 'prices:admin_update_prices' %}" class="btn btn-secondary btn-sm">
🔄 Обновить прайс (админ)
</a>
</div>
{% endif %}
</div>
{% endblock %}

3
prices/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

9
prices/urls.py Normal file
View File

@ -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'),
]

76
prices/views.py Normal file
View File

@ -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

View File

@ -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'},

View File

@ -16,3 +16,4 @@ django-background-tasks
celery>=5.3.0
redis>=5.0.0
django-celery-results>=2.5.0
requests