Огромная замена логики
All checks were successful
Deploy MES Core / deploy (push) Successful in 11s

This commit is contained in:
2026-04-06 08:06:37 +03:00
parent 0e8497ab1f
commit e88b861f68
48 changed files with 3833 additions and 175 deletions

View File

@@ -1,9 +1,14 @@
from django.contrib import admin
from .models import MaterialCategory, SteelGrade, Material
from django.contrib import admin, messages
from django.utils import timezone
from warehouse.services.transfers import receive_transfer
from .models import Location, Material, MaterialCategory, SteelGrade, StockItem, TransferLine, TransferRecord
@admin.register(MaterialCategory)
class MaterialCategoryAdmin(admin.ModelAdmin):
list_display = ('name', 'gost_standard')
list_display = ('name', 'form_factor', 'gost_standard')
list_filter = ('form_factor',)
search_fields = ('name', 'gost_standard')
@admin.register(SteelGrade)
@@ -17,3 +22,103 @@ class MaterialAdmin(admin.ModelAdmin):
list_filter = ('category', 'steel_grade')
search_fields = ('name', 'full_name')
readonly_fields = ('full_name',)
@admin.register(Location)
class LocationAdmin(admin.ModelAdmin):
list_display = ('name', 'is_production_area')
list_filter = ('is_production_area',)
search_fields = ('name',)
@admin.register(StockItem)
class StockItemAdmin(admin.ModelAdmin):
list_display = ('location', 'material', 'entity', 'quantity', 'is_remnant', 'unique_id')
list_filter = ('location', 'is_remnant', 'material__category')
search_fields = ('material__name', 'material__full_name', 'entity__name', 'entity__drawing_number', 'unique_id')
autocomplete_fields = ('location', 'material', 'entity')
class TransferLineInline(admin.TabularInline):
model = TransferLine
fk_name = 'transfer'
fields = ('stock_item', 'quantity')
autocomplete_fields = ('stock_item',)
extra = 5
def formfield_for_foreignkey(self, db_field, request, **kwargs):
if db_field.name == 'stock_item':
from_location_id = None
tr = getattr(request, '_transfer_obj', None)
if tr and getattr(tr, 'from_location_id', None):
from_location_id = tr.from_location_id
if not from_location_id and request.method == 'POST':
raw = (request.POST.get('from_location') or '').strip()
if raw.isdigit():
from_location_id = int(raw)
if from_location_id:
kwargs['queryset'] = StockItem.objects.filter(location_id=from_location_id)
else:
kwargs['queryset'] = StockItem.objects.all()
return super().formfield_for_foreignkey(db_field, request, **kwargs)
@admin.register(TransferRecord)
class TransferRecordAdmin(admin.ModelAdmin):
list_display = ('from_location', 'to_location', 'id', 'occurred_at', 'sender', 'receiver', 'status', 'is_applied')
list_display_links = ('from_location',)
list_filter = ('status', 'from_location', 'to_location')
search_fields = ('sender__username', 'receiver__username')
autocomplete_fields = ('from_location', 'to_location', 'sender', 'receiver')
inlines = (TransferLineInline,)
actions = ('action_receive',)
def get_changeform_initial_data(self, request):
initial = super().get_changeform_initial_data(request)
initial.setdefault('sender', request.user.id)
initial.setdefault('occurred_at', timezone.now())
return initial
def get_form(self, request, obj=None, **kwargs):
request._transfer_obj = obj
return super().get_form(request, obj, **kwargs)
def save_model(self, request, obj, form, change):
if not obj.sender_id:
obj.sender = request.user
super().save_model(request, obj, form, change)
def save_related(self, request, form, formsets, change):
super().save_related(request, form, formsets, change)
obj = form.instance
# Применяем перемещение автоматически после сохранения строк.
# Если строк нет — receive_transfer выбросит понятную ошибку.
if not getattr(obj, 'is_applied', False):
try:
receive_transfer(obj.id, request.user.id)
except Exception as e:
self.message_user(request, f'Перемещение id={obj.id}: {e}', level=messages.ERROR)
@admin.action(description='Принять перемещение')
def action_receive(self, request, queryset):
ok = 0
failed = 0
for tr in queryset:
try:
receive_transfer(tr.id, request.user.id)
ok += 1
except Exception as e:
failed += 1
self.message_user(request, f'Перемещение id={tr.id}: {e}', level=messages.ERROR)
if ok:
self.message_user(request, f'Применено: {ok}.', level=messages.SUCCESS)
if failed:
self.message_user(request, f'Ошибок: {failed}.', level=messages.ERROR)

