Django: первая запись данных не сохраняется, когда поле = ноль

Я новичок в Django и сейчас пытаюсь найти причину, по которой первая запись данных из formset не сохраняется (включается) в базу данных, когда числовое поле poQty модели ProductOrder равно нулю.

QueryDict, кажется, работает с 4 записями в этом примере, но первая запись не включается в базу данных, когда productorder_set-0-poQty равен нулю:

<QueryDict: {'csrfmiddlewaretoken': ['6b85qEA4LMz7f3lcUvlpAi74BATLiouApAE1AKLMQhz0I7Gohvmw8jhAHnMf82LT'], 'orderOpen': ['on'], 'orderTable': ['02'], 'menuQuery': ['1'],
'productorder_set-TOTAL_FORMS': ['4'], 'productorder_set-INITIAL_FORMS': ['0'], 'productorder_set-MIN_NUM_FORMS': ['0'], 'productorder_set-MAX_NUM_FORMS': ['1000'], 
'productorder_set-0-prodQuery': ['1'], 'productorder_set-0-poQty': ['0'], 'productorder_set-0-poOrder': [''], 'productorder_set-0-id': [''], 
'productorder_set-1-prodQuery': ['2'], 'productorder_set-1-poQty': ['0'], 'productorder_set-1-poOrder': [''], 'productorder_set-1-id': [''], 
'productorder_set-2-prodQuery': ['4'], 'productorder_set-2-poQty': ['0'], 'productorder_set-2-poOrder': [''], 'productorder_set-2-id': [''], 
'productorder_set-3-prodQuery': ['3'], 'productorder_set-3-poQty': ['0'], 'productorder_set-3-poOrder': [''], 'productorder_set-3-id': ['']}>

Я был бы признателен за помощь в этом... спасибо!

Вот более подробная информация:

models.py

class ProductClass(models.Model):
    classDescription = models.CharField(max_length=255, verbose_name='Type', unique=True, null=True)
    classPrint = models.BooleanField(default=True, verbose_name='Print?')

    class Meta:
        verbose_name_plural = 'Product Classes'

    def __str__(self):
        return self.classDescription
    
class Product(models.Model):
    prodclassQuery = models.ForeignKey(ProductClass, on_delete=models.PROTECT, verbose_name='Product Class', default=1)
    prodDescription = models.CharField(max_length=255, verbose_name='Product')
    prodPrice = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='Price')

    class Meta:
        ordering = ['prodclassQuery', 'prodDescription']

    def __str__(self):
        return self.prodDescription

class Menu(models.Model):
    menuActive = models.BooleanField(verbose_name='Active?', default=False)
    menuDescription = models.CharField(max_length=255, verbose_name='Menu')
    prodQuery = models.ManyToManyField(Product, verbose_name='Product')
    
    class Meta:
        ordering = ['menuDescription', ]
    
    def __str__(self):
        return self.menuDescription
    
class Order(models.Model):
    orderDtOpen = models.DateTimeField(auto_now_add=True)
    orderDtClose = models.DateTimeField(auto_now=True)
    orderOpen = models.BooleanField(default=True, verbose_name='Open?')
    orderTable = models.CharField(max_length=25, verbose_name='Table')
    menuQuery = models.ForeignKey(Menu, on_delete=models.PROTECT, verbose_name='Menu', default=1)
    prodQuery = models.ManyToManyField(Product, through='ProductOrder')
    
class ProductOrder(models.Model):
    poOrder = models.ForeignKey(Order, on_delete=models.CASCADE, verbose_name='Order', default=1)
    prodQuery = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name='Product', default=1)
    poQty = models.PositiveIntegerField(default=0, verbose_name='Quantity')

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=('poOrder', 'prodQuery'), name='once_per_product_order')
        ]

forms.py

class OrderForm(ModelForm):
    class Meta:
        model = Order
        fields = ['orderOpen', 'orderTable', 'menuQuery',]

    def __init__(self, *args, **kwargs):
        super(OrderForm, self).__init__(*args, **kwargs)
        self.fields['menuQuery'].queryset = Menu.objects.filter(menuActive=True)

