diff --git a/myproject/myproject/settings.py b/myproject/myproject/settings.py index a8b8c96..4c65af0 100644 --- a/myproject/myproject/settings.py +++ b/myproject/myproject/settings.py @@ -74,6 +74,7 @@ TENANT_APPS = [ 'products', # Товары и категории 'orders', # Заказы 'inventory', # Складской учет + 'pos', # POS Terminal ] # Объединяем для INSTALLED_APPS diff --git a/myproject/myproject/urls.py b/myproject/myproject/urls.py index 6f9f273..f0ca567 100644 --- a/myproject/myproject/urls.py +++ b/myproject/myproject/urls.py @@ -21,6 +21,7 @@ urlpatterns = [ path('customers/', include('customers.urls')), # Управление клиентами path('inventory/', include('inventory.urls')), # Управление складом path('orders/', include('orders.urls')), # Управление заказами + path('pos/', include('pos.urls')), # POS Terminal ] # Serve media files during development diff --git a/myproject/orders/models.py b/myproject/orders/models.py index 54e71a4..af52d17 100644 --- a/myproject/orders/models.py +++ b/myproject/orders/models.py @@ -495,10 +495,14 @@ class Order(models.Model): return f"Заказ #{self.order_number} - {self.customer}" def save(self, *args, **kwargs): - # Генерируем уникальный номер заказа при создании + # Генерируем уникальный номер заказа при создании (начиная с 100 для 3-значного поиска) if not self.order_number: last_order = Order.objects.order_by('-order_number').first() - self.order_number = (last_order.order_number if last_order else 0) + 1 + if last_order: + # Если ранее нумерация была ниже 100, начинаем с 100; иначе инкремент + self.order_number = max(last_order.order_number + 1, 100) + else: + self.order_number = 100 super().save(*args, **kwargs) def clean(self): diff --git a/myproject/pos/__init__.py b/myproject/pos/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/pos/admin.py b/myproject/pos/admin.py new file mode 100644 index 0000000..0c4ad6d --- /dev/null +++ b/myproject/pos/admin.py @@ -0,0 +1,3 @@ +from django.contrib import admin + +# POS admin (placeholder) diff --git a/myproject/pos/apps.py b/myproject/pos/apps.py new file mode 100644 index 0000000..c13bbe7 --- /dev/null +++ b/myproject/pos/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class PosConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'pos' + verbose_name = 'POS Terminal' diff --git a/myproject/pos/migrations/__init__.py b/myproject/pos/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/myproject/pos/models.py b/myproject/pos/models.py new file mode 100644 index 0000000..5f0effc --- /dev/null +++ b/myproject/pos/models.py @@ -0,0 +1,3 @@ +from django.db import models + +# POS app models (placeholder for future extensions) diff --git a/myproject/pos/static/pos/css/terminal.css b/myproject/pos/static/pos/css/terminal.css new file mode 100644 index 0000000..806b1be --- /dev/null +++ b/myproject/pos/static/pos/css/terminal.css @@ -0,0 +1,53 @@ +/* POS Terminal Styles */ + +body { + background-color: #e9ecef; +} + +.pos-container { + max-width: 100%; + padding: 0 1rem; +} + +.product-card { + cursor: pointer; + user-select: none; + transition: all 0.2s; + border-radius: 12px; + border: 1px solid #dee2e6; + background: white; + height: 100%; + min-height: 140px; +} + +.product-card:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); +} + +.product-card.selected { + background: #e7f3ff; + border-color: #0d6efd; +} + +.product-card .card-body { + padding: 1.25rem; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + text-align: center; +} + +.product-name { + font-weight: 500; + font-size: 1rem; + margin-bottom: 0.5rem; + color: #495057; +} + +.product-stock { + font-size: 0.9rem; + color: #6c757d; + font-style: italic; +} diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js new file mode 100644 index 0000000..3bc0efc --- /dev/null +++ b/myproject/pos/static/pos/js/terminal.js @@ -0,0 +1,170 @@ +// POS Terminal JavaScript + +const CATEGORIES = JSON.parse(document.getElementById('categoriesData').textContent); +const PRODUCTS = JSON.parse(document.getElementById('productsData').textContent); + +let currentCategoryId = null; +const cart = new Map(); // productId -> {id, name, price, qty} + +function formatMoney(v) { + return (Number(v)).toFixed(2); +} + +function renderProducts() { + const grid = document.getElementById('productGrid'); + grid.innerHTML = ''; + const searchTerm = document.getElementById('searchInput').value.toLowerCase(); + + let filtered = currentCategoryId + ? PRODUCTS.filter(p => (p.category_ids || []).includes(currentCategoryId)) + : PRODUCTS; + + if (searchTerm) { + filtered = filtered.filter(p => p.name.toLowerCase().includes(searchTerm)); + } + + filtered.forEach(p => { + const col = document.createElement('div'); + col.className = 'col-6 col-sm-4 col-md-3 col-lg-2'; + + const card = document.createElement('div'); + card.className = 'card product-card'; + card.onclick = () => addToCart(p); + + const body = document.createElement('div'); + body.className = 'card-body'; + + const name = document.createElement('div'); + name.className = 'product-name'; + name.textContent = p.name; + + const stock = document.createElement('div'); + stock.className = 'product-stock'; + stock.textContent = 'В наличии'; + + body.appendChild(name); + body.appendChild(stock); + card.appendChild(body); + col.appendChild(card); + grid.appendChild(col); + }); +} + +function addToCart(p) { + if (!cart.has(p.id)) { + cart.set(p.id, { id: p.id, name: p.name, price: Number(p.price), qty: 1 }); + } else { + cart.get(p.id).qty += 1; + } + renderCart(); +} + +function updateQty(id, delta) { + if (!cart.has(id)) return; + const item = cart.get(id); + item.qty += delta; + if (item.qty <= 0) cart.delete(id); + renderCart(); +} + +function renderCart() { + const list = document.getElementById('cartList'); + list.innerHTML = ''; + let total = 0; + + if (cart.size === 0) { + list.innerHTML = '

Корзина пуста

'; + document.getElementById('cartTotal').textContent = '0.00'; + return; + } + + cart.forEach(item => { + const row = document.createElement('div'); + row.className = 'mb-2 pb-2 border-bottom'; + + const nameRow = document.createElement('div'); + nameRow.className = 'd-flex justify-content-between align-items-start mb-1'; + nameRow.innerHTML = ` +
${item.name}
+ + `; + + const controlsRow = document.createElement('div'); + controlsRow.className = 'd-flex justify-content-between align-items-center'; + + const controls = document.createElement('div'); + controls.className = 'btn-group btn-group-sm'; + + const minus = document.createElement('button'); + minus.className = 'btn btn-outline-secondary'; + minus.innerHTML = ''; + minus.onclick = (e) => { e.stopPropagation(); updateQty(item.id, -1); }; + + const qtySpan = document.createElement('button'); + qtySpan.className = 'btn btn-outline-secondary disabled'; + qtySpan.textContent = item.qty; + + const plus = document.createElement('button'); + plus.className = 'btn btn-outline-secondary'; + plus.innerHTML = ''; + plus.onclick = (e) => { e.stopPropagation(); updateQty(item.id, +1); }; + + controls.appendChild(minus); + controls.appendChild(qtySpan); + controls.appendChild(plus); + + const priceDiv = document.createElement('div'); + priceDiv.className = 'text-end small'; + priceDiv.innerHTML = `${formatMoney(item.price * item.qty)}`; + + controlsRow.appendChild(controls); + controlsRow.appendChild(priceDiv); + + row.appendChild(nameRow); + row.appendChild(controlsRow); + list.appendChild(row); + + total += item.qty * item.price; + }); + + document.getElementById('cartTotal').textContent = formatMoney(total); +} + +function removeFromCart(id) { + cart.delete(id); + renderCart(); +} + +function clearCart() { + cart.clear(); + renderCart(); +} + +document.getElementById('clearCart').onclick = clearCart; + +// Заглушки для функционала (будет реализовано позже) +document.getElementById('checkoutNow').onclick = async () => { + alert('Функционал будет подключен позже: создание заказа и списание со склада.'); +}; + +document.getElementById('scheduleLater').onclick = async () => { + alert('Функционал будет подключен позже: создание заказа на доставку/самовывоз.'); +}; + +// Categories removed from this view - can be added as filter dropdown later if needed + +// Search functionality +document.getElementById('searchInput').addEventListener('input', () => { + renderProducts(); +}); + +// Customer selection +document.getElementById('customerSelectBtn').addEventListener('click', () => { + alert('Функция выбора клиента будет реализована позже'); +}); + +// Инициализация +renderProducts(); +renderCart(); diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html new file mode 100644 index 0000000..ed79935 --- /dev/null +++ b/myproject/pos/templates/pos/terminal.html @@ -0,0 +1,122 @@ +{% extends 'base.html' %} +{% load static %} +{% block title %}POS Terminal{% endblock %} + +{% block extra_css %} + +{% endblock %} + +{% block content %} + +
+
+ +
+ +
+ +
+ +
+
+ + +
+ +
+
+
Корзина
+ +
+
+
+ +
+
+
Итого:
+
0.00
+
+ +
+ + +
+ +
+ + + +
+
+
+
+ + +
+
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+ +{% endblock %} + +{% block extra_js %} + + + + + +{% endblock %} diff --git a/myproject/pos/urls.py b/myproject/pos/urls.py new file mode 100644 index 0000000..4a7a84c --- /dev/null +++ b/myproject/pos/urls.py @@ -0,0 +1,9 @@ +# -*- coding: utf-8 -*- +from django.urls import path +from . import views + +app_name = 'pos' + +urlpatterns = [ + path('', views.pos_terminal, name='terminal'), +] diff --git a/myproject/pos/views.py b/myproject/pos/views.py new file mode 100644 index 0000000..99f3d3e --- /dev/null +++ b/myproject/pos/views.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from django.shortcuts import render +from django.contrib.auth.decorators import login_required +from products.models import Product, ProductCategory +import json + + +@login_required +def pos_terminal(request): + """ + Tablet-friendly POS screen prototype. + Shows categories and in-stock products for quick tap-to-add. + """ + categories_qs = ProductCategory.objects.filter(is_active=True) + products_qs = Product.objects.filter(in_stock=True).prefetch_related('categories') + + categories = [{'id': c.id, 'name': c.name} for c in categories_qs] + products = [{ + 'id': p.id, + 'name': p.name, + 'price': str(p.actual_price), + 'category_ids': [c.id for c in p.categories.all()] + } for p in products_qs] + + context = { + 'categories_json': json.dumps(categories), + 'products_json': json.dumps(products), + 'title': 'POS Terminal', + } + return render(request, 'pos/terminal.html', context) diff --git a/myproject/templates/navbar.html b/myproject/templates/navbar.html index 7e89e46..6442040 100644 --- a/myproject/templates/navbar.html +++ b/myproject/templates/navbar.html @@ -39,7 +39,7 @@ Клиенты