diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b6116d..572f6b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,24 @@ # Changelog All notable changes to this project will be documented in this file. -The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) -and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased] +## Unreleased -## [4.3.0] - 2017-09-08 +## 4.3.1 - 2017-09-14 +### Added + +- Display of changelog. + +### Fixed + +- Annual income now exact sum of all invoices. +- Drilldown to articles sold in given year now shows article names from invoice, not the originally selected article (from master data). + +## 4.3.0 - 2017-09-08 ### Added - Excel export of invoice lists. - TODO ### Changed - TODO - -### Removed - -[Unreleased]: https://github.com/moltob/pybizwiz/compare/4.3.0...HEAD -[4.3.0]: https://github.com/moltob/pybizwiz/compare/4.3.0...v4.2.0 diff --git a/bizwiz/common/templates/common/changelog.html b/bizwiz/common/templates/common/changelog.html new file mode 100644 index 0000000..65a5662 --- /dev/null +++ b/bizwiz/common/templates/common/changelog.html @@ -0,0 +1,5 @@ +{% extends "common/base.html" %} + +{% block content %} + {{ changelog | safe }} +{% endblock content %} diff --git a/bizwiz/common/templates/common/welcome.html b/bizwiz/common/templates/common/welcome.html index 596b7ad..d85dfc2 100644 --- a/bizwiz/common/templates/common/welcome.html +++ b/bizwiz/common/templates/common/welcome.html @@ -12,25 +12,6 @@

{% trans 'Welcome to Bizwiz 4' %}

aspiring photographer. {% endblocktrans %}

-

{% trans 'Background' %}

-

- {% blocktrans %} - With the official start of the photography business at - http://www.bpfotografie.de/, Bizwiz started in early 2007 as a Windows - forms application based on the .NET framework. The first version used in production - was Bizwiz 2 and was written in C# and used MSSQL as a server backend. - {% endblocktrans %} -

-

- {% blocktrans %} - After production use until end of 2016, Bizwiz was fully rewritten as a Django web - application, utilizing Python as well as the typical web languages. Bizwiz is still - a server-rendered (classic) web application, as it is truly data-driven and the - classic render-edit-post cycle applies very well. In addition, learning the classic - Django framework was also an objective of the project. Today, Bizwiz is running - within a Docker container on virtually any platform. - {% endblocktrans %} -

{% trans 'Resources' %}

{% blocktrans %} @@ -40,8 +21,6 @@

{% trans 'Resources' %}

https://hub.docker.com/r/mpagel/bizwiz. {% endblocktrans %}

- -

Version: {{ version }}

- +

Version: {{ version }}