View File

@@ -0,0 +1,65 @@
# Generated by Django 6.0.3 on 2026-04-04 15:14
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('manufacturing', '0001_initial'),
('warehouse', '0003_alter_material_full_name'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='Location',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Место хранения')),
('is_production_area', models.BooleanField(default=False, verbose_name='Это производственный участок')),
],
options={
'verbose_name': 'Склад/Участок',
'verbose_name_plural': 'Склады и участки',
},
),
migrations.CreateModel(
name='StockItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.FloatField(verbose_name='Количество (шт/м/кг/лист)')),
('is_remnant', models.BooleanField(default=False, verbose_name='Деловой остаток')),
('current_length', models.FloatField(blank=True, null=True, verbose_name='Текущая длина, мм')),
('current_width', models.FloatField(blank=True, null=True, verbose_name='Текущая ширина, мм')),
('unique_id', models.CharField(blank=True, max_length=50, null=True, unique=True, verbose_name='ID/Маркировка (для ДО)')),
('entity', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='manufacturing.productentity', verbose_name='Произведённая сущность')),
('location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='warehouse.location', verbose_name='Где находится')),
('material', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='warehouse.material', verbose_name='Сырьё')),
],
options={
'verbose_name': 'Единица на складе',
'verbose_name_plural': 'Остатки на складах',
},
),
migrations.CreateModel(
name='TransferRecord',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('sent', 'В пути'), ('received', 'Принято'), ('discrepancy', 'Расхождение')], default='sent', max_length=20, verbose_name='Статус')),
('created_at', models.DateTimeField(auto_now_add=True)),
('received_at', models.DateTimeField(blank=True, null=True)),
('from_location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='outgoing', to='warehouse.location')),
('items', models.ManyToManyField(to='warehouse.stockitem', verbose_name='Перемещаемые объекты')),
('receiver', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='received_transfers', to=settings.AUTH_USER_MODEL)),
('sender', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='sent_transfers', to=settings.AUTH_USER_MODEL)),
('to_location', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incoming', to='warehouse.location')),
],
options={
'verbose_name': 'Перемещение',
'verbose_name_plural': 'Перемещения',
},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.3 on 2026-04-05 08:27
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0004_location_stockitem_transferrecord'),
]
operations = [
migrations.AlterModelOptions(
name='stockitem',
options={'verbose_name': 'Единица на складах', 'verbose_name_plural': 'Единицы на складах'},
),
]

View File

@@ -0,0 +1,17 @@
# Generated by Django 6.0.3 on 2026-04-05 08:50
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0005_alter_stockitem_options'),
]
operations = [
migrations.AlterModelOptions(
name='stockitem',
options={'verbose_name': 'Единица на складе', 'verbose_name_plural': 'Единицы на складе'},
),
]

View File

@@ -0,0 +1,65 @@
# Generated by Django 6.0.3 on 2026-04-05 11:42
import django.db.models.deletion
import django.utils.timezone
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0006_alter_stockitem_options'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.RemoveField(
model_name='transferrecord',
name='items',
),
migrations.AddField(
model_name='transferrecord',
name='is_applied',
field=models.BooleanField(default=False, verbose_name='Применено'),
),
migrations.AddField(
model_name='transferrecord',
name='occurred_at',
field=models.DateTimeField(default=django.utils.timezone.now, verbose_name='Дата/время'),
),
migrations.AlterField(
model_name='transferrecord',
name='from_location',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='outgoing', to='warehouse.location', verbose_name='Откуда'),
),
migrations.AlterField(
model_name='transferrecord',
name='receiver',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='received_transfers', to=settings.AUTH_USER_MODEL, verbose_name='Кому'),
),
migrations.AlterField(
model_name='transferrecord',
name='sender',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='sent_transfers', to=settings.AUTH_USER_MODEL, verbose_name='Кто'),
),
migrations.AlterField(
model_name='transferrecord',
name='to_location',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incoming', to='warehouse.location', verbose_name='Куда'),
),
migrations.CreateModel(
name='TransferLine',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.FloatField(verbose_name='Количество')),
('stock_item', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='warehouse.stockitem', verbose_name='Единица на складе')),
('transfer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='lines', to='warehouse.transferrecord', verbose_name='Перемещение')),
],
options={
'verbose_name': 'Строка перемещения',
'verbose_name_plural': 'Строки перемещения',
'unique_together': {('transfer', 'stock_item')},
},
),
]

