feat(pos): add editable showcase creation date for kits
- Add showcase_created_at field to ProductKit model - Display days ago as badge in product card (0 дней, 1 день, etc.) - Add date input field in edit modal - Auto-set current date/time for new showcase kits Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -98,6 +98,37 @@ function formatMoney(v) {
|
|||||||
return (Number(v)).toFixed(2);
|
return (Number(v)).toFixed(2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Форматирует дату как относительное время в русском языке
|
||||||
|
* @param {string|null} isoDate - ISO дата или null
|
||||||
|
* @returns {string} - "0 дней", "1 день", "2 дня", "5 дней", и т.д.
|
||||||
|
*/
|
||||||
|
function formatDaysAgo(isoDate) {
|
||||||
|
if (!isoDate) return '';
|
||||||
|
|
||||||
|
const created = new Date(isoDate);
|
||||||
|
const now = new Date();
|
||||||
|
const diffMs = now - created;
|
||||||
|
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||||
|
|
||||||
|
// Русские формы множественного числа
|
||||||
|
const lastTwo = diffDays % 100;
|
||||||
|
const lastOne = diffDays % 10;
|
||||||
|
|
||||||
|
let suffix;
|
||||||
|
if (lastTwo >= 11 && lastTwo <= 19) {
|
||||||
|
suffix = 'дней';
|
||||||
|
} else if (lastOne === 1) {
|
||||||
|
suffix = 'день';
|
||||||
|
} else if (lastOne >= 2 && lastOne <= 4) {
|
||||||
|
suffix = 'дня';
|
||||||
|
} else {
|
||||||
|
suffix = 'дней';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${diffDays} ${suffix}`;
|
||||||
|
}
|
||||||
|
|
||||||
// ===== ФУНКЦИИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
|
// ===== ФУНКЦИИ ДЛЯ РАБОТЫ С КЛИЕНТОМ =====
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -911,7 +942,7 @@ function renderProducts() {
|
|||||||
const stock = document.createElement('div');
|
const stock = document.createElement('div');
|
||||||
stock.className = 'product-stock';
|
stock.className = 'product-stock';
|
||||||
|
|
||||||
// Для витринных комплектов показываем название витрины И количество (доступно/всего)
|
// Для витринных комплектов показываем количество (доступно/всего) и дней на витрине
|
||||||
if (item.type === 'showcase_kit') {
|
if (item.type === 'showcase_kit') {
|
||||||
const availableCount = item.available_count || 0;
|
const availableCount = item.available_count || 0;
|
||||||
const totalCount = item.total_count || availableCount;
|
const totalCount = item.total_count || availableCount;
|
||||||
@@ -922,7 +953,14 @@ function renderProducts() {
|
|||||||
let badgeText = totalCount > 1 ? `${availableCount}/${totalCount}` : `${availableCount}`;
|
let badgeText = totalCount > 1 ? `${availableCount}/${totalCount}` : `${availableCount}`;
|
||||||
let cartInfo = inCart > 0 ? ` <span class="badge bg-warning text-dark">🛒${inCart}</span>` : '';
|
let cartInfo = inCart > 0 ? ` <span class="badge bg-warning text-dark">🛒${inCart}</span>` : '';
|
||||||
|
|
||||||
stock.innerHTML = `🌺 ${item.showcase_name} <span class="badge ${badgeClass} ms-1">${badgeText}</span>${cartInfo}`;
|
// Добавляем отображение дней с момента создания как бейдж справа
|
||||||
|
const daysAgo = formatDaysAgo(item.showcase_created_at);
|
||||||
|
const daysBadge = daysAgo ? ` <span class="badge bg-info ms-auto">${daysAgo}</span>` : '';
|
||||||
|
|
||||||
|
stock.innerHTML = `<span class="badge ${badgeClass}" style="font-size: 0.9rem;">${badgeText}</span>${daysBadge}${cartInfo}`;
|
||||||
|
stock.style.display = 'flex';
|
||||||
|
stock.style.justifyContent = 'space-between';
|
||||||
|
stock.style.alignItems = 'center';
|
||||||
stock.style.color = '#856404';
|
stock.style.color = '#856404';
|
||||||
stock.style.fontWeight = 'bold';
|
stock.style.fontWeight = 'bold';
|
||||||
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
|
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
|
||||||
@@ -1837,6 +1875,19 @@ async function openEditKitModal(kitId) {
|
|||||||
// Заполняем поля формы
|
// Заполняем поля формы
|
||||||
document.getElementById('tempKitName').value = kit.name;
|
document.getElementById('tempKitName').value = kit.name;
|
||||||
document.getElementById('tempKitDescription').value = kit.description;
|
document.getElementById('tempKitDescription').value = kit.description;
|
||||||
|
|
||||||
|
// Заполняем поле даты размещения на витрине
|
||||||
|
if (kit.showcase_created_at) {
|
||||||
|
// Конвертируем ISO в формат datetime-local (YYYY-MM-DDTHH:MM)
|
||||||
|
const date = new Date(kit.showcase_created_at);
|
||||||
|
// Компенсация смещения часового пояса
|
||||||
|
const offset = date.getTimezoneOffset() * 60000;
|
||||||
|
const localDate = new Date(date.getTime() - offset);
|
||||||
|
document.getElementById('showcaseCreatedAt').value = localDate.toISOString().slice(0, 16);
|
||||||
|
} else {
|
||||||
|
document.getElementById('showcaseCreatedAt').value = '';
|
||||||
|
}
|
||||||
|
|
||||||
document.getElementById('priceAdjustmentType').value = kit.price_adjustment_type;
|
document.getElementById('priceAdjustmentType').value = kit.price_adjustment_type;
|
||||||
document.getElementById('priceAdjustmentValue').value = kit.price_adjustment_value;
|
document.getElementById('priceAdjustmentValue').value = kit.price_adjustment_value;
|
||||||
|
|
||||||
@@ -2275,6 +2326,7 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
const kitName = document.getElementById('tempKitName').value.trim();
|
const kitName = document.getElementById('tempKitName').value.trim();
|
||||||
const showcaseId = document.getElementById('showcaseSelect').value;
|
const showcaseId = document.getElementById('showcaseSelect').value;
|
||||||
const description = document.getElementById('tempKitDescription').value.trim();
|
const description = document.getElementById('tempKitDescription').value.trim();
|
||||||
|
const showcaseCreatedAt = document.getElementById('showcaseCreatedAt').value;
|
||||||
const photoFile = document.getElementById('tempKitPhoto').files[0];
|
const photoFile = document.getElementById('tempKitPhoto').files[0];
|
||||||
|
|
||||||
// Валидация
|
// Валидация
|
||||||
@@ -2329,6 +2381,12 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
formData.append('quantity', showcaseKitQuantity); // Количество экземпляров на витрину
|
formData.append('quantity', showcaseKitQuantity); // Количество экземпляров на витрину
|
||||||
}
|
}
|
||||||
formData.append('description', description);
|
formData.append('description', description);
|
||||||
|
if (showcaseCreatedAt) {
|
||||||
|
formData.append('showcase_created_at', showcaseCreatedAt);
|
||||||
|
console.log('[DEBUG] Sending showcase_created_at:', showcaseCreatedAt);
|
||||||
|
} else {
|
||||||
|
console.log('[DEBUG] showcase_created_at is empty, NOT sending');
|
||||||
|
}
|
||||||
formData.append('items', JSON.stringify(items));
|
formData.append('items', JSON.stringify(items));
|
||||||
formData.append('price_adjustment_type', priceAdjustmentType);
|
formData.append('price_adjustment_type', priceAdjustmentType);
|
||||||
formData.append('price_adjustment_value', priceAdjustmentValue);
|
formData.append('price_adjustment_value', priceAdjustmentValue);
|
||||||
@@ -2398,6 +2456,7 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
|||||||
|
|
||||||
// Сбрасываем поля формы
|
// Сбрасываем поля формы
|
||||||
document.getElementById('tempKitDescription').value = '';
|
document.getElementById('tempKitDescription').value = '';
|
||||||
|
document.getElementById('showcaseCreatedAt').value = '';
|
||||||
document.getElementById('tempKitPhoto').value = '';
|
document.getElementById('tempKitPhoto').value = '';
|
||||||
document.getElementById('photoPreview').style.display = 'none';
|
document.getElementById('photoPreview').style.display = 'none';
|
||||||
document.getElementById('priceAdjustmentType').value = 'none';
|
document.getElementById('priceAdjustmentType').value = 'none';
|
||||||
|
|||||||
@@ -219,6 +219,14 @@
|
|||||||
<textarea class="form-control" id="tempKitDescription" rows="3" placeholder="Краткое описание комплекта"></textarea>
|
<textarea class="form-control" id="tempKitDescription" rows="3" placeholder="Краткое описание комплекта"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Дата размещения на витрине -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<label for="showcaseCreatedAt" class="form-label">Дата размещения на витрине</label>
|
||||||
|
<input type="datetime-local" class="form-control" id="showcaseCreatedAt"
|
||||||
|
placeholder="Выберите дату и время">
|
||||||
|
<small class="text-muted">Оставьте пустым для текущего времени</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Загрузка фото -->
|
<!-- Загрузка фото -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="tempKitPhoto" class="form-label">Фото комплекта (опционально)</label>
|
<label for="tempKitPhoto" class="form-label">Фото комплекта (опционально)</label>
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ def get_showcase_kits_for_pos():
|
|||||||
'product_kit__price',
|
'product_kit__price',
|
||||||
'product_kit__sale_price',
|
'product_kit__sale_price',
|
||||||
'product_kit__base_price',
|
'product_kit__base_price',
|
||||||
|
'product_kit__showcase_created_at',
|
||||||
'showcase_id',
|
'showcase_id',
|
||||||
'showcase__name'
|
'showcase__name'
|
||||||
).annotate(
|
).annotate(
|
||||||
@@ -161,7 +162,9 @@ def get_showcase_kits_for_pos():
|
|||||||
'total_count': item['total_count'], # Всего на витрине (включая в корзине)
|
'total_count': item['total_count'], # Всего на витрине (включая в корзине)
|
||||||
'showcase_item_ids': available_item_ids, # IDs только доступных
|
'showcase_item_ids': available_item_ids, # IDs только доступных
|
||||||
# Флаг неактуальной цены
|
# Флаг неактуальной цены
|
||||||
'price_outdated': price_outdated
|
'price_outdated': price_outdated,
|
||||||
|
# Дата размещения на витрине
|
||||||
|
'showcase_created_at': item.get('product_kit__showcase_created_at')
|
||||||
})
|
})
|
||||||
|
|
||||||
return showcase_kits
|
return showcase_kits
|
||||||
@@ -1052,7 +1055,8 @@ def get_product_kit_details(request, kit_id):
|
|||||||
'final_price': str(kit.actual_price),
|
'final_price': str(kit.actual_price),
|
||||||
'showcase_id': showcase_id,
|
'showcase_id': showcase_id,
|
||||||
'items': items,
|
'items': items,
|
||||||
'photo_url': photo_url
|
'photo_url': photo_url,
|
||||||
|
'showcase_created_at': kit.showcase_created_at.isoformat() if kit.showcase_created_at else None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
except ProductKit.DoesNotExist:
|
except ProductKit.DoesNotExist:
|
||||||
@@ -1087,6 +1091,7 @@ def create_temp_kit_to_showcase(request):
|
|||||||
sale_price_str = request.POST.get('sale_price', '')
|
sale_price_str = request.POST.get('sale_price', '')
|
||||||
photo_file = request.FILES.get('photo')
|
photo_file = request.FILES.get('photo')
|
||||||
showcase_kit_quantity = int(request.POST.get('quantity', 1)) # Количество букетов на витрину
|
showcase_kit_quantity = int(request.POST.get('quantity', 1)) # Количество букетов на витрину
|
||||||
|
showcase_created_at_str = request.POST.get('showcase_created_at', '').strip()
|
||||||
|
|
||||||
# Парсим items из JSON
|
# Парсим items из JSON
|
||||||
items = json.loads(items_json)
|
items = json.loads(items_json)
|
||||||
@@ -1101,6 +1106,23 @@ def create_temp_kit_to_showcase(request):
|
|||||||
except (ValueError, InvalidOperation):
|
except (ValueError, InvalidOperation):
|
||||||
sale_price = None
|
sale_price = None
|
||||||
|
|
||||||
|
# Showcase created at (опционально)
|
||||||
|
showcase_created_at = None
|
||||||
|
if showcase_created_at_str:
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
showcase_created_at = datetime.fromisoformat(showcase_created_at_str)
|
||||||
|
except ValueError:
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
showcase_created_at = datetime.strptime(showcase_created_at_str, '%Y-%m-%dT%H:%M')
|
||||||
|
except ValueError:
|
||||||
|
pass # Неверный формат, оставляем как None
|
||||||
|
|
||||||
|
# Если не указана - устанавливаем текущее время для новых комплектов
|
||||||
|
if not showcase_created_at:
|
||||||
|
showcase_created_at = timezone.now()
|
||||||
|
|
||||||
# Валидация
|
# Валидация
|
||||||
if not kit_name:
|
if not kit_name:
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
@@ -1161,7 +1183,8 @@ def create_temp_kit_to_showcase(request):
|
|||||||
price_adjustment_type=price_adjustment_type,
|
price_adjustment_type=price_adjustment_type,
|
||||||
price_adjustment_value=price_adjustment_value,
|
price_adjustment_value=price_adjustment_value,
|
||||||
sale_price=sale_price,
|
sale_price=sale_price,
|
||||||
showcase=showcase
|
showcase=showcase,
|
||||||
|
showcase_created_at=showcase_created_at
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Создаём KitItem для каждого товара из корзины
|
# 2. Создаём KitItem для каждого товара из корзины
|
||||||
@@ -1284,6 +1307,9 @@ def update_product_kit(request, kit_id):
|
|||||||
- photo: Новое фото (опционально)
|
- photo: Новое фото (опционально)
|
||||||
- remove_photo: '1' для удаления фото
|
- remove_photo: '1' для удаления фото
|
||||||
"""
|
"""
|
||||||
|
import logging
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
kit = ProductKit.objects.prefetch_related('kit_items__product', 'photos').get(id=kit_id, is_temporary=True)
|
kit = ProductKit.objects.prefetch_related('kit_items__product', 'photos').get(id=kit_id, is_temporary=True)
|
||||||
|
|
||||||
@@ -1296,6 +1322,7 @@ def update_product_kit(request, kit_id):
|
|||||||
sale_price_str = request.POST.get('sale_price', '')
|
sale_price_str = request.POST.get('sale_price', '')
|
||||||
photo_file = request.FILES.get('photo')
|
photo_file = request.FILES.get('photo')
|
||||||
remove_photo = request.POST.get('remove_photo', '') == '1'
|
remove_photo = request.POST.get('remove_photo', '') == '1'
|
||||||
|
showcase_created_at_str = request.POST.get('showcase_created_at', '').strip()
|
||||||
|
|
||||||
items = json.loads(items_json)
|
items = json.loads(items_json)
|
||||||
|
|
||||||
@@ -1308,6 +1335,31 @@ def update_product_kit(request, kit_id):
|
|||||||
except (ValueError, InvalidOperation):
|
except (ValueError, InvalidOperation):
|
||||||
sale_price = None
|
sale_price = None
|
||||||
|
|
||||||
|
# Showcase created at (опционально)
|
||||||
|
showcase_created_at = None
|
||||||
|
if showcase_created_at_str:
|
||||||
|
logger.warning(f"[DEBUG] showcase_created_at_str received: '{showcase_created_at_str}'")
|
||||||
|
try:
|
||||||
|
from datetime import datetime
|
||||||
|
showcase_created_at = datetime.fromisoformat(showcase_created_at_str)
|
||||||
|
logger.warning(f"[DEBUG] Parsed fromisoformat: {showcase_created_at}")
|
||||||
|
except ValueError as e:
|
||||||
|
logger.warning(f"[DEBUG] fromisoformat failed: {e}, trying strptime")
|
||||||
|
try:
|
||||||
|
showcase_created_at = datetime.strptime(showcase_created_at_str, '%Y-%m-%dT%H:%M')
|
||||||
|
logger.warning(f"[DEBUG] Parsed strptime: {showcase_created_at}")
|
||||||
|
except ValueError as e2:
|
||||||
|
logger.warning(f"[DEBUG] strptime also failed: {e2}")
|
||||||
|
pass # Неверный формат, оставляем как есть
|
||||||
|
|
||||||
|
# Делаем datetime timezone-aware
|
||||||
|
if showcase_created_at and showcase_created_at.tzinfo is None:
|
||||||
|
from django.utils import timezone
|
||||||
|
showcase_created_at = timezone.make_aware(showcase_created_at)
|
||||||
|
logger.warning(f"[DEBUG] Made aware: {showcase_created_at}")
|
||||||
|
|
||||||
|
logger.warning(f"[DEBUG] Final showcase_created_at value: {showcase_created_at}")
|
||||||
|
|
||||||
# Валидация
|
# Валидация
|
||||||
if not kit_name:
|
if not kit_name:
|
||||||
return JsonResponse({'success': False, 'error': 'Необходимо указать название'}, status=400)
|
return JsonResponse({'success': False, 'error': 'Необходимо указать название'}, status=400)
|
||||||
@@ -1381,6 +1433,11 @@ def update_product_kit(request, kit_id):
|
|||||||
kit.price_adjustment_type = price_adjustment_type
|
kit.price_adjustment_type = price_adjustment_type
|
||||||
kit.price_adjustment_value = price_adjustment_value
|
kit.price_adjustment_value = price_adjustment_value
|
||||||
kit.sale_price = sale_price
|
kit.sale_price = sale_price
|
||||||
|
if showcase_created_at is not None: # Обновляем только если передана
|
||||||
|
kit.showcase_created_at = showcase_created_at
|
||||||
|
logger.warning(f"[DEBUG] Saving kit.showcase_created_at = {kit.showcase_created_at}")
|
||||||
|
else:
|
||||||
|
logger.warning(f"[DEBUG] showcase_created_at is None, NOT updating field")
|
||||||
kit.save()
|
kit.save()
|
||||||
|
|
||||||
# Обновляем состав
|
# Обновляем состав
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2026-01-23 22:05
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('products', '0002_bouquetname'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productkit',
|
||||||
|
name='showcase_created_at',
|
||||||
|
field=models.DateTimeField(blank=True, help_text='Дата создания букета для витрины (редактируемая)', null=True, verbose_name='Дата размещения на витрине'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -93,6 +93,14 @@ class ProductKit(BaseProductEntity):
|
|||||||
help_text="Временные комплекты не показываются в каталоге и создаются для конкретного заказа"
|
help_text="Временные комплекты не показываются в каталоге и создаются для конкретного заказа"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Showcase creation date - editable date for when the bouquet was put on display
|
||||||
|
showcase_created_at = models.DateTimeField(
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Дата размещения на витрине",
|
||||||
|
help_text="Дата создания букета для витрины (редактируемая)"
|
||||||
|
)
|
||||||
|
|
||||||
order = models.ForeignKey(
|
order = models.ForeignKey(
|
||||||
'orders.Order',
|
'orders.Order',
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
|
|||||||
@@ -158,49 +158,41 @@ class ImageProcessor:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _resize_image(img, size):
|
def _resize_image(img, size):
|
||||||
"""
|
"""
|
||||||
Изменяет размер изображения с сохранением пропорций.
|
Изменяет размер изображения с center-crop до точного квадратного размера.
|
||||||
НЕ увеличивает маленькие изображения (сохраняет качество).
|
НЕ увеличивает маленькие изображения (сохраняет качество).
|
||||||
Создает адаптивный квадрат по размеру реального изображения.
|
Создает квадратное изображение без белых полей.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
img: PIL Image object
|
img: PIL Image object
|
||||||
size: Кортеж (width, height) - максимальный целевой размер
|
size: Кортеж (width, height) - целевой размер (обычно квадратный)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
PIL Image object - квадратное изображение с минимальным белым фоном
|
PIL Image object - квадратное изображение без белых полей
|
||||||
"""
|
"""
|
||||||
# Копируем изображение, чтобы не модифицировать оригинал
|
|
||||||
img_copy = img.copy()
|
img_copy = img.copy()
|
||||||
|
target_width, target_height = size
|
||||||
|
|
||||||
# Вычисляем пропорции исходного изображения и целевого размера
|
# Шаг 1: Center crop для получения квадрата
|
||||||
img_aspect = img_copy.width / img_copy.height
|
# Определяем минимальную сторону (будет размер квадрата)
|
||||||
target_aspect = size[0] / size[1]
|
min_side = min(img_copy.width, img_copy.height)
|
||||||
|
|
||||||
# Определяем, какой размер будет ограничивающим при масштабировании
|
# Вычисляем координаты для обрезки из центра
|
||||||
if img_aspect > target_aspect:
|
left = (img_copy.width - min_side) // 2
|
||||||
# Изображение шире - ограничиваемый размер это ширина
|
top = (img_copy.height - min_side) // 2
|
||||||
new_width = min(img_copy.width, size[0])
|
right = left + min_side
|
||||||
new_height = int(new_width / img_aspect)
|
bottom = top + min_side
|
||||||
|
|
||||||
|
# Обрезаем до квадрата
|
||||||
|
img_cropped = img_copy.crop((left, top, right, bottom))
|
||||||
|
|
||||||
|
# Шаг 2: Масштабируем до целевого размера (если исходный квадрат больше цели)
|
||||||
|
# Не увеличиваем маленькие изображения
|
||||||
|
if min_side > target_width:
|
||||||
|
img_resized = img_cropped.resize((target_width, target_height), Image.Resampling.LANCZOS)
|
||||||
else:
|
else:
|
||||||
# Изображение выше - ограничиваемый размер это высота
|
img_resized = img_cropped
|
||||||
new_height = min(img_copy.height, size[1])
|
|
||||||
new_width = int(new_height * img_aspect)
|
|
||||||
|
|
||||||
# Масштабируем только если необходимо (не увеличиваем маленькие изображения)
|
return img_resized
|
||||||
if img_copy.width > new_width or img_copy.height > new_height:
|
|
||||||
img_copy = img_copy.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
||||||
|
|
||||||
# Создаем адаптивный квадрат по размеру реального изображения (а не по конфигурации)
|
|
||||||
# Это позволяет избежать огромных белых полей для маленьких фото
|
|
||||||
square_size = max(img_copy.width, img_copy.height)
|
|
||||||
new_img = Image.new('RGB', (square_size, square_size), (255, 255, 255))
|
|
||||||
|
|
||||||
# Центрируем исходное изображение на белом фоне
|
|
||||||
offset_x = (square_size - img_copy.width) // 2
|
|
||||||
offset_y = (square_size - img_copy.height) // 2
|
|
||||||
new_img.paste(img_copy, (offset_x, offset_y))
|
|
||||||
|
|
||||||
return new_img
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _make_square_image(img, max_size):
|
def _make_square_image(img, max_size):
|
||||||
|
|||||||
Reference in New Issue
Block a user