From d15c2149185ae787a8b2278156e665a6ff034efc Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Wed, 13 Sep 2017 07:09:44 +0200 Subject: [PATCH 1/8] fixed unset original article in new article form by using service API in full --- bizwiz/invoices/views.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/bizwiz/invoices/views.py b/bizwiz/invoices/views.py index 64cf6f5..65bc059 100644 --- a/bizwiz/invoices/views.py +++ b/bizwiz/invoices/views.py @@ -285,14 +285,22 @@ 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) + + # DEFECT: + # original article is not set this way + # --> incomplete data + # --> incorrect article sales report + # TODO: try to make use of full create_invoice API, do not take shortcut of formset + # TODO: clean up data by reading article references via name comparison + # TODO: implement report based on invoiced article names, not from original one + # TODO: remove "original" references (article and customer) if possible return self.form_valid(invoice_form) From b864c63d899edfe3abee4473be2c995f0d2b401d Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Wed, 13 Sep 2017 07:22:22 +0200 Subject: [PATCH 2/8] based query of sold articles on invoiced article names instead of original article names --- bizwiz/invoices/views.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/bizwiz/invoices/views.py b/bizwiz/invoices/views.py index 65bc059..1a5245d 100644 --- a/bizwiz/invoices/views.py +++ b/bizwiz/invoices/views.py @@ -361,12 +361,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) @@ -384,10 +379,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') @@ -398,7 +393,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) From b1c266b817fe861310437a30333bfc98c13d1bdb Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Wed, 13 Sep 2017 20:06:00 +0200 Subject: [PATCH 3/8] removed original article and customer --- bizwiz/invoices/forms.py | 4 +-- .../migrations/0014_auto_20170913_1944.py | 23 ++++++++++++++ bizwiz/invoices/models.py | 20 ++----------- bizwiz/invoices/services.py | 7 ----- bizwiz/invoices/views.py | 11 ++----- test/bizwiz/invoices/test_models.py | 2 -- test/bizwiz/invoices/test_services.py | 13 -------- test/bizwiz/invoices/test_views.py | 30 +++++-------------- 8 files changed, 37 insertions(+), 73 deletions(-) create mode 100644 bizwiz/invoices/migrations/0014_auto_20170913_1944.py 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 1a5245d..b43100e 100644 --- a/bizwiz/invoices/views.py +++ b/bizwiz/invoices/views.py @@ -293,15 +293,6 @@ def forms_valid(self, invoice_form, invoiced_article_formset): rebates=rebates, ) - # DEFECT: - # original article is not set this way - # --> incomplete data - # --> incorrect article sales report - # TODO: try to make use of full create_invoice API, do not take shortcut of formset - # TODO: clean up data by reading article references via name comparison - # TODO: implement report based on invoiced article names, not from original one - # TODO: remove "original" references (article and customer) if possible - return self.form_valid(invoice_form) @@ -338,6 +329,8 @@ class Sales(mixins.LoginRequiredMixin, SizedColumnsMixin, tables.SingleTableView """Sales per year.""" table_class = SalesTable column_widths = ('10%', '20%', '30%', '40%',) + + # TODO correct query to show yearly sum including rebates queryset = Invoice.objects \ .exclude(date_paid=None) \ .annotate(year_paid=functions.ExtractYear('date_paid')) \ 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..9dfc093 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, ), ] @@ -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 From 5e63ad329ea5ffe45fc0d430a114805a624afb3a Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Thu, 14 Sep 2017 07:23:22 +0200 Subject: [PATCH 4/8] fixed aggregation in sales --- bizwiz/invoices/views.py | 15 ++++++++++----- test/bizwiz/invoices/test_views.py | 2 +- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/bizwiz/invoices/views.py b/bizwiz/invoices/views.py index b43100e..dd7e163 100644 --- a/bizwiz/invoices/views.py +++ b/bizwiz/invoices/views.py @@ -330,16 +330,21 @@ class Sales(mixins.LoginRequiredMixin, SizedColumnsMixin, tables.SingleTableView table_class = SalesTable column_widths = ('10%', '20%', '30%', '40%',) - # TODO correct query to show yearly sum including rebates 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' diff --git a/test/bizwiz/invoices/test_views.py b/test/bizwiz/invoices/test_views.py index 9dfc093..faf32a3 100644 --- a/test/bizwiz/invoices/test_views.py +++ b/test/bizwiz/invoices/test_views.py @@ -168,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 From 69e36680fc2b1fd8db5be8c8b9dbd8c90d3fa047 Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Thu, 14 Sep 2017 18:03:25 +0200 Subject: [PATCH 5/8] documented changes of new version --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9e7a3cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,5 @@ +# 4.3.1 + +Various sales report fixes and improvements: +* 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). From 1c16a5a46fed7f4be68dd5dde5b9692805c96347 Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Thu, 14 Sep 2017 18:19:07 +0200 Subject: [PATCH 6/8] basic changelog display logic --- bizwiz/common/templates/common/welcome.html | 21 ++------------------- bizwiz/common/views.py | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/bizwiz/common/templates/common/welcome.html b/bizwiz/common/templates/common/welcome.html index 596b7ad..91af7fe 100644 --- a/bizwiz/common/templates/common/welcome.html +++ b/bizwiz/common/templates/common/welcome.html @@ -12,25 +12,8 @@