{% endblock content %} diff --git a/bizwiz/common/urls.py b/bizwiz/common/urls.py index 757015d..a14bfc7 100644 --- a/bizwiz/common/urls.py +++ b/bizwiz/common/urls.py @@ -6,5 +6,6 @@ urlpatterns = [ url(r'^$', views.Welcome.as_view(), name='index'), + url(r'^changelog$', views.Changelog.as_view(), name='changelog'), url(r'^session-filter/$', views.SessionFilter.as_view(), name='session-filter'), ] diff --git a/bizwiz/common/views.py b/bizwiz/common/views.py index c03f355..cb52b62 100644 --- a/bizwiz/common/views.py +++ b/bizwiz/common/views.py @@ -1,14 +1,43 @@ +import logging +import os + +from django import conf from django.views import generic +import markdown from bizwiz.common.session_filter import set_session_filter from bizwiz.version import BIZWIZ_VERSION +_logger = logging.getLogger(__name__) + class Welcome(generic.TemplateView): template_name = 'common/welcome.html' def get_context_data(self, **kwargs): - return super().get_context_data(version=BIZWIZ_VERSION, **kwargs) + return super().get_context_data( + version=BIZWIZ_VERSION, + **kwargs + ) + + +class Changelog(generic.TemplateView): + template_name = 'common/changelog.html' + + def get_context_data(self, **kwargs): + + # read an render changelog markdown: + try: + with open(os.path.join(conf.settings.BASE_DIR, 'CHANGELOG.md')) as changelogfile: + changelog = markdown.markdown(changelogfile.read()) + except FileNotFoundError: + _logger.warning('Changelog not found.') + changelog = '' + + return super().get_context_data( + changelog=changelog, + **kwargs + ) class SizedColumnsMixin: diff --git a/bizwiz/invoices/forms.py b/bizwiz/invoices/forms.py index f500655..46bcfee 100644 --- a/bizwiz/invoices/forms.py +++ b/bizwiz/invoices/forms.py @@ -170,7 +170,7 @@ class Meta: class InvoicedCustomerForm(forms.ModelForm): class Meta: model = InvoicedCustomer - exclude = ('original_customer', 'invoice') + exclude = ('invoice', ) helper = helper.FormHelper() helper.form_tag = False @@ -253,7 +253,7 @@ class BaseInvoicedArticleFormset(forms.BaseInlineFormSet): min_num=1, validate_min=True, extra=0, - exclude=('original_article', 'invoice') + exclude=('invoice', ) ) diff --git a/bizwiz/invoices/migrations/0014_auto_20170913_1944.py b/bizwiz/invoices/migrations/0014_auto_20170913_1944.py new file mode 100644 index 0000000..ca9c409 --- /dev/null +++ b/bizwiz/invoices/migrations/0014_auto_20170913_1944.py @@ -0,0 +1,23 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-09-13 17:44 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('invoices', '0013_auto_20170903_1541'), + ] + + operations = [ + migrations.RemoveField( + model_name='invoicedarticle', + name='original_article', + ), + migrations.RemoveField( + model_name='invoicedcustomer', + name='original_customer', + ), + ] diff --git a/bizwiz/invoices/models.py b/bizwiz/invoices/models.py index 30e1847..5867214 100644 --- a/bizwiz/invoices/models.py +++ b/bizwiz/invoices/models.py @@ -78,14 +78,6 @@ class InvoicedArticle(ArticleBase): default=ItemKind.ARTICLE, blank=True, ) - original_article = models.ForeignKey( - Article, - on_delete=models.SET_NULL, - verbose_name=_("Original article"), - blank=True, - null=True, - related_name='invoiced_articles', - ) amount = models.PositiveSmallIntegerField(_("Amount")) invoice = models.ForeignKey( Invoice, @@ -100,7 +92,7 @@ class Meta: @classmethod def create(cls, invoice, article, amount): - invoiced_article = InvoicedArticle(invoice=invoice, original_article=article, amount=amount) + invoiced_article = InvoicedArticle(invoice=invoice, amount=amount) if article: copy_field_data(ArticleBase, article, invoiced_article) return invoiced_article @@ -115,14 +107,6 @@ def total(self): class InvoicedCustomer(CustomerBase): - original_customer = models.ForeignKey( - Customer, - on_delete=models.SET_NULL, - verbose_name=_("Original customer"), - blank=True, - null=True, - related_name='invoiced_customer', - ) invoice = models.OneToOneField( Invoice, verbose_name=_("Invoice"), @@ -136,6 +120,6 @@ class Meta: @classmethod def create(cls, invoice, customer): - invoiced_customer = InvoicedCustomer(invoice=invoice, original_customer=customer) + invoiced_customer = InvoicedCustomer(invoice=invoice) copy_field_data(CustomerBase, customer, invoiced_customer) return invoiced_customer diff --git a/bizwiz/invoices/services.py b/bizwiz/invoices/services.py index 69362c9..3059841 100644 --- a/bizwiz/invoices/services.py +++ b/bizwiz/invoices/services.py @@ -81,15 +81,8 @@ def create_invoice(*, customer, invoiced_articles=None, project=None, rebates=No _logger.info('Creating new invoice {} for customer {}.'.format(invoice.number, customer)) if invoiced_articles: - # get articles from database by names: - names = {a.name for a in invoiced_articles} - articles = Article.objects.filter(name__in=names) - article_by_name = {a.name: a for a in articles} - for invoiced_article in invoiced_articles: - original_article = article_by_name.get(invoiced_article.name) invoiced_article.invoice = invoice - invoiced_article.original_article = original_article invoiced_article.save() _logger.debug(' {}'.format(invoiced_article)) diff --git a/bizwiz/invoices/views.py b/bizwiz/invoices/views.py index 64cf6f5..dd7e163 100644 --- a/bizwiz/invoices/views.py +++ b/bizwiz/invoices/views.py @@ -285,14 +285,13 @@ def forms_valid(self, invoice_form, invoiced_article_formset): rebates = invoice_form.cleaned_data['rebates'] with transaction.atomic(): + invoiced_articles = invoiced_article_formset.save(commit=False) invoice = services.create_invoice( project=project, + invoiced_articles=invoiced_articles, customer=original_customer, rebates=rebates, ) - invoiced_article_formset.instance = invoice - invoiced_article_formset.save() - services.refresh_rebates(invoice) return self.form_valid(invoice_form) @@ -330,15 +329,22 @@ class Sales(mixins.LoginRequiredMixin, SizedColumnsMixin, tables.SingleTableView """Sales per year.""" table_class = SalesTable column_widths = ('10%', '20%', '30%', '40%',) + queryset = Invoice.objects \ .exclude(date_paid=None) \ .annotate(year_paid=functions.ExtractYear('date_paid')) \ .values('year_paid') \ - .filter(invoiced_articles__kind=ItemKind.ARTICLE) \ - .annotate(num_invoices=models.Count('id', distinct=True), - num_articles=models.Sum('invoiced_articles__amount'), - total=models.Sum( + .annotate(total=models.Sum( models.F('invoiced_articles__price') * models.F('invoiced_articles__amount') + )) \ + .annotate(num_invoices=models.Count('id', distinct=True), + num_articles=models.Sum( + models.Case( + models.When(invoiced_articles__kind=ItemKind.ARTICLE, + invoiced_articles__price__gt=0, + then='invoiced_articles__amount'), + default=0 + ) )) template_name = 'invoices/sales_list.html' @@ -353,12 +359,7 @@ def get_context_data(self, **kwargs): class ArticleSalesTable(tables.Table): - name = tables.LinkColumn( - 'articles:update', - args=[tables.utils.A('original_article__pk')], - verbose_name=_("Invoice text"), - accessor='original_article__name' - ) + name = tables.Column(_("Invoice text")) amount = tables.Column(_("Ordered"), accessor='year_amount', attrs=COLUMN_RIGHT_ALIGNED) total = tables.Column(_('Total value'), attrs=COLUMN_RIGHT_ALIGNED) @@ -376,10 +377,10 @@ class ArticleSales(mixins.LoginRequiredMixin, SizedColumnsMixin, tables.SingleTa def get_queryset(self): return InvoicedArticle.objects \ - .filter(kind=ItemKind.ARTICLE, original_article__isnull=False) \ + .filter(kind=ItemKind.ARTICLE) \ .filter(price__gt=0) \ .filter(invoice__date_paid__year=self.kwargs['year']) \ - .values('original_article__pk', 'original_article__name') \ + .values('name') \ .annotate(year_amount=models.Sum('amount'), total=models.Sum(models.F('price') * models.F('amount'))) \ .order_by('-year_amount') @@ -390,7 +391,7 @@ def get_context_data(self, **kwargs): # prepare chart data by separating x and y axis: queryset = context['object_list'] article_amounts = [a['year_amount'] for a in queryset] - article_names = [a['original_article__name'] for a in queryset] + article_names = [a['name'] for a in queryset] # clear names of articles with minor contribution: total_amount = sum(article_amounts) diff --git a/requirements-prod.txt b/requirements-prod.txt index 5931a67..6031c19 100644 --- a/requirements-prod.txt +++ b/requirements-prod.txt @@ -5,6 +5,7 @@ django-crispy-forms==1.6.1 django-jquery-js==3.1.1 django-tables2==1.10.0 gunicorn==19.7.1 +Markdown==2.6.9 olefile==0.44 Pillow==4.2.1 py==1.4.34 diff --git a/requirements.txt b/requirements.txt index 6aac9d6..32c0861 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,9 @@ pytest-django # PDF generation reportlab +# markdown to html (e.g. for changelog display) +markdown + # serve static files simply via gunicorn whitenoise diff --git a/test/bizwiz/invoices/test_models.py b/test/bizwiz/invoices/test_models.py index 5c628eb..3bc96c7 100644 --- a/test/bizwiz/invoices/test_models.py +++ b/test/bizwiz/invoices/test_models.py @@ -31,7 +31,6 @@ def test__invoiced_customer__create(): assert c2.company_name == 'COMPANY_NAME' assert c2.zip_code == 'ZIP' assert c2.city == 'CITY' - assert c2.original_customer == c1 def test__invoiced_article__create(): @@ -48,7 +47,6 @@ def test__invoiced_article__create(): assert a2.name == 'NAME' assert a2.price == 1.2 assert a2.amount == 1 - assert a2.original_article == a1 assert a2.kind == ItemKind.ARTICLE diff --git a/test/bizwiz/invoices/test_services.py b/test/bizwiz/invoices/test_services.py index 9649eb7..40694ee 100644 --- a/test/bizwiz/invoices/test_services.py +++ b/test/bizwiz/invoices/test_services.py @@ -92,7 +92,6 @@ def test__create_invoice__global(customer, posted_articles): invoice = create_invoice(customer=customer, invoiced_articles=posted_articles) assert not invoice.project - assert invoice.invoiced_customer.original_customer == customer invoiced_articles = list(invoice.invoiced_articles.all()) assert len(invoiced_articles) == 2 assert invoiced_articles[0].name == 'A1' @@ -123,18 +122,6 @@ def test__create_invoice__number_unique(customer, posted_articles): assert invoice.number not in {'123', '45'} -@pytest.mark.django_db -def test__create_invoice__original_article(customer, posted_articles): - article = Article(name='A1', price=5) - article.save() - - invoice = create_invoice(customer=customer, invoiced_articles=posted_articles) - - invoiced_articles = invoice.invoiced_articles.all() - assert invoiced_articles[0].original_article == article - assert invoiced_articles[1].original_article is None - - @pytest.fixture def invoice_with_rebates(customer, posted_articles): rebate1 = Rebate(name='NAME1', kind=RebateKind.PERCENTAGE, value=10, auto_apply=False) diff --git a/test/bizwiz/invoices/test_views.py b/test/bizwiz/invoices/test_views.py index 0266114..faf32a3 100644 --- a/test/bizwiz/invoices/test_views.py +++ b/test/bizwiz/invoices/test_views.py @@ -72,13 +72,6 @@ def test__sales__queryset__empty(): @pytest.fixture def invoices_for_aggregation(): - articles = [ - Article(name='A', price=1), - Article(name='B', price=2), - ] - for a in articles: - a.save() - invoices = [ Invoice(number=1), Invoice(number=2, date_paid=datetime.date(2016, 1, 2)), @@ -91,46 +84,41 @@ def invoices_for_aggregation(): invoice_articles1 = [ InvoicedArticle( invoice=invoices[0], - name='A1', + name='A', price=1, amount=1, - original_article=articles[0], kind=ItemKind.ARTICLE, ), ] invoiced_articles2 = [ InvoicedArticle( invoice=invoices[1], - name='B1', + name='A', price=10.03, amount=5, - original_article=articles[0], kind=ItemKind.ARTICLE, ), InvoicedArticle( invoice=invoices[1], - name='B2', + name='B', price=11, amount=6, - original_article=articles[1], kind=ItemKind.ARTICLE, ), ] invoiced_articles3 = [ InvoicedArticle( invoice=invoices[2], - name='C1', + name='A', price=100, amount=10, - original_article=articles[0], kind=ItemKind.ARTICLE, ), InvoicedArticle( invoice=invoices[2], - name='C2', + name='B', price=101, amount=11, - original_article=articles[1], kind=ItemKind.ARTICLE, ), InvoicedArticle( @@ -144,18 +132,16 @@ def invoices_for_aggregation(): invoiced_articles4 = [ InvoicedArticle( invoice=invoices[3], - name='D1', + name='A', price=1000, amount=20, - original_article=articles[0], kind=ItemKind.ARTICLE, ), InvoicedArticle( invoice=invoices[3], - name='D2', + name='B', price=1001, amount=21, - original_article=articles[1], kind=ItemKind.ARTICLE, ), ] @@ -182,7 +168,7 @@ def test__sales__queryset__computation(invoices_for_aggregation): assert sales_2016['year_paid'] == 2016 assert sales_2016['num_invoices'] == 2 assert sales_2016['num_articles'] == 11 + 10 + 6 + 5 - assert sales_2016['total'] == decimal.Decimal('2227.15') + assert sales_2016['total'] == decimal.Decimal('2217.15') @pytest.mark.django_db @@ -191,7 +177,7 @@ def test__article_sales__queryset__computation(invoices_for_aggregation): sales.kwargs = dict(year=2016) qs = sales.get_queryset() - sales_by_article_name = {s['original_article__name']: s for s in qs} + sales_by_article_name = {s['name']: s for s in qs} assert len(sales_by_article_name) == 2