Skip to content

Commit

Permalink
Merge pull request #38 from hackathone-prosept-team2/feat/backup
Browse files Browse the repository at this point in the history
Feat/backup
  • Loading branch information
ratarov authored Dec 14, 2023
2 parents 7370ad0 + e636aaa commit 594dcd1
Show file tree
Hide file tree
Showing 20 changed files with 247 additions and 78 deletions.
16 changes: 13 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
# YANDEX & PROSEPT HACKATHON: Сервис сопоставления товаров Просепт и наименований дилеров. Команда 2.
http://81.31.246.5/ <br>
http://81.31.246.5/backup/ - временный вход <br>
http://81.31.246.5/<br>
http://81.31.246.5/backup/ - прототип на Django-templates, пока не закончен основной фронтенд<br>
данные для пробного входа на сайт и в админ-панель
```
email: [email protected]
password: Password-123
```
## Архивы и фото приложения
## Описание
Заказчик предоставил файлы csv: список своих товаров, список дилеров и результаты ежедневного парсинга сайтов дилеров с ценами.<br>
*Проблема:*<br>
Заказчику необходимо контролировать цены на свою продукцию на сайтах дилеров, но наименования на сайтах значительно отличаются от оригиналльных, и их приходится сопоставлять вручную, что очень трудоемко. Необходимо разработать сервис сопоставления наименований и подбора рекомендаций для операторов - вывод наиболее вероятных подходящих наименований.<br>
*Решение:*<br>
Бэкенд загружает данных цен; составляет список уникальных пар ключ(артикул)-дилер; проверяет, есть ли уже подтвержденные оператором товары; ключи без продукта отправляются на обработку в ML-сервис рекомендаций, который возвращает топ-10 наиболее вероятных совпадений.<br>
Оператор может просмотреть список ключей, отфильтровать их по артикулу/наименованию, статусу и дилеру; открыть нужный ключ, выбрать подходящий товар и создать связь ключ-товар, либо пометить все товары как неподходящие.<br>
Доступен отчет в разрезе дилеров со статистикой загрузки цен, уникальных ключей, с кол-вом ключей, которые надо проверить и количеством принятых решений (подошло/не подошло).<br>
API позволяет выгрузить все пары ключ-товар с опциями "все новые"/"за период"/"в дату".