View File

@@ -0,0 +1,24 @@
# Generated by Django 6.0.3 on 2026-04-05 15:45
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0007_remove_transferrecord_items_and_more'),
]
operations = [
migrations.AlterField(
model_name='transferrecord',
name='received_at',
field=models.DateTimeField(blank=True, default=django.utils.timezone.now, null=True),
),
migrations.AlterField(
model_name='transferrecord',
name='status',
field=models.CharField(choices=[('sent', 'В пути'), ('received', 'Принято'), ('discrepancy', 'Расхождение')], default='received', max_length=20, verbose_name='Статус'),
),
]

View File

@@ -0,0 +1,19 @@
# Generated by Django 6.0.3 on 2026-04-05 19:06
import django.utils.timezone
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0008_alter_transferrecord_received_at_and_more'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='created_at',
field=models.DateTimeField(default=django.utils.timezone.now, editable=False, verbose_name='Поступление'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-04-05 19:49
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0009_stockitem_created_at'),
]
operations = [
migrations.AddField(
model_name='materialcategory',
name='form_factor',
field=models.CharField(choices=[('sheet', 'Лист'), ('bar', 'Прокат/хлыст'), ('other', 'Прочее')], default='other', max_length=16, verbose_name='Форма'),
),
]

View File

@@ -0,0 +1,18 @@
# Generated by Django 6.0.3 on 2026-04-05 20:09
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('warehouse', '0010_materialcategory_form_factor'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='is_customer_supplied',
field=models.BooleanField(default=False, verbose_name='Давальческий'),
),
]

View File

@@ -0,0 +1,20 @@
# Generated by Django 6.0.3 on 2026-04-06 03:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shiftflow', '0018_alter_productionreportconsumption_unique_together_and_more'),
('warehouse', '0011_stockitem_is_customer_supplied'),
]
operations = [
migrations.AddField(
model_name='stockitem',
name='deal',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, to='shiftflow.deal', verbose_name='Сделка'),
),
]

View File

@@ -0,0 +1,48 @@
from django.db import models
class MaterialCategory(models.Model):
"""Категория материала (например, Труба, Лист, Круг)"""
name = models.CharField("Название категории", max_length=100, unique=True)
gost_standard = models.CharField("ГОСТ на тип проката", max_length=255, blank=True, help_text="Напр: ГОСТ 8639-82")
class Meta:
verbose_name = "Категория материала"
verbose_name_plural = "Категории материалов"
def __str__(self):
return self.name
class SteelGrade(models.Model):
"""Марка стали (например, Ст3сп, 09Г2С) и связанные с ней ГОСТы"""
name = models.CharField("Марка стали", max_length=100, unique=True)
gost_standard = models.CharField("ГОСТ/ТУ", max_length=255, blank=True, help_text="Основной стандарт для этой марки")
certificate_pdf = models.FileField("Сертификат/ГОСТ (PDF)", upload_to='certificates/', blank=True, null=True)
class Meta:
verbose_name = "Марка стали"
verbose_name_plural = "Марки стали"
def __str__(self):
return f"{self.name} ({self.gost_standard})" if self.gost_standard else self.name
class Material(models.Model):
"""Конкретная номенклатурная единица (например, Труба 100х100х4)"""
category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория")
steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, verbose_name="Марка стали", null=True, blank=True)
name = models.CharField("Наименование (размер/характеристики)", max_length=255)
full_name = models.CharField("Полное наименование", max_length=500, blank=True, help_text="Генерируется автоматически")
class Meta:
verbose_name = "Материал (номенклатура)"
verbose_name_plural = "Материалы"
def save(self, *args, **kwargs):
category_part = (self.category.name or '').strip() if self.category_id else ''
name_part = (self.name or '').strip()
grade_part = (self.steel_grade.name or '').strip() if self.steel_grade_id else ''
self.full_name = ' '.join([p for p in [category_part, name_part, grade_part] if p])
super().save(*args, **kwargs)
def __str__(self):
return self.full_name or ' '.join([p for p in [(self.category.name if self.category_id else ''), self.name, (self.steel_grade.name if self.steel_grade_id else '')] if p]).strip()

View File