class ProductOrderForm(ModelForm):
    filtered_products = None

    class Meta:
        model = ProductOrder
        fields = ['prodQuery', 'poQty',]
        widgets = {'prodQuery': TextInput}

    def __init__(self, *args, **kwargs):
        super(ProductOrderForm, self).__init__(*args, **kwargs)
        self.filtered_products = Product.objects.filter(menu__prodQuery = self.instance.prodQuery)
        self.fields['prodQuery'].queryset = self.filtered_products

views.py

def create_order(request):
    form = OrderForm(request.POST or None, request.FILES or None)
    filtered_products = ProductOrderForm()
    ProductOrderFormset = inlineformset_factory(Order, Order.prodQuery.through, form = ProductOrderForm, 
                                                extra = len(filtered_products.filtered_products),
                                                can_delete = False)
    if request.POST and form.is_valid():
        order = form.save(commit=False)
        formset = ProductOrderFormset(request.POST, instance=order)
        if formset.is_valid():
            try:  
                order.save()
                formset.save()
                return redirect('/orders')
            except:
                pass
    else:
        formset = ProductOrderFormset(request.POST or None, instance=Order(), 
                                      initial=[{'prodQuery': prod.id} for prod in filtered_products.filtered_products],)
    context = {
        'form': form, 
        'formset': formset,
    }
    return render(request,'create_order.html', context)

шаблон

    <form method="post">
        {% csrf_token %}
        {% load custom_tags %}
        {{ form.orderOpen.label }} {{ form.orderOpen }}
        {{ form.orderTable.label }} {{ form.orderTable }}
        {{ form.menuQuery.label }} {{ form.menuQuery }}

        {{ formset.management_form }}
        {% for form in formset %}
        
        <table>
        <tr>
            <th><label for="{{ form.prodQuery.auto_id }}">
                {% with idx=forloop.counter0 %}
                {% with prod=form.fields.prodQuery.queryset|index:idx %}
                {{ prod.prodclassQuery }} {{ prod }} {{ prod.prodPrice }}            
                {% endwith %}
                {% endwith %}
            </label></th>
            <td>  
                <input type="hidden" name="{{ form.prodQuery.html_name }}" value="{{ form.prodQuery.value }}" cols="80" rows="20" id="{{ form.prodQuery.auto_id }}">
            </td>
        </tr>
        <tr>
            <!--<th><label for="{{ form.poQty.auto_id }}">Quantity:</label></th>-->
            <td>
                <button type="button" class="btn btn-outline-secondary" onclick="incrementValue('{{ form.poQty.auto_id }}')">+</button>
                <input type="number" name="{{ form.poQty.html_name }}" value="0" min="0" id="{{ form.poQty.auto_id }}">
                <button type="button" class="btn btn-outline-secondary" onclick="decreaseValue('{{ form.poQty.auto_id }}')">-</button>
                <input type="hidden" name="{{ form.poOrder.html_name }}" id="{{ form.poOrder.auto_id }}">
                <input type="hidden" name="{{ form.id.html_name }}" id="{{ form.id.auto_id }}">
            </td>
        </tr>
        </table>
        {% endfor %}
 
    <div>  
    <a type="button" class="btn btn-danger" href="{%url 'urlOrders' %}">Cancel</a>
    <button type="submit" class="btn btn-success">Create</button>
    </div>

    </form>

PS.: Я решил "подражать" шаблону inlineformset_factory, чтобы добавить несколько кнопок для увеличения/уменьшения количества товара.

Чтобы гарантировать, что первая запись всегда будет включена, независимо от значения poQty, вы можете установить extra в 1. Это гарантирует, что по крайней мере одна дополнительная форма будет включена в набор форм, даже если poQty будет нулевой для всех записей.

Используйте следующий вид:

