I have an Expense
model and an ExpenseLineItem
model. Just like a typical expense/invoice, one expense can have several line items to make up the total cost of an invoice. I'm trying to use class based views to create and update expenses. I've successfully coded the CreateView
to make a new expense with multiple expense line items.
My problem is when I try and update an existing Expense which already has several expense line items. Here's my code below, and I can't figure out what the issue is. The mixins (TitleMixin
, CancelSuccessMixin
, SelectedApartment
)are mine and work fine.
I'm getting an error that, I believe, means that it's trying to save a new copy of the ExpenseLineItems
but fails since those already exist. Almost like I'm not providing an instance
argument.
What am I doing wrong?
forms.py
class ExpenseForm(ModelForm):
class Meta:
model = Expense
fields = ['apart', 'inv_num', 'vendor', 'due_date']
ExpenseLineItemFormset = inlineformset_factory(Expense, ExpenseLineItem, fields=('description', 'account', 'amt'), can_delete=False)
Here's my ExpenseUpdate
view:
class ExpenseUpdate(TitleMixin, CancelSuccessMixin, SelectedApartment, UpdateView):
model = Expense
form_class = ExpenseForm
template_name = 'accounting/expense.html'
def get(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
expense_line_item_form = ExpenseLineItemFormset(instance = self.object)
return self.render_to_response(self.get_context_data(form = form, expense_line_item_form = expense_line_item_form))
def post(self, request, *args, **kwargs):
self.object = self.get_object()
form_class = self.get_form_class()
form = self.get_form(form_class)
expense_line_item_form = ExpenseLineItemFormset(self.request.POST, instance=self.object)
if (form.is_valid() and expense_line_item_form.is_valid()):
return self.form_valid(form, expense_line_item_form)
return self.form_invalid(form, expense_line_item_form)
def form_valid(self, form, expense_line_item_form):
self.object = form.save()
expense_line_item_form.instance = self.object
expense_line_item_form.save()
return HttpResponseRedirect(self.get_success_url())
def form_invalid(self, form, expense_line_item_form):
return self.render_to_response(self.get_context_data(form=form, expense_line_item_form=expense_line_item_form))
Error code I get:
MultiValueDictKeyError at /stuff/2/accounting/update-expense/25/
"u'expenselineitem_set-0-id'"
Request Method: POST
Request URL: http://localhost:8000/stuff/2/accounting/update-expense/25/
Django Version: 1.8.3
Exception Type: MultiValueDictKeyError
Exception Value:
"u'expenselineitem_set-0-id'"
Exception Location: /usr/local/lib/python2.7/dist-packages/django/utils/datastructures.py in __getitem__, line 322
Edit: Relevant part of my template:
<form class="form-horizontal" action="" method="post">
{% csrf_token %}
{% load widget_tweaks %}
<div class="row">
<div class="col-md-12">
<table class="table table-tight">
<thead>
<th>Description</th>
<th class="text-right">Account</th>
<th class="text-right">Amount</th>
</thead>
<tbody>
{{ expense_line_item_form.management_form }}
{% for eli in expense_line_item_form %}
<tr>
<td>{{ eli.description|attr:'cols:29' }}</td>
<td class="text-right">{{ eli.account }}</td>
<td class="text-right">{{ eli.amt }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div class="col-md-12 text-right">
<a href="{{ cancel }}" class="btn btn-default btn-lg">Cancel</a>
<input class="btn btn-success btn-lg" type="submit" value="Post" />
</div>
<br><br>
</form>
EDIT -- Working Form Template I thought I would add the working version of my template, should someone else need it:
<tbody>
{{ expense_line_item_form.management_form }}
{% for eli in expense_line_item_form %}
<tr>
<td>{{ eli.id }} {{ eli.description|attr:'cols:29' }}</td> <!-- <<==== Here's where I simply added {{ eli.id }}. That's all I changed :) -->
<td class="text-right">{{ eli.account }}</td>
<td class="text-right">{{ eli.amt }}</td>
</tr>
{% endfor %}
</tbody>
You need to include the form id for each form in the formset (it won't be shown to the user, as it is rendered as a hidden input). Without that form, the value is missing from the POST data, and you get a KeyError
as you are seeing.
From the formset docs:
Notice how we need to explicitly render
{{ form.id }}
. This ensures that the model formset, in the POST case, will work correctly. (This example assumes a primary key named id. If you’ve explicitly defined your own primary key that isn’t called id, make sure it gets rendered.)
In your case, you are looping through the formset with {% for eli in expense_line_item_form %}
, so you need to include {{ eli.id }}
.