@@ -1,8 +1,20 @@
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
from django.core.exceptions import ValidationError
import uuid
class MaterialCategory(models.Model):
"""Категория материала (например, Труба, Лист, Круг)"""
FORM_FACTOR_CHOICES = [
('sheet', 'Лист'),
('bar', 'Прокат/хлыст'),
('other', 'Прочее'),
]
name = models.CharField("Название категории", max_length=100, unique=True)
form_factor = models.CharField('Форма', max_length=16, choices=FORM_FACTOR_CHOICES, default='other')
gost_standard = models.CharField("ГОСТ на тип проката", max_length=255, blank=True, help_text="Напр: ГОСТ 8639-82")
class Meta:
@@ -26,7 +38,7 @@ class SteelGrade(models.Model):
return f"{self.name} ({self.gost_standard})" if self.gost_standard else self.name
class Material(models.Model):
"""Конкретная номенклатурная единица (например, Труба 100х100х4)"""
"""Конкретная номенклатурная единица (например, Труба 100х100х4)."""
category = models.ForeignKey(MaterialCategory, on_delete=models.PROTECT, verbose_name="Категория")
steel_grade = models.ForeignKey(SteelGrade, on_delete=models.PROTECT, verbose_name="Марка стали", null=True, blank=True)
name = models.CharField("Наименование (размер/характеристики)", max_length=255)
@@ -46,3 +58,134 @@ class Material(models.Model):
def __str__(self):
return self.full_name or ' '.join([p for p in [(self.category.name if self.category_id else ''), self.name, (self.steel_grade.name if self.steel_grade_id else '')] if p]).strip()
class Location(models.Model):
"""Место хранения: центральный склад или склад участка (лазер/гибка/сварка и т.д.)."""
name = models.CharField("Место хранения", max_length=100, unique=True)
is_production_area = models.BooleanField("Это производственный участок", default=False)
class Meta:
verbose_name = "Склад/Участок"
verbose_name_plural = "Склады и участки"
def __str__(self):
return self.name
class StockItem(models.Model):
"""Физический остаток на складе.
Правила заполнения:
- если это сырьё: заполнен material, entity пустой
- если это готовая деталь: заполнен entity, material пустой
Количество (quantity) интерпретируется как "единица учёта" для конкретного объекта:
- для листа может быть 1 лист
- для профиля/трубы может быть 1 хлыст
- для готовых деталей обычно шт.
"""
material = models.ForeignKey('warehouse.Material', on_delete=models.PROTECT, null=True, blank=True, verbose_name="Сырьё")
entity = models.ForeignKey('manufacturing.ProductEntity', on_delete=models.PROTECT, null=True, blank=True, verbose_name="Произведённая сущность")
deal = models.ForeignKey('shiftflow.Deal', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Сделка')
location = models.ForeignKey(Location, on_delete=models.PROTECT, verbose_name="Где находится")
quantity = models.FloatField("Количество (шт/м/кг/лист)")
created_at = models.DateTimeField("Поступление", default=timezone.now, editable=False)
created_at = models.DateTimeField("Поступление", default=timezone.now, editable=False)
is_remnant = models.BooleanField("Деловой остаток", default=False)
is_customer_supplied = models.BooleanField('Давальческий', default=False)
current_length = models.FloatField("Текущая длина, мм", null=True, blank=True)
current_width = models.FloatField("Текущая ширина, мм", null=True, blank=True)
unique_id = models.CharField("ID/Маркировка (для ДО)", max_length=50, unique=True, null=True, blank=True)
class Meta:
verbose_name = "Единица на складе"
verbose_name_plural = "Единицы на складе"
def clean(self):
if self.material_id and not self.entity_id:
category = getattr(self.material, 'category', None)
form_factor = getattr(category, 'form_factor', 'other') if category else 'other'
if form_factor == 'sheet':
if self.current_length in (None, '') or self.current_width in (None, ''):
raise ValidationError('Для листового материала нужно заполнить длину и ширину (мм).')
if form_factor == 'bar':
if self.current_length in (None, ''):
raise ValidationError('Для проката нужно заполнить длину (мм).')
super().clean()
def save(self, *args, **kwargs):
if self.is_remnant and not self.unique_id:
while True:
candidate = f"DO-{uuid.uuid4().hex[:12].upper()}"
if not StockItem.objects.filter(unique_id=candidate).exists():
self.unique_id = candidate
break
super().save(*args, **kwargs)
def __str__(self):
obj = self.entity if self.entity_id else self.material
return f"{obj} | {self.quantity} | {self.location}"
class TransferRecord(models.Model):
"""Документ перемещения между складами.
Состав перемещения хранится в строках TransferLine, где указывается:
- какая складская позиция списывается со склада-источника
- сколько списывается
Статусы «в пути/принято» можно расширить позже. Сейчас ключевое — корректное списание/начисление.
"""
STATUS_CHOICES = [
('sent', 'В пути'),
('received', 'Принято'),
('discrepancy', 'Расхождение'),
]
from_location = models.ForeignKey(Location, related_name='outgoing', on_delete=models.PROTECT, verbose_name='Откуда')
to_location = models.ForeignKey(Location, related_name='incoming', on_delete=models.PROTECT, verbose_name='Куда')
sender = models.ForeignKey(User, related_name='sent_transfers', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Кто')
receiver = models.ForeignKey(User, related_name='received_transfers', on_delete=models.PROTECT, null=True, blank=True, verbose_name='Кому')
occurred_at = models.DateTimeField('Дата/время', default=timezone.now)
status = models.CharField("Статус", max_length=20, choices=STATUS_CHOICES, default='received')
created_at = models.DateTimeField(auto_now_add=True)
received_at = models.DateTimeField(null=True, blank=True, default=timezone.now)
is_applied = models.BooleanField('Применено', default=False)
class Meta:
verbose_name = "Перемещение"
verbose_name_plural = "Перемещения"
def __str__(self):
return f"{self.from_location} -> {self.to_location}"
class TransferLine(models.Model):
"""Строка перемещения: сколько списать из конкретной складской позиции."""
transfer = models.ForeignKey(TransferRecord, related_name='lines', on_delete=models.CASCADE, verbose_name='Перемещение')
stock_item = models.ForeignKey(StockItem, on_delete=models.PROTECT, verbose_name='Единица на складе')
quantity = models.FloatField('Количество')
class Meta:
verbose_name = 'Строка перемещения'
verbose_name_plural = 'Строки перемещения'
unique_together = ('transfer', 'stock_item')
def __str__(self):
return f"{self.transfer_id}: {self.stock_item_id} x {self.quantity}"

View File

@@ -0,0 +1,5 @@
"""
Сервисный слой приложения warehouse.
Здесь живут операции складского учёта, требующие транзакций и блокировок.
"""

View File

@@ -0,0 +1,71 @@
from django.db import transaction
from django.utils import timezone
from warehouse.models import StockItem, TransferLine, TransferRecord
@transaction.atomic
def receive_transfer(transfer_id: int, receiver_id: int) -> None:
"""
Строгое перемещение: принять TransferRecord.
Логика:
- если уже received -> идемпотентно выходим
- блокируем TransferRecord
- блокируем связанные StockItem
- обновляем location на to_location
- ставим receiver/received_at/status
"""
tr = (
TransferRecord.objects.select_for_update()
.select_related('from_location', 'to_location')
.get(pk=transfer_id)
)
if tr.is_applied:
return
lines = list(TransferLine.objects.filter(transfer=tr).select_related('stock_item', 'stock_item__location', 'stock_item__material', 'stock_item__entity'))
if not lines:
raise RuntimeError('В перемещении нет строк.')
for ln in lines:
if float(ln.quantity) <= 0:
continue
src = StockItem.objects.select_for_update().get(pk=ln.stock_item_id)
if src.location_id != tr.from_location_id:
raise RuntimeError('Единица на складе находится не на складе-источнике.')
if float(ln.quantity) > float(src.quantity):
raise RuntimeError('Недостаточно количества в источнике для перемещения.')
if src.unique_id and float(ln.quantity) != float(src.quantity):
raise RuntimeError('Нельзя частично перемещать позицию с ID/маркировкой.')
if float(ln.quantity) == float(src.quantity):
src.location_id = tr.to_location_id
src.created_at = timezone.now()
src.save(update_fields=['location', 'created_at'])
continue
src.quantity = float(src.quantity) - float(ln.quantity)
src.save(update_fields=['quantity'])
# ВАЖНО: не объединяем с существующими позициями на складе-получателе,
# чтобы сохранялась история поступлений/дат/партий.
StockItem.objects.create(
material=src.material,
entity=src.entity,
location_id=tr.to_location_id,
quantity=float(ln.quantity),
is_remnant=src.is_remnant,
current_length=src.current_length,
current_width=src.current_width,
)
tr.status = 'received'
tr.receiver_id = receiver_id
tr.received_at = timezone.now()
tr.is_applied = True
tr.save(update_fields=['status', 'receiver_id', 'received_at', 'is_applied'])