def create_order(request):
    form = OrderForm(request.POST or None, request.FILES or None)
    filtered_products = ProductOrderForm()
    ProductOrderFormset = inlineformset_factory(Order, Order.prodQuery.through, form=ProductOrderForm, 
                                                extra=1, 
                                                can_delete=False)
    if request.POST and form.is_valid():
        order = form.save(commit=False)
        formset = ProductOrderFormset(request.POST, instance=order)
        if formset.is_valid():
            try:  
                order.save()
                formset.save()
                return redirect('/orders')
            except:
                pass
    else:
        formset = ProductOrderFormset(request.POST or None, instance=Order(), 
                                      initial=[{'prodQuery': prod.id} for prod in filtered_products.filtered_products])
    context = {
        'form': form, 
        'formset': formset,
    }
    return render(request, 'create_order.html', context)

Таким образом, установив значение extra в 1, набор форм всегда будет включать по крайней мере одну дополнительную форму, даже если поле poQty для первой записи равно нулю.

Редактирование

Как вы указали в комментарии, если установка extra=1 вызывает проблемы с загрузкой всех вариантов продукта в форме, вы можете попробовать другой подход, чтобы гарантировать, что первая запись всегда будет включена, независимо от значения poQty.

Один из способов добиться этого - проверить, есть ли в наборе форм отправленные данные. Если данных нет, можно вручную добавить форму для первого товара.

Попробуйте так:

def create_order(request):
    form = OrderForm(request.POST or None, request.FILES or None)
    filtered_products = ProductOrderForm()
    ProductOrderFormset = inlineformset_factory(Order, Order.prodQuery.through, form=ProductOrderForm, 
                                                extra=0,  # Set extra to 0 initially
                                                can_delete=False)
    if request.POST and form.is_valid():
        order = form.save(commit=False)
        formset = ProductOrderFormset(request.POST, instance=order)
        if formset.is_valid():
            try:  
                order.save()
                formset.save()
                return redirect('/orders')
            except:
                pass
    else:
        formset = ProductOrderFormset(request.POST or None, instance=Order())
        if not request.POST:
            initial_data = [{'prodQuery': prod.id} for prod in filtered_products.filtered_products]
            formset.forms.append(ProductOrderForm(initial=initial_data))
    context = {
        'form': form, 
        'formset': formset,
    }
    return render(request, 'create_order.html', context)

Теперь, даже если данные не будут отправлены, набор форм будет включать форму для первого продукта.

Редактирование 2

Похоже, что в качестве начального аргумента ожидается словарь с ключами, соответствующими полям формы, но вместо него получается список словарей.

Попробуйте это:

def create_order(request):
    form = OrderForm(request.POST or None, request.FILES or None)
    filtered_products = ProductOrderForm()
    ProductOrderFormset = inlineformset_factory(Order, Order.prodQuery.through, form=ProductOrderForm, 
                                                extra=0,  # Set extra to 0 initially
                                                can_delete=False)
    if request.POST and form.is_valid():
        order = form.save(commit=False)
        formset = ProductOrderFormset(request.POST, instance=order)
        if formset.is_valid():
            try:  
                order.save()
                formset.save()
                return redirect('/orders')
            except:
                pass
    else:
        formset = ProductOrderFormset(request.POST or None, instance=Order())
        if not request.POST:
            initial_data = [{'prodQuery': prod.id, 'poQty': None} for prod in filtered_products.filtered_products]
            formset.forms.append(ProductOrderForm(initial=data) for data in initial_data)
    context = {
        'form': form, 
        'formset': formset,
    }
    return render(request, 'create_order.html', context)

Другой подход:

Вы также можете попробовать установить начальное количество для форм в None вместо 0. Таким образом, Django распознает их как измененные и попытается сохранить их, как показано ниже:

else:
    formset = ProductOrderFormset(
        request.POST or None,
        instance=Order(),
        initial=[{'prodQuery': prod.id, 'poQty': None} for prod in filtered_products.filtered_products],
    )
Вернуться на верх