## Фото приложения
[Фото основных страниц](https://github.com/hackathone-prosept-team2/backend_django/tree/main/presentation)

## FRONTEND:
Expand Down
30 changes: 30 additions & 0 deletions apps/dealers/filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.db.models import QuerySet, Q
from django.http import HttpRequest

from config.constants import MATCH_NUMBER

from .models import DealerKey, Match


def filter_keys(
qs: QuerySet[DealerKey], request: HttpRequest
) -> QuerySet[DealerKey]:
"""Фильтрация списка ключей по наименованию/артикулу/статусу/дилеру."""
text = request.GET.get("text")
status = request.GET.get("status")
dealer = request.GET.get("dealer")

if text:
qs = qs.filter(Q(name__icontains=text) | Q(key__icontains=text))
if dealer:
qs = qs.filter(dealer=dealer)
if status:
if status == Match.MatchStatus.YES:
return qs.filter(product_id__isnull=False)
if status == Match.MatchStatus.NO:
return qs.filter(declined=MATCH_NUMBER)
else:
return qs.filter(
product_id__isnull=True, declined__lt=MATCH_NUMBER
)
return qs
12 changes: 12 additions & 0 deletions apps/dealers/forms.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from django import forms

from .crud import list_dealers
from .models import Match

STATUS_CHOICES = [("", "---------")] + Match.MatchStatus.choices


class FilterForm(forms.Form):
text = forms.CharField(required=False)
status = forms.ChoiceField(choices=STATUS_CHOICES, required=False)
dealer = forms.ModelChoiceField(queryset=list_dealers(), required=False)
5 changes: 5 additions & 0 deletions apps/dealers/templatetags/user_filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
register = template.Library()


@register.filter
def addclass(field, css):
return field.as_widget(attrs={"class": css})


@register.filter
def substract(value, arg):
try:
Expand Down
14 changes: 13 additions & 1 deletion apps/dealers/views.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from typing import Any
from django.db.models.query import QuerySet
from django.http import (
HttpRequest,
HttpResponsePermanentRedirect,
Expand All @@ -9,6 +11,8 @@
from config.constants import COMMON_PAGE

from .crud import key_details, list_dealers_report_data, list_keys
from .filters import filter_keys
from .forms import FilterForm
from .models import Dealer, DealerKey
from .services import choose_match, decline_matches

Expand All @@ -19,7 +23,15 @@ class KeysView(ListView):
model = DealerKey
template_name = "dealers/index.html"
paginate_by = COMMON_PAGE
queryset = list_keys()

def get_context_data(self, **kwargs: Any) -> dict[str, Any]:
context = super().get_context_data(**kwargs)
form = FilterForm(data=self.request.GET or None)
context["form"] = form
return context

def get_queryset(self) -> QuerySet[Any]:
return filter_keys(qs=list_keys(), request=self.request)


class KeysDetailView(DetailView):
Expand Down
10 changes: 10 additions & 0 deletions apps/prices/urls.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from django.urls import path

from .views import DeleteAllPrices, ImportPrices

app_name = "prices"

urlpatterns = [
path("prices/import/", ImportPrices.as_view(), name="import_prices"),
path("prices/delete/", DeleteAllPrices.as_view(), name="delete_prices"),
]
30 changes: 30 additions & 0 deletions apps/prices/views.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from django.http import (
HttpRequest,
HttpResponsePermanentRedirect,
)
from django.shortcuts import redirect
from django.views import View

from .crud import there_are_prices_in_db
from .services import create_prices, delete_prices_and_relations


class ImportPrices(View):
"""Загрузка всех цен из стартового файла."""

def get(
self, request: HttpRequest, *args, **kwargs
) -> HttpResponsePermanentRedirect:
if not there_are_prices_in_db():
create_prices()
return redirect("dealers:index")


class DeleteAllPrices(View):
"""Удаление всех загруженных цен и принятых решений по подбору."""

def get(
self, request: HttpRequest, *args, **kwargs
) -> HttpResponsePermanentRedirect:
delete_prices_and_relations()
return redirect("dealers:index")
1 change: 1 addition & 0 deletions config/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
urlpatterns = [
path("admin/", admin.site.urls),
path("backup/", include("apps.dealers.urls", namespace="dealers")),
path("backup/", include("apps.prices.urls", namespace="prices")),
path("api/v1/", include("apps.api.v1.urls")),
path("api/schema/", SpectacularAPIView.as_view(), name="schema"),
path(
Expand Down
Binary file modified presentation/1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified presentation/2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified presentation/3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified presentation/4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified presentation/5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added presentation/6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added presentation/7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added presentation/8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added presentation/9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
159 changes: 89 additions & 70 deletions templates/dealers/details.html
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ <h1>Информация по подобранным продуктам и це
<tbody>
<tr>
<td>{{ object.dealer.name }}</td>
<td>{{ object.key }}</td>
<td>{{ object.key|truncatechars:20 }}</td>
<td>{{ object.name }}</td>
<td>
{% if object.product_id %}Подобран
{% if object.product_id %}Подтверждено
{% elif object.declined == 10 %}Не подходит
{% else %}На проверку
{% endif %}
Expand All @@ -33,75 +33,94 @@ <h1>Информация по подобранным продуктам и це
</table>
<br>
<br>
<h4>Список подходящих продуктов:</h4>
{% if object.matches.count > 1 %}
<a href="{% url 'dealers:decline_all' object.id %}" class=" btn btn-sm btn-danger">
Ничего не подходит
</a>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Артикул</th>
<th scope="col">Наименование</th>
<th scope="col">Себестоимость</th>
<th scope="col">Рек.цена</th>
<th scope="col">Статус подбора</th>
<th scope="col">Показатель схожести</th>
<th scope="col"></th>
</tr>
</thead>
<tbody>
{% for prod_match in object.matches.all %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ prod_match.product.article }}</td>
<td>{{ prod_match.product.name }}</td>
<td>{{ prod_match.product.cost }}</td>
<td>{{ prod_match.product.recommended_price }}</td>
<td>{{ prod_match.get_status_display }}</td>
<td>{{ prod_match.similarity }}%</td>
<td>
<a href="{% url 'dealers:choose_product' pk=object.id prod_id=prod_match.product_id %}"
class="btn btn-sm btn-danger">
Выбрать
</a>
</td>
</tr>
{% empty %}
<tr>
<td scope="col"></td>
<td scope="col"></td>
<td scope="col">Нет подобранных рекомендаций</td>
<td scope="col"></td>
<td scope="col"></td>
<td scope="col"></td>
<td scope="col"></td>
</tr>
{% endfor %}
</tbody>
</table>
<h4>
Список подходящих продуктов:
<a class="btn btn-primary btn-sm" data-bs-toggle="collapse" href="#matches" role="button" aria-expanded="true"
aria-controls="matches">
Скрыть / Показать
</a>
</h4>
<div class="collapse multi-collapse show" id="matches">

{% if object.matches.count > 1 %}
<a href="{% url 'dealers:decline_all' object.id %}" class=" btn btn-sm btn-danger">
Ничего не подходит
</a>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">Артикул</th>
<th scope="col">Наименование</th>
<th scope="col">Себестоимость</th>
<th scope="col">Рек.цена</th>
<th scope="col">Статус подбора</th>
<th scope="col">Показатель схожести</th>
<th scope="col">Решение</th>
</tr>
</thead>
<tbody>
{% for prod_match in object.matches.all %}
<tr>
<td>{{ forloop.counter }}</td>
<td>{{ prod_match.product.article }}</td>
<td>{{ prod_match.product.name }}</td>
<td>{{ prod_match.product.cost }}</td>
<td>{{ prod_match.product.recommended_price }}</td>
<td>{{ prod_match.get_status_display }}</td>
<td>{{ prod_match.similarity }}%</td>
<td>
<a href="{% url 'dealers:choose_product' pk=object.id prod_id=prod_match.product_id %}"
class="btn btn-sm btn-danger">
Выбрать
</a>
</td>
</tr>
{% empty %}
<tr>
<td scope="col"></td>
<td scope="col"></td>
<td scope="col">Нет подобранных рекомендаций</td>
<td scope="col"></td>
<td scope="col"></td>
<td scope="col"></td>
<td scope="col"></td>
</tr>
{% endfor %}
</tbody>
</table>

</div>

<br>
<br>
<h4>История цен:</h4>
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Дата</th>
<th scope="col">Наименование</th>
<th scope="col">Цена</th>
</tr>
</thead>
<tbody>
{% for price in object.prices.all %}
<tr>
<td>{{ price.date }}</td>
<td>{{ price.name }}</td>
<td>{{ price.price }}</td>
</tr>
<h4>
История цен:
<a class="btn btn-primary btn-sm" data-bs-toggle="collapse" href="#prices" role="button" aria-expanded="true"
aria-controls="prices">
Скрыть / Показать
</a>
</h4>
<div class="collapse multi-collapse show" id="prices">
<table class="table table-striped">
<thead>
<tr>
<th scope="col">Дата</th>
<th scope="col">Наименование</th>
<th scope="col">Цена</th>
</tr>
</thead>
<tbody>
{% for price in object.prices.all %}
<tr>
<td>{{ price.date }}</td>
<td>{{ price.name }}</td>
<td>{{ price.price }}</td>
</tr>

{% endfor %}
</tbody>
</table>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
Loading

0 comments on commit 594dcd1

Please sign in to comment.