diff --git a/assets/js/custom.js b/assets/js/custom.js index 641c805..61d85cf 100644 --- a/assets/js/custom.js +++ b/assets/js/custom.js @@ -1,62 +1,27 @@ -$(document).ready(function(){ - /*client*/ - - $('input#id_first_name').keyup(function(){ - var f = $(this).val(); - var l = $('input#id_last_name').val() - $('input#id_display_name').val(f+' '+l); - }); - - $('input#id_last_name').keyup(function(){ - var l = $(this).val(); - var f = $('input#id_first_name').val() - $('input#id_display_name').val(f+' '+l); - }); - - - if ($( "select#id_item_type").val() == 'fixed'){ - $('input#id_amount').removeAttr("disabled") - }else if($( "select#id_item_type").val() == 'quantity'){ - $('input#id_quantity').removeAttr("disabled") - $('input#id_rate').removeAttr("disabled") - - } - - - - - $( "select#id_item_type").click(function(){ - var selected = $(this).val(); - - if (selected == 'quantity'){ - - $('input#id_quantity').removeAttr("disabled") - $('input#id_rate').removeAttr("disabled") - $('input#id_amount').attr("disabled", "true") - - } - if(selected == 'fixed'){ - - $('input#id_quantity').attr("disabled", "true") - $('input#id_rate').attr("disabled", "true") - $('input#id_amount').removeAttr("disabled") - } - }); - - - -$( "select#items").click(function(){ - var selected = $(this).val(); - - $('div#item-pick').append(''+selected+'') - -}); - - - - - - -}); + $(document).ready(function() { + $('input#id_first_name').keyup(function(){ + var f = $(this).val(); + var l = $('input#id_last_name').val() + $('input#id_display_name').val(f+' '+l); + }); + + $('input#id_last_name').keyup(function(){ + var l = $(this).val(); + var f = $('input#id_first_name').val() + $('input#id_display_name').val(f+' '+l); + }); + + if ($( "select#id_item_type").val() == 'fixed'){ + $('input#id_amount').removeAttr("disabled") + }else if($( "select#id_item_type").val() == 'quantity'){ + $('input#id_quantity').removeAttr("disabled") + $('input#id_rate').removeAttr("disabled") + } + + $( "select#items").click(function(){ + var selected = $(this).val(); + $('div#item-pick').append(''+selected+'') + }); + }); \ No newline at end of file diff --git a/assets/js/formset.js b/assets/js/formset.js new file mode 100644 index 0000000..cedfc64 --- /dev/null +++ b/assets/js/formset.js @@ -0,0 +1,150 @@ +'use strict'; + +let formset = function() { + + let totalForm = Number( $('#id_form-TOTAL_FORMS').val() ); + let totalFormCounter; + let arr = [] + + + let amountComputation = function(){ + // Total amount calculation + $('.field-quantity').on('keydown keyup keypress focusout focus', function() { + let rate = $(this).parent().children('.field-rate').children().val(); + let quantity = $(this).children().val(); + let total = parseInt(rate) * parseInt(quantity); + + if (!isNaN(total)){ + $(this).parent().children('.field-amount').text(total); + } + }); + $('.field-rate').on('keydown keyup keypress focusout focus', function() { + let rate = $(this).children().val(); + let quantity = $(this).parent().children('.field-quantity').children().val(); + let total = parseInt(rate) * parseInt(quantity); + + if (!isNaN(total)){ + $(this).parent().children('.field-amount').text(total); + } + }); + } + + // Display created elements + for(let counter=0; counter < totalForm; counter++){ + let rowForm = $('.row-item').find('input[name="form-'+counter+'-description"]').parent().parent().attr('id',counter); + let del = $('.form-row#'+counter).find('.field-delete').children().attr('id','id_form-'+counter+'-delete'); + + // Add remove text + if ( del.attr('id') != 'id_form-0-delete'){ + del.text('remove'); + } + + let rate = $('.form-row#'+counter).find('.field-rate').children().val(); + let quantity = $('.form-row#'+counter).find('.field-quantity').children().val(); + let total = parseInt(rate) * parseInt(quantity); + + if (!isNaN(total)){ + $('.form-row#'+counter).find('.field-amount').text(total); + } + + // Delete order form + $('a#id_form-'+counter+'-delete').on('click', function(){ + let totalForm = Number( $('#id_form-TOTAL_FORMS').val() ); + let newTotalForm = $('#id_form-TOTAL_FORMS').val(totalForm-1) ; + if ( $(this).attr('id') != 'id_form-0-delete' ) { + let removeForm = $(this).parent().parent(); + removeForm.remove(); + } + }); + + amountComputation(); + + // Sub-t0tal computation + $('.field-amount').each(function(counter){ + arr[counter] = Number( $(this).text() ) + }); + let subTotal = 0 + for(let counter=0; counter' + +'
' + +'
' + +'
' + +'Edit' + +'Delete' + +'|Generate PDF|' + +' Send' + +'
' + +'
' + +'
' + +'' + +'
' + +'
' + +' ' + +'
' + +'
' + +obj[data].fields.invoice_number + +'
' + +'
' + +'
' + +'
' + +' ' + +'
' + +'
' + +obj[data].fields.client + +'
' + +'
' + +'
' + +'
' + +' ' + +'
' + +'
' + +obj[data].fields.item + +'
' + +'
' + +'
' + +'
' + +' ' + +'
' + +'
' + +obj[data].fields.invoice_date + +'
' + +'
' + +'
' + +'
' + +' ' + +'
' + +'
' + +obj[data].fields.due_date + +'
' + +'
' + +'
' + +'
' + +' ' + +'
' + +'
' + +obj[data].fields.remarks + +'
' + +'
' + ) + } + }); + }); + }); diff --git a/invoices/admin.py b/invoices/admin.py index 23ffee9..64a8d87 100644 --- a/invoices/admin.py +++ b/invoices/admin.py @@ -1,5 +1,13 @@ from django.contrib import admin -from invoices.models import Invoice +from invoices.models import Invoice, Item -# Register your models here. -admin.site.register(Invoice) \ No newline at end of file + +class ItemInline(admin.TabularInline): + model = Item + +class InvoiceAdmin(admin.ModelAdmin): + inlines = [ItemInline] + + +admin.site.register(Invoice, InvoiceAdmin) +admin.site.register(Item) \ No newline at end of file diff --git a/invoices/forms.py b/invoices/forms.py index 1f7ee6d..0979643 100644 --- a/invoices/forms.py +++ b/invoices/forms.py @@ -2,19 +2,23 @@ from django import forms from django.conf import settings +from django.forms import BaseFormSet, formset_factory -from invoices.models import Invoice from clients.models import Client from datetime import date - +from invoices.models import Invoice, Item class InvoiceForm(forms.ModelForm): """invoice form """ - due_date = forms.DateField(input_formats=settings.DATE_INPUT_FORMATS) - invoice_date = forms.DateField(input_formats=settings.DATE_INPUT_FORMATS) + due_date = forms.DateField(widget=forms.DateInput(attrs={'type':'date'}), + input_formats=settings.DATE_INPUT_FORMATS + ) + invoice_date = forms.DateField(widget=forms.DateInput(attrs={'type':'date'}), + input_formats=settings.DATE_INPUT_FORMATS + ) class Meta: model = Invoice @@ -22,11 +26,9 @@ class Meta: 'due_date', 'invoice_number', 'invoice_date', - 'paid', 'remarks', 'description', 'payment_status', - 'item', ) def __init__(self,*args, **kwargs): @@ -35,7 +37,6 @@ def __init__(self,*args, **kwargs): self.company = kwargs.pop('company', None) return super(InvoiceForm, self).__init__(*args, **kwargs) - def clean_invoice_date(self): """ Invoice date validation """ @@ -60,7 +61,9 @@ def clean_invoice_number(self): """ Invoice number unique together validation """ invoice_number = self.cleaned_data.get('invoice_number') - invoice_number_q = Invoice.objects.filter(invoice_number__exact=invoice_number, company=self.company) + invoice_number_q = Invoice.objects.filter(invoice_number__exact=invoice_number, + company=self.company + ) if not self.instance : if invoice_number_q.exists(): raise forms.ValidationError("Invoice Number already exists:") @@ -70,7 +73,6 @@ def clean_invoice_number(self): return invoice_number - class InvoiceEmailForm(forms.Form): """invoice form for send email """ @@ -97,3 +99,29 @@ def clean_text(self): return text +class ItemForm(forms.ModelForm): + """ Add item form + """ + class Meta: + model = Item + fields = ('description', + 'quantity', + 'rate' + ) + + +class BaseItemFormSet(BaseFormSet): + def clean(self): + if any(self.errors): + return + + for count,form in enumerate(self.forms): + if not form.data['form-'+str(count)+'-description']: + print("not description") + raise forms.ValidationError("Description is required!") + if not form.data['form-'+str(count)+'-quantity']: + print("not qty") + raise forms.ValidationError("Quantity is required!") + if not form.data['form-'+str(count)+'-rate']: + print("not rate") + raise forms.ValidationError("Rate is required!") diff --git a/invoices/migrations/0001_initial.py b/invoices/migrations/0001_initial.py index 6c6c75f..34a1080 100644 --- a/invoices/migrations/0001_initial.py +++ b/invoices/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 2.0 on 2018-02-02 03:42 +# Generated by Django 2.0 on 2018-02-14 08:54 +from django.conf import settings from django.db import migrations, models import django.db.models.deletion import invoices.utils @@ -10,7 +11,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('clients', '0001_initial'), + ('users', '0005_remove_company_owner'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('clients', '0010_client_archive'), ] operations = [ @@ -18,18 +21,38 @@ class Migration(migrations.Migration): name='Invoice', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('archive', models.BooleanField(default=False)), + ('date_created', models.DateTimeField(auto_now_add=True)), + ('date_updated', models.DateTimeField(auto_now=True)), + ('description', models.TextField(blank=True, max_length=255, null=True)), + ('due_date', models.DateField()), ('invoice_number', models.PositiveIntegerField()), ('invoice_date', models.DateField()), - ('due_date', models.DateField()), - ('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent')], default='draft', max_length=10)), - ('paid', models.BooleanField(default=False)), - ('remarks', models.TextField(blank=True, max_length=255, null=True)), - ('pdf', models.FileField(blank=True, null=True, upload_to=invoices.utils.get_invoice_directory)), - ('description', models.CharField(blank=True, max_length=255, null=True)), ('payment_status', models.BooleanField(default=False)), + ('pdf', models.FileField(blank=True, null=True, upload_to=invoices.utils.get_invoice_directory)), + ('remarks', models.TextField(blank=True, max_length=255, null=True)), + ('status', models.CharField(choices=[('draft', 'Draft'), ('sent', 'Sent')], default='draft', max_length=10)), + ('client', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='clients.Client')), + ('company', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Company')), + ('owner', models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='invoice', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='Item', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.PositiveIntegerField()), ('date_created', models.DateTimeField(auto_now_add=True)), ('date_updated', models.DateTimeField(auto_now=True)), - ('client', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='clients.Client')), + ('description', models.CharField(blank=True, max_length=255, null=True)), + ('quantity', models.PositiveIntegerField(blank=True, null=True)), + ('rate', models.PositiveIntegerField(blank=True, null=True)), + ('invoice', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='invoices.Invoice')), + ('owner', models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='items', to=settings.AUTH_USER_MODEL)), ], ), + migrations.AlterUniqueTogether( + name='invoice', + unique_together={('invoice_number', 'company')}, + ), ] diff --git a/invoices/migrations/0002_auto_20180202_0342.py b/invoices/migrations/0002_auto_20180202_0342.py deleted file mode 100644 index 590ba80..0000000 --- a/invoices/migrations/0002_auto_20180202_0342.py +++ /dev/null @@ -1,39 +0,0 @@ -# Generated by Django 2.0 on 2018-02-02 03:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('users', '0001_initial'), - ('invoices', '0001_initial'), - ('items', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='invoice', - name='company', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='users.Company'), - ), - migrations.AddField( - model_name='invoice', - name='item', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='items.Item'), - ), - migrations.AddField( - model_name='invoice', - name='owner', - field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='invoice', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterUniqueTogether( - name='invoice', - unique_together={('invoice_number', 'company')}, - ), - ] diff --git a/invoices/migrations/0002_auto_20180219_0339.py b/invoices/migrations/0002_auto_20180219_0339.py new file mode 100644 index 0000000..5cb1f42 --- /dev/null +++ b/invoices/migrations/0002_auto_20180219_0339.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0 on 2018-02-19 03:39 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='description', + field=models.CharField(max_length=255), + ), + ] diff --git a/invoices/migrations/0003_auto_20180202_0454.py b/invoices/migrations/0003_auto_20180202_0454.py deleted file mode 100644 index 09f4702..0000000 --- a/invoices/migrations/0003_auto_20180202_0454.py +++ /dev/null @@ -1,24 +0,0 @@ -# Generated by Django 2.0 on 2018-02-02 04:54 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0002_auto_20180202_0342'), - ] - - operations = [ - migrations.AlterField( - model_name='invoice', - name='client', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='clients.Client'), - ), - migrations.AlterField( - model_name='invoice', - name='company', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.Company'), - ), - ] diff --git a/invoices/migrations/0003_auto_20180219_0848.py b/invoices/migrations/0003_auto_20180219_0848.py new file mode 100644 index 0000000..274be3c --- /dev/null +++ b/invoices/migrations/0003_auto_20180219_0848.py @@ -0,0 +1,23 @@ +# Generated by Django 2.0 on 2018-02-19 08:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0002_auto_20180219_0339'), + ] + + operations = [ + migrations.AlterField( + model_name='item', + name='quantity', + field=models.PositiveIntegerField(), + ), + migrations.AlterField( + model_name='item', + name='rate', + field=models.PositiveIntegerField(), + ), + ] diff --git a/invoices/migrations/0004_auto_20180205_0713.py b/invoices/migrations/0004_auto_20180205_0713.py deleted file mode 100644 index 7d9b448..0000000 --- a/invoices/migrations/0004_auto_20180205_0713.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0 on 2018-02-05 07:13 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0003_auto_20180202_0454'), - ] - - operations = [ - migrations.AlterField( - model_name='invoice', - name='invoice_number', - field=models.PositiveIntegerField(blank=True, null=True), - ), - ] diff --git a/invoices/migrations/0005_auto_20180205_0742.py b/invoices/migrations/0005_auto_20180205_0742.py deleted file mode 100644 index 4a2654e..0000000 --- a/invoices/migrations/0005_auto_20180205_0742.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0 on 2018-02-05 07:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('invoices', '0004_auto_20180205_0713'), - ] - - operations = [ - migrations.AlterField( - model_name='invoice', - name='invoice_number', - field=models.PositiveIntegerField(), - ), - ] diff --git a/invoices/models.py b/invoices/models.py index 499837d..0592182 100644 --- a/invoices/models.py +++ b/invoices/models.py @@ -1,16 +1,15 @@ -from django.db import models from django.conf import settings +from django.db import models + -from items.models import Item from clients.models import Client from users.models import User, Company from invoices.utils import get_invoice_directory - class Invoice(models.Model): - '''creating database for invoice - ''' + """ Creating database for invoice + """ DRAFT = 'draft' SENT = 'sent' @@ -18,28 +17,27 @@ class Invoice(models.Model): (DRAFT, 'Draft'), (SENT, 'Sent'), ) - owner = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE, related_name='invoice', default='') - company = models.ForeignKey(Company, on_delete=models.CASCADE) + archive = models.BooleanField(default=False) client = models.ForeignKey(Client, on_delete=models.SET_NULL, null=True) - + company = models.ForeignKey(Company, on_delete=models.CASCADE) + date_created = models.DateTimeField(auto_now_add=True) + date_updated = models.DateTimeField(auto_now=True) + description = models.TextField(max_length=255, null=True, blank=True) + due_date = models.DateField() invoice_number = models.PositiveIntegerField() invoice_date = models.DateField() - due_date = models.DateField() - item = models.ForeignKey(Item ,on_delete=models.SET_NULL, null=True) - - status = models.CharField(max_length=10, choices=STATUS, default='draft') - paid = models.BooleanField(default=False) - remarks = models.TextField(max_length=255,null=True, blank=True) - - pdf = models.FileField(upload_to=get_invoice_directory,null=True, blank=True) - description = models.CharField(max_length=255, null=True, blank=True) - + owner = models.ForeignKey(settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='invoice', + default='' + ) payment_status = models.BooleanField( default=False) - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) + pdf = models.FileField(upload_to=get_invoice_directory,null=True, blank=True) + remarks = models.TextField(max_length=255,null=True, blank=True) + status = models.CharField(max_length=10, choices=STATUS, default='draft') class Meta: - unique_together = (("invoice_number", "company"),) + unique_together = ('invoice_number', 'company') def __str__(self): return f"{self.invoice_number}" @@ -48,8 +46,21 @@ def get_invoice_number(self): return f"{self.invoice_number}".zfill(9) +class Item(models.Model): + """ Item form + """ + amount = models.PositiveIntegerField() + date_created = models.DateTimeField(auto_now_add=True) + date_updated = models.DateTimeField(auto_now=True) + description = models.CharField(max_length=255) + invoice = models.ForeignKey(Invoice, on_delete=models.CASCADE, null=True) + owner = models.ForeignKey(settings.AUTH_USER_MODEL, + on_delete=models.CASCADE, + related_name='items', + default='' + ) + quantity = models.PositiveIntegerField() + rate = models.PositiveIntegerField() - - - - + def __str__(self): + return f"{self.description}" diff --git a/invoices/urls.py b/invoices/urls.py index 98fb0cb..41525ff 100644 --- a/invoices/urls.py +++ b/invoices/urls.py @@ -8,7 +8,8 @@ InvoiceDeleteView, InvoiceEmailView, PdfPreview, - MakeInvoiceView + MakeInvoiceView, + InvoiceAjaxView, ) @@ -23,4 +24,6 @@ path('invoice/pdf//', PdfPreview.as_view(), name='invoice_pdf'), #make invoice for client path('clients/make_invoice//', MakeInvoiceView.as_view(), name='make_invoice'), + #ajax + path('invoice/ajax/view//', InvoiceAjaxView.as_view(), name='invoice_ajax'), ] \ No newline at end of file diff --git a/invoices/views.py b/invoices/views.py index a810137..47a54c8 100644 --- a/invoices/views.py +++ b/invoices/views.py @@ -1,11 +1,13 @@ import tempfile , time, os, errno +from django.core import serializers from django.contrib.auth.mixins import LoginRequiredMixin from django.conf import settings from django.core.mail import EmailMultiAlternatives from django.contrib import messages from django.db.models import Q -from django.http import Http404, HttpResponse +from django.forms import formset_factory +from django.http import Http404, HttpResponse, JsonResponse from django.shortcuts import render, redirect, get_object_or_404 from django.template.loader import get_template from django.views import View @@ -14,17 +16,15 @@ from clients.forms import ClientForm from clients.models import Client -from items.models import Item from invoices.mixins import PdfMixin, UserIsOwnerMixin -from invoices.models import Invoice -from invoices.forms import InvoiceForm, InvoiceEmailForm +from invoices.models import Invoice, Item +from invoices.forms import BaseItemFormSet, ItemForm, InvoiceForm, InvoiceEmailForm from users.models import User from xhtml2pdf import pisa from io import BytesIO - class IndexView(LoginRequiredMixin, TemplateView): template_name = 'index.html' @@ -64,7 +64,6 @@ def post(self, *args, **kwargs): return render(self.request, self.template_name, context=context) - class MakeInvoiceView(LoginRequiredMixin,TemplateView): """Make an invoice for client """ @@ -77,8 +76,12 @@ def get(self, *args, **kwargs): context['client'] = get_object_or_404(Client, pk=kwargs['client_id']) context['invoice_form'] = InvoiceForm() context['invoice_form'].fields['client'].empty_label = None - context['invoice_form'].fields['client'].queryset = Client.objects.filter(pk=kwargs['client_id']) - context['invoice_form'].fields['item'].queryset = Item.objects.filter(company=self.request.user.company) + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + pk=kwargs['client_id'] + ) + context['invoice_form'].fields['item'].queryset = Item.objects.filter( + company=self.request.user.company + ) return render(self.request, self.template_name, context) def post(self, *args, **kwargs): @@ -98,12 +101,12 @@ def post(self, *args, **kwargs): else: context = {} context['invoice_form'] = invoice_form - context['invoice_form'].fields['client'].queryset = Client.objects.filter(company=self.request.user.company) - context['invoice_form'].fields['item'].queryset = Item.objects.filter(company=self.request.user.company) + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + company=self.request.user.company + ) return render(self.request, self.template_name, context) - class InvoiceListView(LoginRequiredMixin ,TemplateView): """Display list of invoice """ @@ -112,14 +115,73 @@ class InvoiceListView(LoginRequiredMixin ,TemplateView): def get(self, *args, **kwargs): """Display invoices data """ - context = {} - invoices = Invoice.objects.filter(company=self.request.user.company) query = self.request.GET.get("q") if query: invoices = invoices.filter(invoice_number__icontains=query) + else: + invoices = Invoice.objects.filter(company=self.request.user.company) + context = {} + context['client_form'] = ClientForm() + ItemFormSet = formset_factory(ItemForm, formset=BaseItemFormSet) + context['formset'] = ItemFormSet() context['invoices'] = invoices + context['invoice_form'] = InvoiceForm() + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + company=self.request.user.company, + archive=False + ).order_by('-date_updated') return render(self.request, self.template_name, context) + def post(self, *args, **kwargs): + """ Get filled invoice form and create + """ + ItemFormSet = formset_factory(ItemForm, formset=BaseItemFormSet) + formset = ItemFormSet(self.request.POST) + invoice_form = InvoiceForm(self.request.POST, company=self.request.user.company) + + if invoice_form.is_valid() and formset.is_valid(): + # Save invoice + invoice = invoice_form.save(commit=False) + invoice.owner = self.request.user + invoice.company = self.request.user.company + invoice.save() + + # Check latest item id + try: + latest = Item.objects.latest('id') + item_id = latest.id + 1 + except: + latest = 0 + item_id = latest + 1 + + # Save item/s + for count,form in enumerate(formset): + item_form = ItemForm() + item_id = item_id + count + item = item_form.save(commit=False) + item.id = item_id + item.invoice = invoice + item.owner = self.request.user + item.description = form.data['form-'+str(count)+'-description'] + item.quantity = form.data['form-'+str(count)+'-quantity'] + item.rate = form.data['form-'+str(count)+'-rate'] + item.amount = int(item.rate) * int(item.quantity) + item.save() + messages.success(self.request, 'Invoice is successfully Added') + return redirect('invoices') + else: + invoices = Invoice.objects.filter(company=self.request.user.company) + context = {} + context['invoices'] = invoices + context['invoice_form'] = invoice_form + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + company=self.request.user.company, + archive=False + ).order_by('-date_updated') + context['formset'] = ItemFormSet(self.request.POST) + context['client_form'] = ClientForm(self.request.POST) + return render(self.request, self.template_name, context) + return render(self.request, self.template_name, context) class InvoiceView(UserIsOwnerMixin, TemplateView): @@ -137,6 +199,21 @@ def get(self, *args, **kwargs): return render(self.request, self.template_name, context) +class InvoiceAjaxView(UserIsOwnerMixin, View): + """View invoice information + """ + + def get(self, *args, **kwargs): + """Display pdf in browser + """ + invoice = get_object_or_404(Invoice, id=kwargs['invoice_id']) + data = serializers.serialize('json', [invoice]) + return JsonResponse({'invoice': data, + }, + safe = False, + status=200 + ) + class InvoiceAddView(LoginRequiredMixin,TemplateView): """Adding invoice @@ -148,8 +225,9 @@ def get(self, *args, **kwargs): """ context = {} context['invoice_form'] = InvoiceForm() - context['invoice_form'].fields['client'].queryset = Client.objects.filter(company=self.request.user.company) - context['invoice_form'].fields['item'].queryset = Item.objects.filter(company=self.request.user.company) + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + company=self.request.user.company + ) return render(self.request, self.template_name, context) def post(self, *args, **kwargs): @@ -166,8 +244,9 @@ def post(self, *args, **kwargs): else: context = {} context['invoice_form'] = invoice_form - context['invoice_form'].fields['client'].queryset = Client.objects.filter(company=self.request.user.company) - context['invoice_form'].fields['item'].queryset = Item.objects.filter(company=self.request.user.company) + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + company=self.request.user.company + ) return render(self.request, self.template_name, context) @@ -183,15 +262,19 @@ def get(self, *args, **kwargs): invoice = get_object_or_404(Invoice, pk=kwargs['invoice_id']) context = {} context['invoice_form'] = InvoiceForm(instance=invoice) - context['invoice_form'].fields['client'].queryset = Client.objects.filter(company=self.request.user.company) - context['invoice_form'].fields['item'].queryset = Item.objects.filter(company=self.request.user.company) + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + company=self.request.user.company + ) return render(self.request, self.template_name, context) def post(self, *args, **kwargs): """Get filled invoice form and create invoice """ invoice = get_object_or_404(Invoice, pk=kwargs['invoice_id']) - invoice_form = InvoiceForm(self.request.POST, instance=invoice, company=self.request.user.company) + invoice_form = InvoiceForm(self.request.POST, + instance=invoice, + company=self.request.user.company + ) if invoice_form.is_valid() : invoice_form.save() messages.success(self.request, 'Invoice is successfully updated') @@ -199,8 +282,9 @@ def post(self, *args, **kwargs): else: context = {} context['invoice_form'] = InvoiceForm(self.request.POST) - context['invoice_form'].fields['client'].queryset = Client.objects.filter(company=self.request.user.company) - context['invoice_form'].fields['item'].queryset = Item.objects.filter(company=self.request.user.company) + context['invoice_form'].fields['client'].queryset = Client.objects.filter( + company=self.request.user.company + ) return render(self.request, self.template_name, context) diff --git a/items/__init__.py b/items/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/items/admin.py b/items/admin.py deleted file mode 100644 index f70f107..0000000 --- a/items/admin.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.contrib import admin -from items.models import Item - -# Register your models here. -admin.site.register(Item) \ No newline at end of file diff --git a/items/apps.py b/items/apps.py deleted file mode 100644 index 4b1dd39..0000000 --- a/items/apps.py +++ /dev/null @@ -1,5 +0,0 @@ -from django.apps import AppConfig - - -class ItemsConfig(AppConfig): - name = 'items' diff --git a/items/forms.py b/items/forms.py deleted file mode 100644 index 2c8102c..0000000 --- a/items/forms.py +++ /dev/null @@ -1,84 +0,0 @@ -import datetime - - -from django import forms -from django.conf import settings - - -from clients.models import Client -from items.models import Item -from invoices.models import Invoice -from datetime import date - - - -class ItemForm(forms.ModelForm): - description = forms.CharField(widget=forms.Textarea) - - class Meta: - model = Item - fields = ('item_type', - 'order_number', - 'description', - 'rate', - 'quantity', - 'amount', - 'total_amount' - ) - - def clean_amount(self): - """amount validation - """ - item_type = self.data.get('item_type') - amount = self.data.get('amount') - if item_type == Item.FIXED: - if not amount: - raise forms.ValidationError("you pick fixed - amount should have a value") - return amount - - def clean_rate(self): - """rate validation - """ - quantity = self.data.get('quantity') - rate = self.data.get('rate') - if rate: - if not quantity: - raise forms.ValidationError("rate has value but doest have an quantity/s") - if not rate and quantity: - raise forms.ValidationError("quantity has value and rate must have value too") - return rate - - def clean_quantity(self): - """quantity validation - """ - item_type = self.data.get('item_type') - quantity = self.data.get('quantity') - rate = self.data.get('rate') - if item_type == Item.QUANTITY: - if not quantity: - raise forms.ValidationError("you pick quantity - quantity must have a value") - return quantity - - def save(self,commit=False): - """save invoice form - """ - instance = super(ItemForm, self).save(commit=False) - item_type = self.data.get('item_type') - amount = self.data.get('amount') - quantity = self.data.get('quantity') - rate = self.data.get('rate') - - if item_type == Item.FIXED: - instance.amount = amount - instance.quantity = None - instance.rate = None - instance.total_amount = amount - elif item_type == Item.QUANTITY : - instance.amount = None - instance.quantity = quantity - instance.rate = rate - instance.total_amount = (int(quantity)*int(rate) ) - if commit: - instance.save() - return instance - diff --git a/items/migrations/0001_initial.py b/items/migrations/0001_initial.py deleted file mode 100644 index 89bbf36..0000000 --- a/items/migrations/0001_initial.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 2.0 on 2018-02-02 03:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='Item', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('invoiced', models.BooleanField(default=False)), - ('item_type', models.CharField(choices=[('fixed', 'Fixed Price'), ('hourly', 'Hourly')], default='fixed', max_length=10)), - ('order_number', models.PositiveIntegerField(blank=True, null=True)), - ('description', models.CharField(blank=True, max_length=255, null=True)), - ('rate', models.PositiveIntegerField(blank=True, null=True)), - ('hours', models.PositiveIntegerField(blank=True, null=True)), - ('start_date', models.DateField()), - ('end_date', models.DateField()), - ('amount', models.PositiveIntegerField(blank=True, null=True)), - ('total_amount', models.PositiveIntegerField(blank=True, null=True)), - ('date_created', models.DateTimeField(auto_now_add=True)), - ('date_updated', models.DateTimeField(auto_now=True)), - ], - ), - ] diff --git a/items/migrations/0002_auto_20180202_0342.py b/items/migrations/0002_auto_20180202_0342.py deleted file mode 100644 index 78584f0..0000000 --- a/items/migrations/0002_auto_20180202_0342.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 2.0 on 2018-02-02 03:42 - -from django.conf import settings -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('users', '0001_initial'), - ('items', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.AddField( - model_name='item', - name='company', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='item_company', to='users.Company'), - ), - migrations.AddField( - model_name='item', - name='owner', - field=models.ForeignKey(default='', on_delete=django.db.models.deletion.CASCADE, related_name='items', to=settings.AUTH_USER_MODEL), - ), - ] diff --git a/items/migrations/0003_auto_20180202_0454.py b/items/migrations/0003_auto_20180202_0454.py deleted file mode 100644 index c24a7b0..0000000 --- a/items/migrations/0003_auto_20180202_0454.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 2.0 on 2018-02-02 04:54 - -from django.db import migrations, models -import django.db.models.deletion - - -class Migration(migrations.Migration): - - dependencies = [ - ('items', '0002_auto_20180202_0342'), - ] - - operations = [ - migrations.AlterField( - model_name='item', - name='company', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='item_company', to='users.Company'), - ), - ] diff --git a/items/migrations/0004_auto_20180205_0901.py b/items/migrations/0004_auto_20180205_0901.py deleted file mode 100644 index 101c786..0000000 --- a/items/migrations/0004_auto_20180205_0901.py +++ /dev/null @@ -1,31 +0,0 @@ -# Generated by Django 2.0 on 2018-02-05 09:01 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('items', '0003_auto_20180202_0454'), - ] - - operations = [ - migrations.RenameField( - model_name='item', - old_name='hours', - new_name='quantity', - ), - migrations.RemoveField( - model_name='item', - name='end_date', - ), - migrations.RemoveField( - model_name='item', - name='start_date', - ), - migrations.AlterField( - model_name='item', - name='item_type', - field=models.CharField(choices=[('fixed', 'Fixed Price'), ('quality', 'Quality')], default='fixed', max_length=10), - ), - ] diff --git a/items/migrations/0005_auto_20180205_0907.py b/items/migrations/0005_auto_20180205_0907.py deleted file mode 100644 index d09b31f..0000000 --- a/items/migrations/0005_auto_20180205_0907.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 2.0 on 2018-02-05 09:07 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('items', '0004_auto_20180205_0901'), - ] - - operations = [ - migrations.AlterField( - model_name='item', - name='item_type', - field=models.CharField(choices=[('fixed', 'Fixed Price'), ('quantity', 'Quantity')], default='fixed', max_length=10), - ), - ] diff --git a/items/migrations/__init__.py b/items/migrations/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/items/mixins.py b/items/mixins.py deleted file mode 100644 index 6c41b92..0000000 --- a/items/mixins.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.contrib.auth.mixins import AccessMixin -from django.shortcuts import get_object_or_404, redirect - - -from items.models import Item - - - -class UserIsOwnerMixin(AccessMixin): - """Check ownership request - """ - def dispatch(self, *args, **kwargs): - """ Request ownership check - """ - item = get_object_or_404(Item, id=kwargs['item_id']) - if not self.request.user.is_authenticated: - return self.handle_no_permission() - elif self.request.user != item.owner: - return redirect('index') - return super().dispatch(self.request, *args, **kwargs) \ No newline at end of file diff --git a/items/models.py b/items/models.py deleted file mode 100644 index a03a039..0000000 --- a/items/models.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.db import models -from django.db import models -from django.conf import settings - - -from clients.models import Client -from users.models import User, Company - - - -class Item(models.Model): - FIXED = 'fixed' - QUANTITY = 'quantity' - INVOICE_TYPE = ( - (FIXED, 'Fixed Price'), - (QUANTITY, 'Quantity'), - ) - owner = models.ForeignKey(settings.AUTH_USER_MODEL,on_delete=models.CASCADE, related_name='items', default='') - company = models.ForeignKey(Company, on_delete=models.CASCADE, null=True, related_name='item_company') - invoiced = models.BooleanField(default=False) - item_type = models.CharField(max_length=10,choices=INVOICE_TYPE, default='fixed') - order_number = models.PositiveIntegerField( null=True, blank=True) - description = models.CharField(max_length=255, null=True, blank=True) - rate = models.PositiveIntegerField(null=True, blank=True) - quantity = models.PositiveIntegerField(null=True, blank=True) - amount = models.PositiveIntegerField(null=True, blank=True) - total_amount = models.PositiveIntegerField(null=True, blank=True) - - date_created = models.DateTimeField(auto_now_add=True) - date_updated = models.DateTimeField(auto_now=True) - - def __str__(self): - return f"{self.order_number}" - - def total(self): - return self.rate*self.quantity \ No newline at end of file diff --git a/items/tests.py b/items/tests.py deleted file mode 100644 index 7ce503c..0000000 --- a/items/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/items/urls.py b/items/urls.py deleted file mode 100644 index a587f60..0000000 --- a/items/urls.py +++ /dev/null @@ -1,13 +0,0 @@ -from django.urls import path -from items.views import ItemListView, ItemView, ItemAddView, ItemEditView, ItemDeleteView - - - -urlpatterns = [ - path('items/', ItemListView.as_view(), name='items'), - path('item/view//', ItemView.as_view(), name='item_view'), - path('item/add/', ItemAddView.as_view(), name='item_add'), - path('item/edit//', ItemEditView.as_view(), name='item_edit'), - path('item/delete//', ItemDeleteView.as_view(), name='item_delete'), - -] \ No newline at end of file diff --git a/items/views.py b/items/views.py deleted file mode 100644 index 81c80bf..0000000 --- a/items/views.py +++ /dev/null @@ -1,135 +0,0 @@ -import tempfile , time, os, errno - - -from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.conf import settings -from django.core.mail import EmailMultiAlternatives -from django.db.models import Q -from django.http import HttpResponse -from django.shortcuts import render, redirect, get_object_or_404 -from django.template.loader import get_template -from django.template.loader import get_template -from django.views.generic import TemplateView - - -from clients.forms import ClientForm -from clients.models import Client -from invoices.models import Invoice -from invoices.forms import InvoiceForm, InvoiceEmailForm -from items.models import Item -from items.forms import ItemForm -from items.mixins import UserIsOwnerMixin -from users.models import User - -# for pdf -from xhtml2pdf import pisa -from io import BytesIO - - - -class ItemListView(LoginRequiredMixin,TemplateView): - """Display list of invoice - """ - template_name = 'items/all_item.html' - - def get(self, *args, **kwargs): - """Display invoices data - """ - context = {} - items = Item.objects.filter( company=self.request.user.company) - query = self.request.GET.get("q") - if query: - items = items.filter(order_number__icontains=query) - context['items'] = items - return render(self.request, self.template_name, context) - - - -class ItemView(UserIsOwnerMixin,TemplateView): - """View invoice information - """ - template_name = 'items/view_item.html' - - def get(self, *args, **kwargs): - """Get invoice information - """ - item = get_object_or_404(Item, pk=kwargs['item_id']) - item.save() - context = {'item': item, } - return render(self.request, self.template_name, context) - - - -class ItemAddView(LoginRequiredMixin,TemplateView): - """Adding invoice - """ - template_name = 'items/update_item.html' - - def get(self, *args, **kwargs): - """Display invoice form - """ - context = {} - context['form'] = ItemForm() - return render(self.request, self.template_name, context) - - def post(self, *args, **kwargs): - """Get filled invoice form and create - """ - form = ItemForm(self.request.POST) - if form.is_valid() : - item = form.save(commit=False) - item.owner = self.request.user - item.company = self.request.user.company - item.save() - messages.success(self.request, 'Item is successfully Added') - return redirect('items') - else: - context = {} - context['form'] = form - return render(self.request, self.template_name, context) - - - -class ItemEditView(UserIsOwnerMixin,TemplateView): - """Editing invoice - """ - template_name = 'items/update_item.html' - - def get(self, *args, **kwargs): - """Display invoice form - """ - item = get_object_or_404(Item, pk=kwargs['item_id']) - context = {} - context['form'] = ItemForm(instance=item) - return render(self.request, self.template_name, context) - - def post(self, *args, **kwargs): - """Get filled invoice form and create invoice - """ - item = get_object_or_404(Item, pk=kwargs['item_id']) - form = ItemForm(self.request.POST, instance=item) - if form.is_valid() : - item = form.save(commit=False) - item.save() - messages.success(self.request, 'Item is successfully updated') - return redirect('items') - else: - context = {} - context['form'] = ItemForm(self.request.POST) - return render(self.request, self.template_name, context) - - - -class ItemDeleteView(UserIsOwnerMixin,TemplateView): - """Delete invoice - """ - def get(self, *args, **kwargs): - """Display invoice data - """ - item = get_object_or_404(Item, pk=kwargs['item_id']) - item.delete() - messages.error(self.request, 'Client is successfully deleted') - return redirect('items') - - diff --git a/swift_invoice/settings.py b/swift_invoice/settings.py index 01ee567..2f45dbe 100644 --- a/swift_invoice/settings.py +++ b/swift_invoice/settings.py @@ -41,7 +41,6 @@ #apps 'users', 'clients', - 'items', 'invoices', 'widget_tweaks', diff --git a/swift_invoice/urls.py b/swift_invoice/urls.py index b90e5b7..d82aaea 100644 --- a/swift_invoice/urls.py +++ b/swift_invoice/urls.py @@ -10,6 +10,5 @@ path('', include('users.urls')), path('', include('clients.urls')), path('', include('invoices.urls')), - path('', include('items.urls')), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/templates/base.html b/templates/base.html index d85cace..dd38a91 100644 --- a/templates/base.html +++ b/templates/base.html @@ -5,15 +5,12 @@ Invoice - - - -
{% if request.user.is_authenticated %}
@@ -105,8 +101,13 @@ - + + + + +{% block js %} +{% endblock %} \ No newline at end of file diff --git a/templates/invoices/all_invoice.html b/templates/invoices/all_invoice.html index c32a085..09dfe9b 100644 --- a/templates/invoices/all_invoice.html +++ b/templates/invoices/all_invoice.html @@ -1,53 +1,348 @@ {% extends 'base.html' %} +{% load widget_tweaks %} {% block content_auth %} {% if messages %}
    {% for message in messages %} - {{ message }} - - × + {{ message }} + × {% endfor %}
{% endif %} -
+

