feat(inventory): учитывать коэффициент конверсии при резервировании компонентов комплектов
Добавлены поля original_sales_unit и conversion_factor в KitItemSnapshot для хранения единиц продажи и коэффициентов конверсии на момент создания снимка. Обновлена логика резервирования запасов для корректного расчета количества в базовых единицах. Изменения в шаблоне редактирования комплектов для сохранения выбранных единиц продажи при обновлении списка опций. BREAKING CHANGE: Изменена структура данных в KitItemSnapshot, требуется миграция базы данных.
This commit is contained in:
@@ -222,9 +222,14 @@ def reserve_stock_on_item_create(sender, instance, created, **kwargs):
|
|||||||
|
|
||||||
for kit_item in instance.kit_snapshot.items.select_related('original_product'):
|
for kit_item in instance.kit_snapshot.items.select_related('original_product'):
|
||||||
if kit_item.original_product:
|
if kit_item.original_product:
|
||||||
# Суммируем количество: qty компонента * qty комплектов в заказе
|
# Рассчитываем количество одного компонента в базовых единицах
|
||||||
|
component_qty_base = kit_item.quantity
|
||||||
|
if kit_item.conversion_factor and kit_item.conversion_factor > 0:
|
||||||
|
component_qty_base = kit_item.quantity / kit_item.conversion_factor
|
||||||
|
|
||||||
|
# Суммируем количество: qty компонента (base) * qty комплектов в заказе
|
||||||
product_quantities[kit_item.original_product_id] += (
|
product_quantities[kit_item.original_product_id] += (
|
||||||
kit_item.quantity * Decimal(str(instance.quantity))
|
component_qty_base * Decimal(str(instance.quantity))
|
||||||
)
|
)
|
||||||
|
|
||||||
# Создаём по одному резерву на каждый уникальный товар
|
# Создаём по одному резерву на каждый уникальный товар
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2026-01-21 07:27
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('orders', '0003_order_summary'),
|
||||||
|
('products', '0001_add_sales_unit_to_kititem'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='kititemsnapshot',
|
||||||
|
name='conversion_factor',
|
||||||
|
field=models.DecimalField(blank=True, decimal_places=6, help_text='Сколько единиц продажи в 1 базовой единице товара', max_digits=15, null=True, verbose_name='Коэффициент конверсии'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='kititemsnapshot',
|
||||||
|
name='original_sales_unit',
|
||||||
|
field=models.ForeignKey(blank=True, help_text='Единица продажи на момент создания снимка', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='kit_item_snapshots', to='products.productsalesunit', verbose_name='Единица продажи'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -140,6 +140,25 @@ class KitItemSnapshot(models.Model):
|
|||||||
verbose_name="Группа вариантов"
|
verbose_name="Группа вариантов"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
original_sales_unit = models.ForeignKey(
|
||||||
|
'products.ProductSalesUnit',
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
related_name='kit_item_snapshots',
|
||||||
|
verbose_name="Единица продажи",
|
||||||
|
help_text="Единица продажи на момент создания снимка"
|
||||||
|
)
|
||||||
|
|
||||||
|
conversion_factor = models.DecimalField(
|
||||||
|
max_digits=15,
|
||||||
|
decimal_places=6,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
verbose_name="Коэффициент конверсии",
|
||||||
|
help_text="Сколько единиц продажи в 1 базовой единице товара"
|
||||||
|
)
|
||||||
|
|
||||||
quantity = models.DecimalField(
|
quantity = models.DecimalField(
|
||||||
max_digits=10,
|
max_digits=10,
|
||||||
decimal_places=3,
|
decimal_places=3,
|
||||||
|
|||||||
@@ -382,6 +382,8 @@ class ProductKit(BaseProductEntity):
|
|||||||
product_sku=item.product.sku if item.product else '',
|
product_sku=item.product.sku if item.product else '',
|
||||||
product_price=product_price,
|
product_price=product_price,
|
||||||
variant_group_name=item.variant_group.name if item.variant_group else '',
|
variant_group_name=item.variant_group.name if item.variant_group else '',
|
||||||
|
original_sales_unit=item.sales_unit,
|
||||||
|
conversion_factor=item.sales_unit.conversion_factor if item.sales_unit else None,
|
||||||
quantity=item.quantity or Decimal('1'),
|
quantity=item.quantity or Decimal('1'),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -725,6 +725,9 @@
|
|||||||
|
|
||||||
// Функция для обновления списка единиц продажи при выборе товара
|
// Функция для обновления списка единиц продажи при выборе товара
|
||||||
async function updateSalesUnitsOptions(salesUnitSelect, productValue) {
|
async function updateSalesUnitsOptions(salesUnitSelect, productValue) {
|
||||||
|
// Сохраняем текущее значение перед очисткой (важно для редактирования)
|
||||||
|
const currentValue = salesUnitSelect.value;
|
||||||
|
|
||||||
// Очищаем текущие опции
|
// Очищаем текущие опции
|
||||||
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
salesUnitSelect.innerHTML = '<option value="">---------</option>';
|
||||||
salesUnitSelect.disabled = true;
|
salesUnitSelect.disabled = true;
|
||||||
@@ -765,6 +768,13 @@
|
|||||||
salesUnitSelect.appendChild(option);
|
salesUnitSelect.appendChild(option);
|
||||||
});
|
});
|
||||||
salesUnitSelect.disabled = false;
|
salesUnitSelect.disabled = false;
|
||||||
|
|
||||||
|
// Восстанавливаем значение
|
||||||
|
if (currentValue) {
|
||||||
|
salesUnitSelect.value = currentValue;
|
||||||
|
}
|
||||||
|
// Обновляем Select2
|
||||||
|
$(salesUnitSelect).trigger('change');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1214,7 +1224,7 @@
|
|||||||
photoPreview.innerHTML = '';
|
photoPreview.innerHTML = '';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
window.removePhoto = function (index) {
|
window.removePhoto = function (index) {
|
||||||
selectedFiles.splice(index, 1);
|
selectedFiles.splice(index, 1);
|
||||||
|
|||||||
29
prepare_js.py
Normal file
29
prepare_js.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
file_path = r'c:\Users\team_\Desktop\test_qwen\myproject\products\templates\products\productkit_edit.html'
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
lines = f.readlines()
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"File not found: {file_path}")
|
||||||
|
exit(1)
|
||||||
|
|
||||||
|
# Extract script part (approx lines 451 to 1321)
|
||||||
|
# Note: lines are 0-indexed in list
|
||||||
|
script_lines = lines[450:1322]
|
||||||
|
script_content = "".join(script_lines)
|
||||||
|
|
||||||
|
# Replace Django tags
|
||||||
|
# Replace {% ... %} with "TEMPLATETAG"
|
||||||
|
script_content = re.sub(r'\{%.*?%\}', '"TEMPLATETAG"', script_content)
|
||||||
|
# Replace {{ ... }} with "VARIABLE" or {}
|
||||||
|
script_content = re.sub(r'\{\{.*?\}\}', '{}', script_content)
|
||||||
|
|
||||||
|
# Save to temp js file
|
||||||
|
temp_js_path = r'c:\Users\team_\Desktop\test_qwen\temp_check.js'
|
||||||
|
with open(temp_js_path, 'w', encoding='utf-8') as f:
|
||||||
|
f.write(script_content)
|
||||||
|
|
||||||
|
print(f"Written to {temp_js_path}")
|
||||||
Reference in New Issue
Block a user