Django Импорт-экспорт: Проблемы с импортом в модель с UUID для поля id

Я пытаюсь импортировать файл csv (в кодировке utf-8) через Django Admin в модель Django, используя пакет Django-import-export (v3.0.0b4). Изначально я работал с последней стабильной версией, но мне предложили попробовать предварительную версию. Предварительный просмотр импорта выглядит правильно, но интерфейс показывает следующую ошибку для всех строк csv:

Без указания поля "" не является действительным UUID.

Я пробовал несколько вариантов включения 'id' в import_id_fields или исключения поля 'id' и использования переименованного поля 'unique_id' в качестве крючка. Я также пытался выполнить импорт с пустыми записями как в столбце 'id', так и в столбце 'unique_id' в csv. Также поле id было полностью опущено в csv. По какой-то причине пустые поля возвращаются независимо от того, заполняю я поля id или нет. Я подозреваю, что я делаю что-то небольшое неправильно, но не могу понять что. resources.py, models.py и admin.py включены ниже. Рад поделиться другими фрагментами, если это необходимо.

models.py

from django.db import models
from django.db.models import Sum
import uuid
from datetime import datetime

class Purchase(models.Model):
    id = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False)
    date = models.DateField(blank=True,null=True)
    seller = models.CharField(max_length=200,default='')
    number = models.CharField(max_length=200,blank=True,default='')
    customer = models.CharField(max_length=200,default='')
    salesperson = models.CharField(max_length=200,blank=True,default='')
    discount = models.FloatField(blank=True,default=0,null=True)
    shipping = models.FloatField(blank=True,default=0,null=True)
    tax = models.FloatField(blank=True,default=0,null=True)
    file = models.FileField(blank=True,default='', upload_to='uploads/')

    @property
    def subtotal(self):
        return LineItem.objects.filter(Purchase=self).aggregate(Sum('amount'))['amount__sum']
    @property
    def grand_total(self):
        return round(self.subtotal+self.tax,2)

    class Meta:
        verbose_name_plural = "Purchases"
        
    def __str__(self):
        return self.seller+" "+self.number

class LineItem(models.Model):
    id = models.UUIDField(primary_key=True,default=uuid.uuid4, editable=False)
    purchase = models.ForeignKey(Purchase, on_delete= models.CASCADE, related_name="LineItem",default='',null=True)
    product_id = models.CharField(max_length=200,blank=True,default='')
    name = models.CharField(max_length=200,default='')
    category = models.CharField(max_length=200,blank=True,default='')
    qty = models.FloatField(null=True)
    qty_uom = models.CharField(max_length=200,default='')
    amount = models.FloatField(null=True)
    pack_qty = models.FloatField(null=True)
    pack_uom = models.CharField(max_length=200,default='')

    @property
    def date(self):
        return format(self.purchase.date,f"%m/%d/%Y")
    @property
    def bulk_unit_price(self):
        return round(self.amount/self.qty,2)
    @property
    def unit_price(self):
        return round(self.bulk_unit_price/self.pack_qty,2)

    class Meta:
        verbose_name_plural = "LineItems"
    
    def __str__(self):
        return self.name

resources.py

from import_export import resources, widgets, fields
from django.db.models.query import *
from .models import LineItem, Purchase

class CharRequiredWidget(widgets.CharWidget):
    def clean(self, value, row=None, *args, **kwargs):
        val = super().clean(value)
        if val:
            return val
        else:
            raise ValueError('this field is required')

class FloatWidget(widgets.DecimalWidget):
    def clean (self, value, row=None, *args, **kwargs):
        if self.is_empty(value):
            return None
        return float(str(value))

class ForeignKeyWidgetWithCreation(widgets.ForeignKeyWidget):
    def __init__(self, model, field="pk", create=False, **kwargs):
        self.model = model
        self.field = field
        self.create = create
        super(ForeignKeyWidgetWithCreation, self).__init__(model, field=field, **kwargs)

    def clean(self, value, **kwargs):
        if not value:
            return None

        if self.create:
            self.model.objects.get_or_create(**{self.field: value})

        val = super(ForeignKeyWidgetWithCreation, self).clean(value, **kwargs)

        return self.model.objects.get(**{self.field: val}) if val else None