Invoice

-
- - - - - - - - - - - - - - - - {% for invoice in invoices %} - - - - - - - - - - - + {% for invoice in invoices %} +
+
+
+
+
{{ invoice.client.client_company }}
+
{{ invoice.get_invoice_number }}
+
{{ invoice.due_date }}
+
{{ invoice.status }}
+
+
+ {% endfor %} + + +
+
+
+ {% csrf_token %} +
+

New Invoice


+
+
+ +
+
+
+
+ {{ invoice_form.invoice_number |attr:"placeholder:Invoice #"|attr:"class:form-control" }} +
+
+ {{ invoice_form.invoice_number.errors.as_text }} +
+
+
+
+
+
+ +
+
+
+
+ {{ invoice_form.description |attr:"placeholder:Description"|attr:"class:form-control" }} +
+
+ {{ invoice_form.description.errors.as_text }} +
+
+
+
+
+
+ +
+
+
+
{{ invoice_form.client }}
+ + {{ invoice_form.client.errors.as_text }}
+
+
+
+
+ +
+
+
+
+ {{ invoice_form.invoice_date|attr:"placeholder: yyyy-mm-dd"|attr:"class:form-control" }} +
+
+ {{ invoice_form.invoice_date.errors.as_text }} +
+
+
+
+
+
+ +
+
+
+
+ {{ invoice_form.due_date|attr:"placeholder: yyyy-mm-dd"|attr:"class:form-control" }}
+
{{ invoice_form.due_date.errors.as_text }}
+
+
+
+
+
+
+ +
+
+ Add order +
+ +
+
Order description
+
Qty
+
Rate
+
Amount
+
+
+ +
+ {{ formset.management_form }} + + {% for form in formset %} + + +
+ + {% if formset.non_form_errors %} + + {{ form.non_form_errors }} + {% for error in formset.non_form_errors %} + {{ error|escape }} {% endfor %} -
-
Invoice #Client nameOrder #StatusDate CreatedEditDelete
{{ invoice.invoice_number }}{{ invoice.client }}{{ invoice.item }}{{ invoice.status}}{{ invoice.date_created }}EditDelete
+ {% endif %} + + + {% if form.description.errors %} + + [{{ forloop.counter }}]description error: {{ form.description.errors.as_text }} + +
+ {% endif %} + {% if form.quantity.errors %} + + [{{ forloop.counter }}]quantity error: {{ form.quantity.errors.as_text }} + +
+ {% endif %} + {% if form.rate.errors %} + + [{{ forloop.counter }}]rate error: {{ form.rate.errors.as_text }} + +
+ {% endif %} + {% if form.amount.errors %} + + [{{ forloop.counter }}]amount error: {{ form.amount.errors.as_text }} + +
+ {% endif %} +
+ + +
+
+ {{ form.description|attr:"class:form-control"|attr:"required:True" }} +
+
+ {{ form.quantity|attr:"class:form-control" }} +
+
+ {{ form.rate|attr:"class:form-control" }} +
+
+
+
+ +
+
+ + + + + + + {% endfor %} +
+
+ +
+
+
+
+
+
+
+ +
+
+ +
+
+
+
+ {{ invoice_form.remarks|attr:"placeholder:Remarks"|attr:"class:form-control"}}
+
{{ invoice_form.remarks.errors.as_text }}
+
+
+
+
+

+
+
+ + +
+
+
+
+ +
+
+ + + + {% endblock %} + +{% block js %} + {% endblock %} +