{% 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 'Changelog' %}

+

{{ changelog | safe }}

{% trans 'Resources' %}

{% blocktrans %} diff --git a/bizwiz/common/views.py b/bizwiz/common/views.py index c03f355..a1eaf0f 100644 --- a/bizwiz/common/views.py +++ b/bizwiz/common/views.py @@ -1,14 +1,33 @@ +import logging +import os + +from django import conf from django.views import generic 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) + + # read an render changelog markdown: + try: + with open(os.path.join(conf.settings.BASE_DIR, 'CHANGELOG.md')) as changelogfile: + changelog = changelogfile.read() + except FileNotFoundError: + _logger.warning('Changelog not found.') + changelog = '' + + return super().get_context_data( + version=BIZWIZ_VERSION, + changelog=changelog, + **kwargs + ) class SizedColumnsMixin: From ab9b0ed784c2f1b82da3b1c419dc0e088ba68df8 Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Thu, 14 Sep 2017 19:36:50 +0200 Subject: [PATCH 7/8] added display of changelog --- CHANGELOG.md | 1 + bizwiz/common/templates/common/changelog.html | 5 +++++ bizwiz/common/templates/common/welcome.html | 6 +----- bizwiz/common/urls.py | 1 + bizwiz/common/views.py | 14 ++++++++++++-- requirements.txt | 3 +++ 6 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 bizwiz/common/templates/common/changelog.html diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e7a3cc..044f048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # 4.3.1 Various sales report fixes and improvements: + * 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). 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 91af7fe..d85dfc2 100644 --- a/bizwiz/common/templates/common/welcome.html +++ b/bizwiz/common/templates/common/welcome.html @@ -12,8 +12,6 @@

{% trans 'Welcome to Bizwiz 4' %}

aspiring photographer. {% endblocktrans %}

-

{% trans 'Changelog' %}

-

{{ changelog | safe }}

{% trans 'Resources' %}

{% blocktrans %} @@ -23,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 a1eaf0f..cb52b62 100644 --- a/bizwiz/common/views.py +++ b/bizwiz/common/views.py @@ -3,6 +3,7 @@ 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 @@ -13,18 +14,27 @@ class Welcome(generic.TemplateView): template_name = 'common/welcome.html' + def get_context_data(self, **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 = changelogfile.read() + changelog = markdown.markdown(changelogfile.read()) except FileNotFoundError: _logger.warning('Changelog not found.') changelog = '' return super().get_context_data( - version=BIZWIZ_VERSION, changelog=changelog, **kwargs ) 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 From d4bd91af80624e1de0293f085267bfa25f1c6bb0 Mon Sep 17 00:00:00 2001 From: Mike Pagel Date: Thu, 14 Sep 2017 19:37:35 +0200 Subject: [PATCH 8/8] added markdown to production package list --- requirements-prod.txt | 1 + 1 file changed, 1 insertion(+) 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