Fellipe, forms como o Luan disse são o caminho. Eles vão te ajudar na exibição de validação dos dados da busca.
Vou te passar uma visão geral de como eu trabalho com filtros aqui, pode ser muita informação mas le com calma que vc digere.
Por vezes, filtros podem se tornar muito complexos, e ai é legal vc trabalhar um pouco mais isso.
Por exemplo, vc tem o campo chave_nota, pra este exemplo vou considerar que ele é um charfield e unique. Vou considerar também que a pessoa quando digitar o valor neste campo do form, quer uma busca exata, ou seja, não preciso filtrar por data.
No caso acima, havendo o campo chave_nota eu só preciso filtrar com um chave_nota__iexact=value.
Um outro ponto, é que com quaisquers tipos de campos, quando existem campos de data, eles se aplicam mais ou menos assim:
data_entre=(inicial, final) E (campo1=x ou campo2=x ou campo3=x)
Essas coisas de filtro sempre vão complicar um pouco e eu recomendo sempre quebrar o filtro em algumas partes: Validação do form > Validação de valores padrões vs valores enviados > Construção das Lookups > aplicação na query.
Em function based views, vc faria as validações usando os métodos clean do form e os valores iniciciais enviaos ao form. A construção dos lookups poderia se dar no save() do form e a aplicação na view, usando os valores do save.
Em class based views, eu costumo ter um filter_form. Este filter_form é processado por um método get_filters() que eu crio usando o cleaned_data do form e um dicionario de valores iniciais. O resultado do get_filters é então aplicado a queryset pelo apply_filters(). Fica algo como:
def get_queryset(self):
return self.apply_filters(super(Klass, self).get_queryset())
def apply_filters(self, queryset):
filters = self.get_filters()
# aplica os fitlers ai qs, um a um
return queryset