Files
octopus/myproject/pos/templates/pos/terminal.html
Andrey Smakotin 87cba63c47 Fix: Add _open() and path() methods to TenantAwareFileSystemStorage
Critical fix for Celery photo processing. The storage class now correctly
handles file reading operations by automatically adding tenant_id prefix
when opening files.

Problems fixed:
- Celery tasks could not open image files from storage
- PIL/Pillow couldn't locate files in tenant-specific directories
- temp file deletion was failing due to path validation

Changes:
- Added _open() method to add tenant_id prefix when opening files
- Added path() method to convert relative paths to full filesystem paths
- Updated delete() method to handle paths with or without tenant prefix
- All methods include security checks to prevent cross-tenant access

Testing:
- All 5 existing tests pass
- Verified photo processing task works end-to-end:
  * Reads temp image file from disk
  * Processes and creates all image versions
  * Saves processed files to tenant-specific directory
  * Cleans up temporary files correctly
- Files correctly stored in: media/tenants/{tenant_id}/products/{product_id}/{photo_id}/

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-23 20:30:52 +03:00

528 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends 'base.html' %}
{% load static %}
{% block title %}POS Terminal{% endblock %}
{% block extra_css %}
<link rel="stylesheet" href="{% static 'pos/css/terminal.css' %}">
{% endblock %}
{% block content %}
<!-- Main POS Container -->
<div class="pos-main-container">
<div class="pos-container">
<div class="row g-3" style="height: 100%;">
<!-- Products Grid (Left side - 8/12) -->
<div class="col-md-8" style="display: flex; flex-direction: column; height: 100%;">
<!-- Search Box -->
<div class="mb-3">
<div class="input-group">
<input type="text" class="form-control" id="searchInput" placeholder="Поиск по товарам (мин. 3 символа)...">
<button class="btn btn-outline-secondary" type="button" id="clearSearchBtn" style="display: none;">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<!-- Categories -->
<div class="mb-3">
<div class="row g-3" id="categoryGrid"></div>
</div>
<!-- Products Grid (Блок товаров) - Прокручиваемая область -->
<div class="products-scrollable">
<div class="row g-3" id="productGrid"></div>
</div>
</div>
<!-- Right Panel (4/12) - Fixed -->
<div class="col-md-4">
<div class="right-panel-fixed d-flex flex-column">
<!-- Информация о складе (всегда показываем блок для фиксированной позиции) -->
<div class="card mb-2">
<div class="card-body py-2 px-3 d-flex justify-content-between align-items-center">
{% if current_warehouse %}
<div>
<small class="text-muted d-block" style="font-size: 0.75rem;">Склад:</small>
<strong style="font-size: 0.95rem;">{{ current_warehouse.name }}</strong>
</div>
<button class="btn btn-sm btn-outline-secondary" id="changeWarehouseBtn" style="font-size: 0.75rem;">
<i class="bi bi-arrow-left-right"></i> Сменить
</button>
{% else %}
<div class="text-danger">
<small class="d-block" style="font-size: 0.75rem;">Склад:</small>
<strong style="font-size: 0.95rem;"><i class="bi bi-exclamation-triangle me-1"></i>Не выбран</strong>
</div>
{% endif %}
</div>
</div>
<!-- Cart Panel -->
<div class="card mb-2 flex-grow-1" style="min-height: 0;">
<div class="card-header bg-white d-flex justify-content-between align-items-center">
<h6 class="mb-0">Корзина</h6>
<div class="d-flex gap-1 align-items-center">
<button class="btn btn-outline-primary btn-sm d-flex align-items-center" id="customerSelectBtn">
<i class="bi bi-person me-1"></i>
<div class="d-flex flex-column align-items-start lh-1">
<small class="text-muted" style="font-size: 0.65rem;">Клиент</small>
<span id="customerSelectBtnText" class="fw-semibold">Выбрать</span>
</div>
</button>
<button class="btn btn-sm btn-outline-danger" id="resetCustomerBtn" title="Сбросить на системного клиента" style="display: none;">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<div class="card-body d-flex flex-column" style="min-height: 0;">
<div id="cartList" class="flex-grow-1" style="overflow-y: auto;"></div>
<div class="mt-auto">
<div class="d-flex justify-content-between align-items-center py-1 border-top">
<strong class="mb-0">Итого:</strong>
<strong class="mb-0 text-primary fs-5" id="cartTotal">0.00</strong>
</div>
</div>
</div>
</div>
<!-- Action Buttons Panel -->
<div class="card action-buttons-panel">
<div class="card-body p-2">
<div class="row g-2">
<div class="col-4">
<button class="btn btn-outline-warning rounded-3 w-100" id="addToShowcaseBtn" style="height: 60px;">
<i class="bi bi-flower1"></i><br>На витрину
</button>
</div>
<div class="col-4">
<button class="btn btn-outline-secondary rounded-3 w-100" style="height: 60px;">
</button>
</div>
<div class="col-4">
<button class="btn btn-outline-secondary rounded-3 w-100" style="height: 60px;">
</button>
</div>
<div class="col-4">
<button class="btn btn-outline-secondary rounded-3 w-100" style="height: 60px;">
</button>
</div>
<div class="col-4">
<button class="btn btn-outline-secondary rounded-3 w-100" style="height: 60px;">
</button>
</div>
<div class="col-4">
<button class="btn btn-outline-primary rounded-3 w-100" id="scheduleLater" style="height: 60px;">
<i class="bi bi-calendar2"></i><br>Отложенный заказ
</button>
</div>
<div class="col-4">
<button class="btn btn-outline-secondary rounded-3 w-100" id="clearCart" style="height: 60px;">
<i class="bi bi-trash"></i><br>Очистить
</button>
</div>
<div class="col-4">
<button class="btn btn-outline-secondary rounded-3 w-100" style="height: 60px;">
</button>
</div>
<div class="col-4">
<button class="btn btn-success rounded-3 w-100" id="checkoutNow" style="height: 60px;">
<i class="bi bi-check2-circle"></i><br>Продать
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Modal: Создание временного комплекта на витрину -->
<div class="modal fade" id="createTempKitModal" tabindex="-1" aria-labelledby="createTempKitModalLabel" aria-hidden="true">
<div class="modal-dialog modal-xl">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createTempKitModalLabel">
<i class="bi bi-flower1"></i> Создать временный комплект на витрину
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<!-- Левая колонка: основные поля -->
<div class="col-md-6">
<!-- Название комплекта -->
<div class="mb-3">
<label for="tempKitName" class="form-label">Название комплекта *</label>
<input type="text" class="form-control" id="tempKitName" placeholder="Введите название" required>
</div>
<!-- Выбор витрины -->
<div class="mb-3">
<label for="showcaseSelect" class="form-label">Витрина *</label>
<select class="form-select" id="showcaseSelect" required>
<option value="">Загрузка...</option>
</select>
</div>
<!-- Описание -->
<div class="mb-3">
<label for="tempKitDescription" class="form-label">Описание (опционально)</label>
<textarea class="form-control" id="tempKitDescription" rows="3" placeholder="Краткое описание комплекта"></textarea>
</div>
<!-- Загрузка фото -->
<div class="mb-3">
<label for="tempKitPhoto" class="form-label">Фото комплекта (опционально)</label>
<input type="file" class="form-control" id="tempKitPhoto" accept="image/*">
<div id="photoPreview" class="mt-2" style="display: none;">
<img id="photoPreviewImg" src="" alt="Preview" class="img-thumbnail" style="max-width: 200px; max-height: 200px;">
<button type="button" class="btn btn-sm btn-danger ms-2" id="removePhoto">
<i class="bi bi-x"></i> Удалить
</button>
</div>
</div>
</div>
<!-- Правая колонка: цены и состав -->
<div class="col-md-6">
<!-- Список товаров в корзине -->
<div class="mb-3">
<label class="form-label">Товары в комплекте</label>
<div class="border rounded p-2" id="tempKitItemsList" style="max-height: 200px; overflow-y: auto; background: #f8f9fa;">
<!-- Динамически заполняется через JS -->
</div>
</div>
<!-- Блок ценообразования -->
<div class="card">
<div class="card-header bg-light">
<strong>Ценообразование</strong>
</div>
<div class="card-body">
<!-- Базовая цена -->
<div class="mb-2">
<small class="text-muted">Базовая цена (сумма компонентов):</small>
<div class="fw-bold" id="tempKitBasePrice">0.00 руб.</div>
</div>
<!-- Корректировка цены -->
<div class="mb-2">
<label for="priceAdjustmentType" class="form-label small">Корректировка цены</label>
<select class="form-select form-select-sm" id="priceAdjustmentType">
<option value="none">Без изменения</option>
<option value="increase_percent">Увеличить на %</option>
<option value="increase_amount">Увеличить на сумму</option>
<option value="decrease_percent">Уменьшить на %</option>
<option value="decrease_amount">Уменьшить на сумму</option>
</select>
</div>
<div class="mb-2" id="adjustmentValueBlock" style="display: none;">
<label for="priceAdjustmentValue" class="form-label small">Значение</label>
<input type="number" class="form-control form-control-sm" id="priceAdjustmentValue"
min="0" step="0.01" value="0" placeholder="0.00">
</div>
<!-- Расчётная цена с корректировкой -->
<div class="mb-2 pb-2 border-bottom">
<small class="text-muted">Расчётная цена:</small>
<div class="fw-bold text-primary" id="tempKitCalculatedPrice">0.00 руб.</div>
</div>
<!-- Ручная финальная цена (sale_price) -->
<div class="mb-0">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="useSalePrice">
<label class="form-check-label small" for="useSalePrice">
Установить свою цену (приоритет)
</label>
</div>
<div id="salePriceBlock" style="display: none;">
<input type="number" class="form-control form-control-sm" id="salePrice"
min="0" step="0.01" placeholder="Введите цену">
</div>
</div>
<!-- Итоговая цена продажи -->
<div class="alert alert-success mt-3 mb-0">
<strong>Итоговая цена продажи:</strong><br>
<span class="fs-4" id="tempKitFinalPrice">0.00</span> руб.
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<!-- Кнопка "Разобрать" (отображается только в режиме редактирования) -->
<button type="button" class="btn btn-danger me-auto" id="disassembleKitBtn" style="display: none;">
<i class="bi bi-scissors"></i> Разобрать букет
</button>
<!-- Правая группа кнопок -->
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirmCreateTempKit">
<i class="bi bi-check-circle"></i> Создать и зарезервировать
</button>
</div>
</div>
</div>
</div>
<!-- Модалка: Продажа -->
<div class="modal fade" id="checkoutModal" tabindex="-1" aria-labelledby="checkoutModalLabel" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="checkoutModalLabel">
<i class="bi bi-cash-stack"></i> Продажа
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="row">
<!-- Левая колонка: состав заказа -->
<div class="col-md-7">
<!-- Информация о клиенте -->
<div class="mb-3">
<label class="form-label fw-semibold">Клиент</label>
<div class="d-flex gap-1 align-items-center">
<button class="btn btn-outline-primary btn-sm d-flex align-items-center" id="checkoutCustomerSelectBtn">
<i class="bi bi-person me-1"></i>
<div class="d-flex flex-column align-items-start lh-1">
<small class="text-muted" style="font-size: 0.65rem;">Клиент</small>
<span id="checkoutCustomerSelectBtnText" class="fw-semibold">Выбрать</span>
</div>
</button>
<button class="btn btn-sm btn-outline-danger" id="checkoutResetCustomerBtn" title="Сброс на системного клиента" style="display: none;">
<i class="bi bi-x-lg"></i>
</button>
</div>
</div>
<div class="mb-3">
<strong>Состав заказа</strong>
<div class="border rounded p-3 mt-2" id="checkoutItems" style="max-height: 240px; overflow-y: auto; background: #f8f9fa;">
<!-- Заполняется из JS -->
</div>
</div>
<!-- Опции оплаты и комментарий -->
<div class="row g-3">
<div class="col-md-6">
<label for="paymentMethod" class="form-label">Способ оплаты</label>
<select class="form-select" id="paymentMethod">
<option value="cash">Наличные</option>
<option value="card">Карта</option>
<option value="mixed">Смешанная оплата</option>
</select>
</div>
<div class="col-md-6">
<label for="orderNote" class="form-label">Комментарий</label>
<input type="text" class="form-control" id="orderNote" placeholder="Примечание к заказу">
</div>
</div>
</div>
<!-- Правая колонка: ценообразование -->
<div class="col-md-5">
<div class="card">
<div class="card-header bg-light">
<strong>Ценообразование</strong>
</div>
<div class="card-body">
<!-- Базовая сумма -->
<div class="mb-2">
<small class="text-muted">Базовая сумма (корзина):</small>
<div class="fw-bold" id="checkoutBasePrice">0.00 руб.</div>
</div>
<!-- Скидка -->
<div class="mb-2">
<label for="discountType" class="form-label small">Скидка</label>
<select class="form-select form-select-sm" id="discountType">
<option value="none">Без скидки</option>
<option value="percent">Процент (%)</option>
<option value="amount">Сумма (руб.)</option>
</select>
</div>
<div class="mb-2" id="discountValueBlock" style="display: none;">
<label for="discountValue" class="form-label small">Значение скидки</label>
<input type="number" class="form-control form-control-sm" id="discountValue"
min="0" step="0.01" value="0" placeholder="0.00">
</div>
<!-- Расчётная сумма со скидкой -->
<div class="mb-2 pb-2 border-bottom">
<small class="text-muted">Сумма со скидкой:</small>
<div class="fw-bold text-success" id="checkoutDiscountedPrice">0.00 руб.</div>
</div>
<!-- Ручная финальная сумма -->
<div class="mb-0">
<div class="form-check form-switch mb-2">
<input class="form-check-input" type="checkbox" id="useManualPrice">
<label class="form-check-label small" for="useManualPrice">
Установить свою сумму (приоритет)
</label>
</div>
<div id="manualPriceBlock" style="display: none;">
<input type="number" class="form-control form-control-sm" id="manualPrice"
min="0" step="0.01" placeholder="Введите сумму">
</div>
</div>
<!-- Итоговая сумма продажи -->
<div class="alert alert-success mt-3 mb-0">
<strong>Итого к оплате:</strong><br>
<span class="fs-3" id="checkoutFinalPrice">0.00</span> руб.
</div>
</div>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-success btn-lg" id="confirmCheckoutBtn">
<i class="bi bi-check2-circle"></i> Подтвердить продажу
</button>
</div>
</div>
</div>
</div>
<!-- Модалка: Выбор склада -->
<div class="modal fade" id="selectWarehouseModal" tabindex="-1" aria-labelledby="selectWarehouseModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="selectWarehouseModalLabel">
<i class="bi bi-building"></i> Выбор склада
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="list-group" id="warehouseList">
{% for warehouse in warehouses %}
<button type="button" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center warehouse-item"
data-warehouse-id="{{ warehouse.id }}"
data-warehouse-name="{{ warehouse.name }}">
<div>
<strong>{{ warehouse.name }}</strong>
{% if warehouse.is_default %}
<span class="badge bg-warning text-dark ms-2">По умолчанию</span>
{% endif %}
</div>
{% if warehouse.id == current_warehouse.id %}
<i class="bi bi-check-circle-fill text-success"></i>
{% endif %}
</button>
{% endfor %}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
</div>
</div>
</div>
</div>
<!-- Модалка: Выбор клиента -->
<div class="modal fade" id="selectCustomerModal" tabindex="-1" aria-labelledby="selectCustomerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="selectCustomerModalLabel">
<i class="bi bi-person-search"></i> Выбор клиента
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="customerSearchInput" class="form-label">Поиск клиента</label>
<input type="text" class="form-select" id="customerSearchInput"
placeholder="Начните вводить имя, телефон или email (минимум 3 символа)">
</div>
<div class="d-grid gap-2">
<button type="button" class="btn btn-outline-success" id="createNewCustomerBtn">
<i class="bi bi-person-plus"></i> Создать нового клиента
</button>
<button type="button" class="btn btn-outline-secondary" id="selectSystemCustomerBtn">
<i class="bi bi-person"></i> Выбрать системного клиента (анонимный)
</button>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
</div>
</div>
</div>
</div>
<!-- Модалка: Создание нового клиента -->
<div class="modal fade" id="createCustomerModal" tabindex="-1" aria-labelledby="createCustomerModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createCustomerModalLabel">
<i class="bi bi-person-plus"></i> Создать клиента
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="newCustomerName" class="form-label">Имя *</label>
<input type="text" class="form-control" id="newCustomerName" placeholder="Введите имя клиента" required>
</div>
<div class="mb-3">
<label for="newCustomerPhone" class="form-label">Телефон</label>
<input type="text" class="form-control" id="newCustomerPhone" placeholder="+375XXXXXXXXX">
</div>
<div class="mb-3">
<label for="newCustomerEmail" class="form-label">Email</label>
<input type="email" class="form-control" id="newCustomerEmail" placeholder="email@example.com">
</div>
<div id="createCustomerError" class="alert alert-danger d-none"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="confirmCreateCustomerBtn">
<i class="bi bi-check-circle"></i> Создать
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<!-- Hidden data containers for JavaScript -->
<script id="categoriesData" type="application/json">{{ categories_json|safe }}</script>
<script id="itemsData" type="application/json">{{ items_json|safe }}</script>
<script id="showcaseKitsData" type="application/json">{{ showcase_kits_json|safe }}</script>
<script id="systemCustomerData" type="application/json">
{
"id": {{ system_customer.id }},
"name": "{{ system_customer.name|escapejs }}"
}
</script>
<script id="selectedCustomerData" type="application/json">
{
"id": {{ selected_customer.id }},
"name": "{{ selected_customer.name|escapejs }}"
}
</script>
<script id="cartData" type="application/json">{{ cart_data|safe }}</script>
<script src="{% static 'pos/js/terminal.js' %}"></script>
{% endblock %}