class LineItemResource(resources.ModelResource):

    class Meta:
        model = LineItem
        import_id_fields = ['unique_id',]
        exclude = ('id',)
        fields = ('unique_id','purchase__seller','purchase__number','purchase__date','product_id','name','category','qty','qty_uom','amount','pack_qty','pack_uom',)
        report_skipped = True
    
    unique_id = fields.Field(column_name='unique_id', attribute='id',widget=CharRequiredWidget())
    purchase__seller = fields.Field(attribute='purchase', widget=ForeignKeyWidgetWithCreation(model=Purchase,field='seller',create=True))
    purchase__number = fields.Field(attribute='purchase', widget=ForeignKeyWidgetWithCreation(model=Purchase,field='number',create=True))
    purchase__date = fields.Field(attribute='purchase',widget=ForeignKeyWidgetWithCreation(model=Purchase,field='date',create=True))
    product_id = fields.Field(saves_null_values=False, attribute='product_id',widget=CharRequiredWidget())
    name = fields.Field(saves_null_values=False, attribute='name',widget=CharRequiredWidget())
    category = fields.Field(saves_null_values=False, attribute='category',widget=CharRequiredWidget())
    qty = fields.Field(saves_null_values=False, attribute='qty',widget=FloatWidget())
    qty_uom = fields.Field(saves_null_values=False, attribute='qty_uom',widget=CharRequiredWidget())
    amount = fields.Field(saves_null_values=False, attribute='amount',widget=FloatWidget())
    pack_qty = fields.Field(saves_null_values=False, attribute='pack_qty',widget=FloatWidget())
    pack_uom = fields.Field(saves_null_values=False, attribute='pack_uom',widget=CharRequiredWidget())

admin.py

from django.contrib import admin
from django.contrib import admin
from .models import Purchase, LineItem
from .resources import LineItemResource
from django.db import models
from import_export.admin import ImportExportModelAdmin

class LineItemAdmin(ImportExportModelAdmin):
    resource_class = LineItemResource
    list_display = ('id','purchase','product_id','name','category','qty','qty_uom','amount','pack_qty','pack_uom',)

admin.site.register(Purchase)
admin.site.register(LineItem, LineItemAdmin)

csv структура:

id,unique_id,purchase__date,purchase__seller,purchase__number,product_id,name,category,amount,qty,qty_uom,unit_price,pack_qty,pack_uom,$/unit  
,4e157e12-9a92-e303-44af-ee494593f073,4/29/2022,Vendor 1, 1423840,733111,item 1, category 1, 153.92,9.65,lb,15.95,1,lb,15.95

На первый взгляд, факт наличия пустого поля id может иметь значение (хотя вы говорите, что пытались опустить это поле). Можно попробовать удалить все ссылки на id из Resource и csv, хотя декларацию unique_id нужно будет оставить как есть.

Вы также можете попробовать использовать ForeignKeyWidget вместо ForeignKeyWidgetWithCreation во время отладки.

Вы можете попробовать импортировать через скрипт, а не через консоль администратора. Например:

    with open('data.csv', 'r') as fh:
        dataset = tablib.Dataset().load(fh, headers=False)

    resource = LineItemResource()
    result = resource.import_data(
         dataset,
         raise_errors=True
     )
    print(result)

Это может помочь вам понять ошибку, хотя самым быстрым способом обнаружить ошибку будет прохождение с отладчиком.

Вы также можете попробовать использовать релиз 2.8.1, чтобы посмотреть, есть ли разница (в случае, если это было введено в v3). Пожалуйста, опубликуйте ответ, если вы обнаружили источник ошибки.

Было бы очень интересно, если бы вы смогли воспроизвести это в django-import-export example app. В ветке release-3-x есть тестовая модель под названием UUIDBook. Если вы можете воспроизвести это, используя пример приложения, пожалуйста, поднимите проблему и мы сможем посмотреть.

Вернуться на верх