Podsumowań projektu część druga: co sobie chwalę?

Po wytknięciu samemu sobie niedociągnięć i zaniedbań w pracy nad projektem, mogę z czystym sumieniem podsumować to, z czego jestem zadowolony.

Spełnienie wymagań konkursu
Wymagania konkursowe nie były zbyt obszerne: należało przez minimum dziesięć tygodni pracować nad dowolnym projektem, efekty pracy opisując na blogu. O spełnieniu tych wymagań mówić mogę dopiero dzisiaj, ponieważ ten wpis jest dwudziestym poświęconym konkursowi, a dopiero taka liczba notek, według regulaminu, jest przepustką do etapu głosowania. Jak widać, ten niepozorny wydawałoby się wymóg, okazał się nie być wcale prosty do zrealizowania. Tak czy inaczej, udało się. Udowodniłem sobie, że potrafię zmotywować się i dostosować do narzuconego z góry tempa pracy. Traktuję to jako cenne doświadczenie, ponieważ nigdy wcześniej nie miałem takiego sprawdzianu.

Daj się poznać!
O ile do spełnienia wymagań konkursu zobowiązywał reguamin, o tyle wypełnienie jego idei zależało już tylko od uczestników. Z satysfakcją stwierdzam, że konkurs był świetną okazją i do „dania się poznać”, i do „poznania”. Regularnie odwiedzałem blogi uczestników, a także innych bloggerów, czy to z ciekawośći, czy to w poszukiwaniu wiedzy. W kwestii „dania się poznać”, muszę wyrazić bardzo pozytywne zaskoczenie. Po pierwsze, nie spodziewałem się tak dużej liczby odwiedzin na blogu. Po drugie, wiele komentarzy zadziwiło mnie ilością ciekawych informacji o tematyce związanej z fakturami VAT, za co dziękuję ich autorom.
Garść statystyk:
– liczba odwiedzin bloga: 3821;
– „busiest day”: 6.09.2010 (150 odwiedzin);
– liczba wpisów konkursowych: 20;
– najpopularniejszy wpis: Postać Docelowa Faktury (562 odwiedzin);
– liczba „wydotnetomaniakowanych” wpisów: 6.

Nauka
Celem konkursu, nie mniej istotnym od dołączenia do internetowej społeczności .NET-owej, było poznanie nowych technologii i praktyk programistycznych. Na tym polu osiągnąłem satysfakcjonujące rezultaty (nie udało mi się tylko poznać zbyt wielu wzorców projektowych, o czym pisałem ostatnio). Przede wszystkim, stworzyłem swój pierwszy projekt w ASP.NET MVC, co planowałem już od dłuższego czasu. Poza tym, zdobyłem podstawowe doświadczenie w korzystaniu z repozytorium kodu i przeprowadzaniu testów jednostkowych. Napisałem także kilka funkcji w jQuery i nauczyłem się generować dokumenty PDF. Oprócz doświaczenia praktycznego, zdobyłem sporo wiedzy teoretycznej, dotyczącej wszystkich tych zagadnień. Mam nadzieję, że przynajmniej kilku Czytelnikom przydały się informacje i linki zamieszczone na blogu.

Motywacja i efekt końcowy
Tutaj krótko: obawiałem się po cichu, że po pierwszym tygodniu trwania konkursu, moja motywacja stopnieje i praca stanie w miejscu. Na szczęście tak się nie stąło – udało mi się połączyć kodowanie i dość intensywne korzystanie z wakacji, a później, w październiku, poświęcić projektowi trochę czasu pomiędzy zajęciami na uczelni, a „życiem studenckim”. Udało mi się zrealizować wszystkie założenia projektowe, zaimplementować najważniejsze funkcjonalności i stworzyć poprawnie działającą, choć prostą, aplikację.

Posłowie
Udział w konkursie uznaję za naprawdę wartościowe doświadczenie. Jestem zadowolony z ilości zdobytej wiedzy, poznanych technologii, odwiedzonych blogów i przeczytanych artykułów. „Fizyczny” efekt tej kilkunastotygodniowej przygody znajduje się tutaj.
Na koniec chciałem podziękować wszystkim, którzy śledzili proces powstawania InvoiceInvoker. Przy okazji zachęcam do wzięcia udziału w ankiecie kończącej konkurs!

Podsumowań projektu część pierwsza: co się nie udało?

Już za cztery dni koniec konkursu Daj się poznać – przyszedł więc czas na podsumowanie tych piętnastu tygodni. Ogólnie mogę powiedzieć, że swój udział w konkursie uważam za udany. W dzisiejszym wpisie nie będzie to widoczne, ponieważ trochę ponarzekam. Aby jednak narzekanie nie było tylko pustosłowiem, postaram się wyciągnąć wnioski ze swoich niepowodzeń.

Test-driven development
Na pierwszy ogień biorę testy jednostkowe. Początkowo pisałem je systematycznie, zachowująć zasady Test-driven development. Później jednak wróciły stare przyzwyczajenia i po zaprojektowaniu klasy, przechodziłem od razu do jej implementacji; testów jednostkowych używając właściwie jedynie jako wygodnego narzędzia do uruchomienia pisanej biblioteki (co prawda, cały czas spełniały swoją rolę – wykrywały ewentualne błędy). Kiedy zabrałem się za warstwę prezentacji, możliwość zobaczenia aplikacji w akcji, przyćmiła chęć pisania testów, co uznaję za niepowodzenie. Po przeczytaniu artykułu Piotra Zielińskiego o testowaniu aplikacji ASP.NET MVC dowiedziałem się jednak, że testowanie to niewiele odbiega formą od testów, które napisałem dla pozostałej części projektu. Mogę więc powiedzieć, że zdobyłem podstawową wiedzę teoretyczną i praktyczną o TDD. Na reprymendę zasługuje za to moja samodyscyplina.
Wniosek na przyszłość: przy pracy nad przyszłymi projektami przyda mi się więcej dyscypliny i dbałości o „best practices”. Być może dobrze zrobiłby mi udział w projekcie zespołowym.

Wzorce projektowe
Krótko: wzorzec repozytorium to sporo mniej, niż chciałem przyswoić i zaimplementować. Poczytałem co prawda o popularnym wzorcu singleton, ale nie znalałem w projekcie miejsca, w którym jego implementacja byłaby naturalna i niewymuszona. Natomiast ucząc się ASP.NET MVC 2, chciałem najpierw wypracować własne stanowisko i podejście, a dopiero później skonfrontować swoje rozwiązania z tymi ogólnie przyjętymi.
Wniosek na przyszłość: powinienem poświęcić trochę czasu na lekturę związaną z wzorcami projektowymi, aby znać miejsca odpowiednie do ich zastosowania jeszcze przed etapem projektowania aplikacji.

Ostateczna forma aplikacji
W obecnej formie, InvoiceInvoker na pewno nie powala ani wyglądem, ani wygodą użytkowania. Nie jest gotowy do „użytku publicznego”, choćby ze względów bezpieczeństwa (przykładowo dlatego, że identyfikatory encji bazy danych to po prostu kolejne liczby całkowite, a nie GUIDy). Dlatego też nie zdecydowałem się na jakikolwiek hosting. Zapraszam jednakże do ściągnięcia źródeł (kiedy opublikuję ich ostateczną postać) i lokalnego uruchomienia aplikacji – wszystkie planowane funcjonalności udało mi się zaimplementować. Co do wyglądu i wygody użytkowania: cóż, przede wszystkim jest to projekt służący jedynie nauce, zdobyciu doświadczenia. Nie bez znaczenia jest też to, że pracuje nad nim tylko jedna osoba – która na dodatek nie posiada rozwiniętego zmysłu estetycznego. Co za tym idzie, arkusz stylów aplikacji niewiele różni się od domyślnego, wygenerowanego automatycznie przez Visual Studio.
Ciężko w tym przypadku o wniosek na przyszłość, ponieważ właśnie takiej postaci aplikacji się spodziewałem. Mimo wszystko, faktem jest, że postać ta nie jest w pełni satysfakcjonująca.

Blogowanie
W tej kwestii mam mieszane uczucia. Z jednej strony, treści i formie wpisów nie mam zbyt wiele do zarzucenia (może tylko tyle, że zbyt duża ich część traktowała sucho o napisanym kodzie, a za mała o teorii, czy tematach okołoprogramistycznych). Z drugiej – częstotliwość pojawiania się notek, a także ilość czasu poświęcona na napisanie każdej z nich sprawia, że nie jestem z siebie zadowolony. Początkowo zapowiadało się nieźle, publikowałem dwa lub trzy wpisy tygodniowo. Z biegiem czasu częstotliwość malała, aż w końcu się odwróciła: pisałem raz na dwa tygodnie. Ma to oczywiście związek z końcem wakacji i początkiem roku akademickiego. Tak czy inaczej, w dalszym ciągu nie wypełniłem wymagań konkursowych – ten wpis jest dopiero dziewiętnastym z dwudziestu wymaganych.
Wniosek na przyszłość: wygląda na to, że raportowanie własnej pracy nie jest dla mnie. Być może po zakończeniu konkursu, kiedy tematyka wpisów nie będzie ograniczona żadnym regulaminem, blogowanie stanie się dla mnie łatwiejsze i przyjmniejsze.

Powyższe niedociągnięcia i zaniedbania nie są na szczęście na tyle poważne, abym zaczął rozważać zmianę planów dotyczących przyszłego zawodu. Tworzą za to listę aspektów związanych z tym zawodem, nad którymi muszę jeszcze popracować.
Aby nie zostawiać Czytelnika z poczuciem, że mój udział w konkursie to nic więcej, tylko pasmo porażek, zapraszam do przeczytania kolejnego wpisu, w którym podsumuję korzyści wyniesione z tego udziału.

#ifdef TEST

Dzisiejszy wpis poświęcony będzie tematyce, którą powinienem był poruszyć już ładnych parę tygodni temu – testowaniu warstw dostępu do danych i logiki biznesowej. Zwlekałem z opisaniem tej części projektu, ponieważ planowałem zaprezentować również testy interfejsu użytkownika, których, koniec końców, wcale nie napisałem (pokusa oglądania aplikacji w akcji i sprawdzania wszystkiego własnoręcznie okazała się zbyt duża).
Przejdźmy jednak do rzeczy. Jak już kiedyś wspominałem, solucja InvoiceInvoker składa się z trzech projektów:

  • InvoiceInvoker.Logic – Data Access Layer + Business Logic Layer, z perspektywy czasu stwierdzam, że rozdzielenie tych warstw nie byłoby złym pomysłem,
  • InvoiceInvoker.Logic.Tests – testy jednostkowe pierwszego projektu,
  • InvoiceInvoker.MvcUi – projekt ASP.NET MVC 2, realizujący interakcję z użytkownikiem.

Bohaterem niniejszego wpisu będzie drugi z wymienionych projektów. Zanim przybliżę jego zawartość, opiszę framework testów jednostkowych, z którego korzystam:

NUnit
Testy w NUnit pisze się tak samo, jak zwykłe klasy. Muszą to być klasy publiczne i opatrzone atrybutem TestFixture. Idąc za ciosem, metody testujące to zwykłe metody publiczne z atrybutem Test, zwracające typ void. Przykładowy test klasy wykonującej obliczenia (nazwijmy ją Calculator) wyglądałby więc tak:

using System;
using NUnit.Framework; // przestrzeń nazw NUnit, znajdująca się w bibliotece nunit.framework

namespace SomeProject.Tests
{
	[TestFixture]
	public class CalculatorTests
	{
		[Test]
		public void Adds()
		{
			Calculator calculator = new Calculator(); // utworzenie obiektu testowanej klasy
			int a = 3;
			int b = 5;
			int actual = calculator.Add(a, b); // wynik testowanej metody
			int expected = 8; // spodziewany wynik

			Assert // NUnit.Framework.Assert - klasa, której metody zwracają wynik testu
				.AreEqual( // wynik testu będzie pozytywny, jeśli argumenty będą równe
					expected, actual); // pierwszym argumentem jest spodziewany wynik testowanej metody, drugim - wynik rzeczywisty
		}
	}
}

Gdybyśmy chcieli wykonać ten sam test dla róznych par liczb (nie tylko 3 i 5), metoda Adds mogłaby wyglądać tak:

[Test, Sequential] // Sequential - test zostanie wykonany wielokrotnie
public void Adds([Values(0, 3, -3, 3)] int a, // 0, 3, -3, 3 - kolejne argumenty wywołania testu
	[Values(0, 5, 3, -5)] int b, [Values(0, 8, 0, -2)] int expected)
{
	Calculator calculator = new Calculator();
	int actual = calculator.Add(a, b);

	Assert.AreEqual(expected, actual);
}

Łatwo zauważyć, że „zaszarzona” linia będzie się pojawiać w każdym tego typu teście (Adds, Substracts, Multiplies itp.). Dla takich przypadków istnieje atrybut SetUp:

Calculator calculator; // pole prywatne

[SetUp] // atrybut sprawia, że poniższa metoda zostanie wywołana przed każdym testem
public void TestsSetup()
{
	calculator = new Calculator(); // tę linę można już usunąć z treści metod testujących
}

Może się zdarzyć, że w testach będziemy odwoływać się do pliku konfiguracyjnego testowanego projektu. Warto wiedzieć, że NUnit domyślnie korzysta z pliku konfiguracyjnego [NazwaSolucji].config, mającego się znajdować w katalogu solucji. Odkrycie tej drobnostki zajęło mi dobrą godzinę, wypełnioną zachodzeniem w głowę, dlaczego wynik testu połączenia z bazą danych jest pozytywny, kiedy connection string umieszczony jest „na sztywno” w klasie repozytorium, a negatywny – kiedy jest pobierany z pliku konfiguracyjnego (znajdującego się katalaogu projektu, a nie solucji).

Testy warstwy dostępu do danych
Po przedstawieniu działania NUnit, mogę zaprezentować sposób testowania poszczególnych elementów projektu InvoiceInvoker.Logic. Zacznę od testów warstwy DAL, które umieściłem w przestrzeni nazw InvoiceInvoker.Logic.Tests.RepositoriesTests. Przyznam szczerze, że sposób ich wykonania budzi wiele wątpliwości. Po pierwsze, operacje Get wykonywane są zawsze na encji o identyfikatorze 1, a Update – 2:
(przykład testowania repozytorium sprzedawców – RegisteredSellerRepository)

[Test]
public void GetsById()
{
	RegisteredSeller result = repository.GetById(1); // repository - prywatne pole typu RegisteredSellerRepository

	// sprzedawca o identyfikatorze 1 ma dane takie, jak te podane jako pierwsze argumenty poniższych wywołań:
	Assert.AreEqual("BankAccountNumber", result.BankAccountNumber);
	Assert.AreEqual("BankName", result.BankName);
	Assert.AreEqual("BankSwift", result.BankSwift);
	Assert.AreEqual("City", result.City);
	Assert.AreEqual("CompanyName", result.CompanyName);
	Assert.AreEqual("FirstName", result.FirstName);
	Assert.AreEqual(1, result.Id);
	Assert.AreEqual("InvoiceNumberFormat", result.InvoiceNumberFormat);
	Assert.AreEqual("LastInvoiceNumber", result.LastInvoiceNumber);
	Assert.AreEqual("LastName", result.LastName);
	Assert.AreEqual("NIP", result.Nip);
	Assert.AreEqual("PostalCode", result.PostalCode);
	Assert.AreEqual("REGON", result.Regon);
	Assert.AreEqual("Street", result.Street);
	Assert.AreEqual("UserName", result.UserName);
}

Jeśli dane sprzedawcy o identyfikatorze 1 ulegną zmianie (musiałbym je zmienić ręcznie, jednak istnieje taka możliwość), to test będzie miał wynik negatywny, nawet jeśli metoda GetsById zadziała poprawnie. Test Updates jest wolny od tej wady, ponieważ pobiera encję z bazy, zmienia jej dane, wywołuje metodę Update, a następnie pobiera ją znowu i sprawdza, czy dane się zmieniły. Jednak pobranie encji wiąże się z wywołaniem metody GetById, która nie jest przedmiotem testu – taki test trudno nazwać jednostkowym. Wątpliwości innej natury budzi test dodawania encji – bada tylko, czy liczba wierszy tabeli po wywołaniu metody Add zwiększa się o jeden. Test usuwania łączy dwie ostatnie wady: polega na dodaniu encji (a więc zakłada, że ta operacja działa), usunięciu jej, i sprawdzeniu, czy ilość encji w tabeli nie uległa zmianie.
Czy za napisanie takich testów zasługuję na wieczne potępienie? Jeśli tak, to proszę o niezwłoczne powiadomienie mnie o tym w komentarzu – udam się na wieczną tułaczkę. Na swoją obronę mam tylko tyle, że testy te rzeczywiście pomogły mi wykryć kilka błędów.

Testy generowania dokumentów PDF
Tutaj krótko: nie miałem pojęcia, jak za pomocą kodu sprawdzić, czy wygenerowany plik PDF wygląda jak faktura VAT. Testy polegają więc na wygenerowaniu faktury o określonej właściwości (na przykład zawierającej bardzo długie nazwy produktów) i wyświetleniu jej. Wyniki takich testów zamieściłem na końcu tego wpisu.

Testy logiki biznesowej
Nareszcie testy wolne od kontrowersji. Dotyczą one prawie wszystkich klas opisywanych we wpisach otagowanych jako business logic layer. Dla przykładu, testy klasy zamieniającej liczby na ich reprezentację słowną:

[Test]
public void ConvertsNumbersOf4Digits()
{
	string result = NumberToWordsConverter.Convert(1234);
	Assert.AreEqual("jeden tysiąc dwieście trzydzieści cztery", result);
}

[Test]
public void ConvertsNumbersOf5Digits()
{
	string result = NumberToWordsConverter.Convert(12345);
	Assert.AreEqual("dwanaście tysięcy trzysta czterdzieści pięć", result);
}

// (...)

[Test]
public void ConvertsMoney()
{
	string result = NumberToWordsConverter.ConvertMoney(12.93M, "PLN");
	Assert.AreEqual("dwanaście PLN 93/100", result);

To, co nie budzi kontrowersji, nie budzi też zainteresowania (nie jest to przypadkiem jakieś prawo show-biznesu?) – dlatego pominę szczegółowy opis, dociekliwych odsyłając do kodu źródłowego.

I to by było na tyle. Ten (najdłuższy chyba ze wszystkich) wpis kończy serię wpisów „technicznych”. Zakończenie konkursu zbliża się wielkimi krokami, dlatego pozostałe dwie (bo tyle brakuje do wymaganych dwudziestu) notki poświęcę na podsumowanie tej kilkunastotygodniowej przygody. Zapraszam!

Historia faktur

Dzisiaj przedstawię niektóre elementy strony historii faktur. Słowo „niektóre” pojawia się dlatego, że strona ta czerpie garściami z innych – zaimplementowanych już i opisanych. Od razu pokażę jej wygląd, aby Czytelnik, czytając o kolejnych elementach, znał już ich umiejscowienie i przeznaczenie:

Filtrowanie
Filtrowanie danych omówiłem dość szczegółowo w tym wpisie. Wyjaśnię więc tylko, że faktury można filtrować po statusie, kliencie, produktach, dacie wystawienia i wartości. Na wyjaśnienie zasługuje filtrowanie produktów. Polega ono na tym, że użytkownik podaje początki nazw produktów, które powinna zawierać faktura, oddzielając je przecinkami. Oprócz podanych przez użytkownika, faktura może zawierać też inne produkty (czego nie widać na pokazanym screenie).

Status faktury
Możliwe statusy faktury to wystawiona i zapłacona. Pierwszy jest nadawany automatycznie po utworzeniu, drugi ustawia użytkownik (za pomocą przycisku zapłacona widocznego przy fakturze). Przy implementowaniu tej funkcjonalności, napotkałem problem natury językowej. Chciałem, aby w kodzie programu i bazie danych statusy miały nazwy angielskie (created, paid). Użytkownik powinien jednak widzieć je po polsku, dlatego napisałem dwie proste metody:

private static string TranslateInvoiceStatus(string status)
{
	if (status == Invoice.InvoiceStatus.Created.ToString())
		return "Wystawiona";
	else if (status == Invoice.InvoiceStatus.Paid.ToString())
		return "Zapłacona";
	else
		return "";
}

public static List<KeyValuePair<string, string>> GetInvoiceStatusList(this HtmlHelper helper) // ta metoda jest rozszerzeniem klasy HtmlHelper
{
	List<KeyValuePair<string, string>> result = new List<KeyValuePair<string, string>>();
	
	result.Add(new KeyValuePair<string, string>(TranslateInvoiceStatus(Invoice.InvoiceStatus.Created.ToString()), Invoice.InvoiceStatus.Created.ToString()));
	result.Add(new KeyValuePair<string, string>(TranslateInvoiceStatus(Invoice.InvoiceStatus.Paid.ToString()), Invoice.InvoiceStatus.Paid.ToString()));

	return result;
}

Przeznaczenie pierwszej metody jest uniwersalne, drugiej natomiast używam do wypełnienia rozwijanej listy statusów w filtrze faktur.

Termin płatności
Tutaj jedynie krótkie wyjaśnienie: jeśli faktura ma status inny niż zapłacona i jej termin płatności minął, jest on wyświetlany na czerwono – co widać na screenie.

PDF
Czas na najważniejszą funkcję całej aplikacji – generowanie pliku PDF. Po naciśnięciu przycisku PDF, widocznego przy każdej fakturze, użytownik wybiera, czy chce wygenerować oryginał dokumentu, czy jego kopię:

Po dokonaniu wyboru, wywoływana jest następująca metoda:

public ActionResult ToPdf(int id, PdfCreator.InvoiceType invoiceType)
{
	Invoice invoice = _invoiceRepository.GetById(id); // _invoiceRepository - prywatne repozytorium faktur
	string path = Server.MapPath("~/bin/lastInvoice.pdf"); // po stronie serwera, faktura zapisywana jest do pliku lastInvoice.pdf (być może nie jest to ostateczne rozwiązanie)
	string fileName = invoice.CreationDate.Value.ToShortDateString() + "_" + invoice.Customer.CompanyName + ".pdf"; // nazwa pliku otrzymywanego przez użytkownika jest postaci dataWystawienia_nazwaFirmyKlienckiej.pdf

	PdfCreator pdfCreator = new PdfCreator(invoice); // klasa PdfCreator tworzy dokument PDF na podstawie encji faktury z bazy danych
	pdfCreator.Create(invoiceType);
	pdfCreator.SaveDocument(path, false);

	return File(path, "PDF|*.pdf", fileName); // zwrócenie pliku PDF
}

Etykiety
Na koniec funkcjonalność niewidoczna na pokazanym na początku wpisu screenie – etykiety, pojawiające się po wskazaniu kursorem niektórych komórek tabeli zawierającej listę faktur:

  • po wskazaniu daty wystawienia faktury, wyświetlana jest etykieta zawierająca także datę sprzedaży,
  • do każdego produktu przypisana jest etykieta o treści: [ilość] [j.m.] ([wartość brutto] [waluta]), na przykład: 10 kg (100 PLN),
  • etykieta pola Do zapłaty zawiera również wartości pól Zapłacono i Pozostało do zapłaty.

Wygląda na to, że opisałem już wszystkie najważniejsze podstrony aplikacji. Prawdopodobnie zrezygnuję z tworzenia strony głównej, której rolę być może przejmie przedstawiona właśnie Historia faktur. Od teraz praca nad projektem będzie opierać się na testowaniu i szlifowaniu drobnych modułów programu, uzupełnię także informacje na stronie About. Jak widać, piętnaście tygodni to optymalna ilość czasu na stworzenie podobnego projektu i zdobycie sporej ilości wiedzy i doświadczenia.

Ostatnie okrążenie

Nadszedł wreszcie czas prezentacji finalnego modułu aplikacji. Jest nim model, kontrolery i widoki odpowiedzialne za wystawianie faktur VAT. Wszystkie te elementy są niejako rozszerzeniem ich odpowiedników obsługujących szablony faktur. Rozszerzenie polega głównie na dodaniu pól dat: wystawienia faktury, sprzedaży, terminu płatności; i tabeli zawierającej łączną wartość netto i brutto faktury, a także łączne wartości produktów o poszczególnych stawkach VAT. Skupię się tylko na tych zagadnieniach, jako że reszta aspektów wystawiania faktur jest analogiczna do tych dotyczących tworzenia ich szablonów. Po pełny kod odsyłam natomiast pod ten adres.

Daty na fakturze
Postawowym postanowieniem co do dat na stronie tworzenia faktury było wykorzystanie kontrolki typu datepicker. Przystępną jej implementację znalazłem w jQuery UI. Przyjemnym „ficzerem” tego pakietu jest możliwość zdefiniowania wyglądu kontrolek przed ściągnięciem paczki zawierającej arkusz css (z zestawem grafik) i bibliotekę jQuery. Nie obyło się jednak bez drobnej ingerencji w pobrane pliki – chciałem, aby nazwy dni i miesięcy wyświetlane były po polsku. Wymagało to jedynie podmiany odpowiednich literałów, przykładowo kod:

dayNamesMin:["Su","Mo","Tu","We","Th","Fr","Sa"]

zamieniłem na:

dayNamesMin:["Nd","Pn","Wt","Sr","Cz","Pt","Sb"]

W całej aplikacji używam formatu daty rok-miesiąc-dzień, dlatego kolejną (i ostanią) zmianą bazowego kodu kontrolki było ustawienie właściwości dateFormat na yy-mm-dd. Użycie datepicker wydaje się być, po krótkim obyciu z jQuery, oczywiste i intuicyjne:

$("#creationDate").datepicker(); // dodanie datepicker do pola daty wystawienia fakrury,
$("#saleDate").datepicker(); // ...daty sprzedaży...
$("#paymentDeadline").datepicker(); // ...i terminu płatności

Efekt końcowy jest w pełni zadowalający – wygodny w użyciu i miły oku:

Tabela wartości produktów według stawek VAT
Opisując ten element, najpierw pokażę, o co właściwie chodzi. Tabela produktów jest analogiczna do tej na stronie szablonów faktur (przdstawionej tutaj) – zawiera tylko więcej informacji o każdym produkcie. Pod nią (a właściwie jako jej kontynuacja) powinna znaleźć się tabela, której działanie zamierzam pokazać:

Obrazek w pełni wyjaśnia jej przeznaczenie. Za uaktualnianie tabeli odpowiada funkcja (javascript) updateGeneralValues, wywołująca za pomocą AJAX metodę (C#) GetGeneralValues, obliczającą łączne wartości produktów o kolejnych stawkach VAT i zwracającą następujący obiekt JSON:

// Celowo nie pokazuję całej treści metody - jest dość długa, a sposób jej działania nie jest właściwie istotny.
// Dociekliwych odsyłam do linka kończącego pierwszy akapit wpisu (opisywana metoda znajduje się w klasie InvoiceEditorController).

var data = new
{
	ToPay = toPay.ToString("F"), // toPay (decimal) - łączna wartość brutto wszystkich produktów
	VatRates = vatRates.ToArray(), // vatRates (List<string>) - stawki VAT występujące na fakturze
	NetValues = netValues, // netValues (string[]) - łączne wartości netto produktów o kolejnych stawkach VAT
	VatValues = vatValues, // vatValues (string[]) - łączne wartości tara produktów o kolejnych stawkach VAT
	GrossValues = grossValues // grossValues (string[]) - łączne wartości brutto produktów o kolejnych stawkach VAT
};

Bardziej szczegółowo zaprezentuję funkcję zajmującą się wyświetlaniem opisywanej tabeli: handleGeneralValuesUpdate. Wstawia ona wiersze tabeli w przygotowanie na to miejsce:

<tr> <!-- pierwszy wiersz tabeli jest zakodowany "na sztywno", znajduje się w nim również pole tekstowe służące do dodawania produktów -->
	<td>
		<input type="text" id="newProduct-name" style="width: 130px" /> <!-- rzeczone pole tekstowe -->
	</td>
	<td class="transparentRB"></td> <!-- transparentRB - prawa i dolna krawędź komórki jest niewidoczna -->
	<td class="transparentLB"></td> <!-- transparentLB - lewa i dolna krawędź komórki jest niewidoczna -->
	<td class="transparentLB"></td>
	<th class="separated" style="text-align: center">RAZEM</th> <!-- separated - górna krawędź komórki jest grubsza -->
	<td class="separated" align="right" id="generalNetValue">0,00</td> <!-- komórka przechowująca łączną wartość netto wszystkich produktów -->
	<td class="separated" align="center">X</td>
	<td class="separated" align="right" id="generalVatValue">0,00</td> <!-- komórka przechowująca łączną wartość tara wszystkich produktów -->
	<td class="separated" align="right" id="generalGrossValue">0,00</td> <!-- komórka przechowująca łączną wartość brutto wszystkich produktów -->
	<td class="transparentTRB"></td> <!-- transparentTRB - górna, prawa i dolna krawędź komórki jest niewidoczna -->
</tr>

<tr></tr> <!-- gwarancja, że dolne krawędzie poprzedniego wiersza pozostaną  niewidoczne po dodaniu kolejnych wierszy -->

<tr id="generalValues"></tr> <!-- w tym miejscu pojawią się kolejne wiersze -->

Jej treść wygląda zatem tak:

function handleGeneralValuesUpdate(data) { <!-- funkcja przyjmuje pokazany wcześniej obiekt JSON -->
	<!-- wypełnienie pierwszego wiersza tabeli (pierwsze elementy list przyjmowanego obiektu JSON zawierają łączne wartości dla wszystkich stawek VAT): -->
	$("#generalNetValue").text(data.NetValues[0]);
	$("#generalVatValue").text(data.VatValues[0]);
	$("#generalGrossValue").text(data.GrossValues[0]);

	$("tr[id*=vatRate-]").each(function () { $(this).remove() }); <!-- usunięcie pozostałych wierszy (patrz linia 12) -->

	var rowsCode = ""; <!-- kod nowych wierszy tabeli -->

	for (var i = 1; i != data.VatRates.length; i++) { <!-- nowych wierszy jest tyle, ile stawek VAT występuje w tabeli produktów -->
		rowsCode += '<tr id="vatRate-' + data.VatRates[i] + '">' + <!-- nowy wiersz dostaje odpowiedni identyfikator -->
			'<td class="transparentLTB"></td>' + <!-- transparentLTB - lewa, górna i dolna krawędź komórki jest niewidoczna -->
			'<td class="transparentLTB"></td>' +
			'<td class="transparentLTB"></td>' +
			'<td class="transparentLTB"></td>';

		if (i == 1) rowsCode += '<th style="text-align: center">W tym</th>'; <!-- pierwszy z nowych wierszy będzie zawierał napis "W tym" -->
		else rowsCode += '<th></th>';
		
		rowsCode += '<td align="right">' + data.NetValues[i] + '</td>' + <!-- komórka zawierająca łączną wartość netto produktów -->
			'<td align="right">' + data.VatRates[i] + '</td>' + <!-- komórka zawierająca kolejną stawkę VAT -->
			'<td align="right">' + data.VatValues[i] + '</td>' + <!-- komórka zawierająca łączną wartość tara produktów -->
			'<td align="right">' + data.GrossValues[i] + '</td>' + <!-- komórka zawierająca łączną wartość brutto produktów -->
			'<td class="transparentTRB"></td>' + <!-- transparentTRB - górna, prawa i dolna krawędź komórki jest niewidoczna -->
			'</tr>';
	}

	rowsCode += '<tr id="generalValues"></tr>'; <!-- ten wiersz zostanie wykorzystany przy następnej aktualizacji tabeli -->

	$("#generalValues").replaceWith(rowsCode); <!-- wstawienie rowsCode w przygotowane miejsce -->

	<!-- uaktualnienie innych elementów strony, tutaj nieistotne -->
}

Ostatni komentarz w powyższym kodzie sygnalizuje, że nie zaprezentowałem pełnego ciała funkcji. Oprócz opisywanej tabeli, aktualizuje ona także pola Razem do zapłaty, Słownie do zapłaty i Pozostało do zapłaty – nie jest to jednak nic skomplikowanego.

Bez wątpienia, strona tworzenia faktur wymagała najwięcej pracy. Na szczęście zawiera wiele elementów analogicznych do tych ze strony tworzenia szablonów faktur (np. tabela produktów), która z kolei zawiera elementy analogiczne do tych ze strony tworzenia klientów, czy produktów (np. listy rozwijane). Przy tworzeniu żadnej ze stron nie rzuciłem się więc od razu na głęboką wodę – mogłem stopniowo poznawać nowe kontrolki i mechanizmy. Tym sposobem, bez większych problemów ukończyłem najtrudniejszą część projektu. Teraz pozostało tylko wykorzystać zdobytą wiedzę i stworzyć brakujące strony: historię faktur i stronę główną aplikacji. Postaram się pokonywanie tej ostatniej prostej opisywać z większą częstotliwością… W końcu do zakończenia konkursu już tylko cztery tygodnie!

InvoiceInvoker a świat wielkiej polityki i ekonomii

Tytuł wpisu zwiastuje treść i tematykę poważną i podniosłą, jednak bez obaw – poruszana kwestia nie będzie ściśle polityczna czy ekonomiczna, ani daleko odbiegająca od problemów, z którymi zdarza się borykać programistom. Chodzi mianowicie o dostosowanie aplikacji do obowiązujących przepisów, w tym przypadku – stawek procentowych podatku VAT.
Kilka dni po mojej decyzji o starcie w konkursie Daj się poznać z projektem InvoiceInvoker, zapowiedziano zmianę wspomnianych stawek. Był to chyba pierwszy przypadek wywarcia przez urzędujące władze bezpośredniego wpływu na moje decyzje. Ściślej – decyzje projektowe. Dziś, kiedy już prace nad projektem są właściwie na ukończeniu, zaprezentuję tych decyzji konsekwencje – które, co oczywiste dla projektu programistycznego, przybrały postać linii kodu. Przyznaję jednocześnie, że tematyka tego wpisu jest podyktowana moją niechęcią do ciągłego pisania o co raz to nowych widokach i kontrolerach (choć opisanie finalnych widoków i kontrolerów, tych dotyczących wystawiania faktur, wydaje się być nieuniknione).
Przejdźmy jednak do rzeczy. Podstawową decyzją było jak największe uniezależnienie kodu programu od obowiązujących stawek VAT. Początkowo, wprowadzenie tych stawek chciałem zrzucić na użytkownika i umożliwić łatwą ich edycję (na wypadek, gdyby w przyszłości znowu zostały zmienione). Takie rozwiązanie wymagałoby jednak dodania kolejnej kolumny do tabeli sprzedawców w bazie danych, dlatego ostatecznie postanowiłem utworzyć pole VatRates w pliku konfiguracyjnym aplikacji, odpowiedzialne za przechowywanie rzeczonych stawek VAT:

<configuration>
	<appSettings>
		<add key="VatRates" value="22%, 7 %, 3 %, 0 %, n.p., z.w." />
		<add key="PaymentTypes" value="Przelew, Gotówka, Karta płatnicza" /> <!-- swoją drogą, podobnie postąpiłem w kwestii sposobów płatności -->
	</appSettings>

Dostanie się do tego pola i zwrócenie listy stawek nie jest zatem skomplikowane:

public static List<string> GetVatRates()
{
	string vatRates = ConfigurationManager.AppSettings["VatRates"];
	string[] separator = { ", ", "," };
	return vatRates.Split(separator, StringSplitOptions.RemoveEmptyEntries).ToList();
}

Wszystko to jednak tylko ciągi znaków, a przecież głównym ich przeznaczeniem jest reprezentowanie liczb, przez które nalezy pomnożyć wartość netto, by uzyskać wartość brutto produktu. Wyłuskiwaniem tychże liczb zajmuje się następująca metoda:

public static int GetVatPercentage(string vatRate)
{
	int result = 0;
	string vatPercentage = string.Empty;

	if (vatRate.EndsWith(" %")) // ciąg znaków reprezentujący stawkę procentową VAT może kończyć się tylko znakami " %" lub "%"
		vatPercentage = vatRate.Substring(0, vatRate.Length - 2);
	else if (vatRate.EndsWith("%"))
		vatPercentage = vatRate.Substring(0, vatRate.Length - 1);

	// jeśli stawka VAT nie zawiera procentów (jak np. "z.w." - produkt zwolniony od podatku), to jej wartość wynosi 0

	int.TryParse(vatPercentage, out result);

	return result;
}

Wyliczenie wartości podatku VAT odprowadzonego od produktu i warości brutto tego produtku, przy znanej warości netto, wygląda więc tak:

// przedstawione wcześniej metody znajdują się w statycznej klasie VatRatesProvider
int vatPercentage = VatRatesProvider.GetVatPercentage(product.VatRate); //właściwość VatRate jest ciągiem znaków zawierającym stawkę VAT produktu
product.VatValue = product.NetValue * (vatPercentage * 0.01M); // NetValue - wartość netto produktu, VatValue - warość podatku
product.GrossValue = product.NetValue + product.VatValue; // GrossValue - wartość brutto produktu

Metody GetVatRates natomiast używam do przekazania stawek VAT widokom tworzenia i edycji produktów.

I to wszystko w tym temacie, przedstawione rozwiązanie nie jest skomplikowane, ale chyba warte opisania.
Na koniec małe usprawiedliwienie. Wraz z nadejściem (a niedawno także odejściem) ostatnich dni wakacji, częstotliwość pojawiania się nowych notek zauważalnie spadła, czego powodem, jak łatwo się domyślić, jest natłok wszelakich zajęć różnych od kodowania. Projektowi nie grozi jednak, jeśli dobrze rozumiem regulamin i FAQ konkursu, dyskwalifikacja: jeśli dobrze rozumiem regulamin i FAQ konkursu, bardziej niż częstotliwość wpisów, liczy się ich ilość – która do piętnastego listopada powinna liczyć co najmniej dzwadzieścia.

Starcia runda druga: produkty na szablonie faktury

Ostatnio opisałem rozwiązanie pierwszej części mojego problemu. Po tygodniowej przerwie, wypełnionej korzystaniem z ostatnich dni wakacji, wracam na ring i ostatecznie nokautuję przeciwnika na trzech płaszczyznach.

1. HTML
Pierwszą z nich jest struktura tebeli produktów (szczegóły w punkcie drugim listy umieszczonej w akapicie O co chodziło? poprzedniego wpisu). Składają się na nią następujące wiersze:
– nagłówek tabeli:

<table>
	<tr>
		<th>Nazwa</th>
		<th>PKWiU</th>
		<th title="Jednostka miary">J. miary</th>
		<th>Cena netto</th>
		<th>Stawka VAT</th>
		<th style="min-width: 52px"></th>
	</tr>

– produkty juz zawarte w szablonie (istotne przy edycji już istniejącego szablonu):

<% foreach (var item in Model.Products) { %> <!-- Model - szablon faktury, Products - produkty zawarte w szablonie -->
<tr id="product-<%: item.Id %>"> <!-- każdy wiersz ma odpowiedni identyfikator -->
	<td><%: item.Name %></td>
	<td align="center"><%: item.Pkwiu %></td>
	<td><%: item.MeasureUnit %></td>
	<td align="right"><%: item.NetPrice.ToString("N") %></td>
	<td align="right"><%: item.VatRate %></td>
	<td>
		<input type="button" value="Usuń" onclick="removeProduct(<%: item.Id %>)" /> <!-- przycisk usuwania produktu, funkcję removeProduct pokażę w akapicie dotyczącym jQuery -->
	</td>
</tr>
<% } %>

– pusty wiersz, w miejsce którego wstawiane będą dodawane przez użytkownika produkty:

<tr id="addedProduct"></tr>

– wiersz umożliwiający dodanie produktu do szablonu:

	<tr id="newProduct">
		<td>
			<input type="text" id="newProduct-name" style="width: 130px" /> <!-- pole tekstowe umożliwiające dodanie produktu -->
		</td>
		<td class="transparentRB"></td> <!-- komórka o niewiczonej prawej i dolnej krawędzi -->
		<td class="transparentLRB"></td> <!-- komórka o niewiczonej lewej, prawej i dolnej krawędzi -->
		<td class="transparentLRB"></td>
		<td class="transparentLRB"></td>
		<td class="transparentLRB"></td>
	</tr>
</table>

Całość, dla szablonu z już dodanymi produktami, prezentuje się tak:

2. C#
Skoro uzyskałem wygląd tabeli, mogę zabrać się za napisanie kilku metod C#, z których korzystać będą (poprzez AJAX) funckje jQuery. Interakcja z użytkownikiem wygląda tak:
1. Użytkownik wpisuje początek nazwy produktu, który zamierza dodać do szablonu,
2. Dzięki Autocomplete i metodzie FilterProducts, wyświetlana jest lista podpowiedzi,
3. Użytkownik wybiera produkt z listy,
4. Wywoływana jest metoda AddProduct,
5. Użytkownik, jeśli chce, usuwa produkt (przycisk Usuń),
6. Po wciśnięciu przycisku Usuń wywoływana jest metoda RemoveProduct.

Pominąłem tutaj rolę jQuery – opiszę ją w kolejnym paragrafie. Wspomnę na razie tylko o wielce wygodnym pluginie, jakim jest Autocomplete (używam jego zmodyfikowanej wersji). Odpowiada on dokładnie za to, czego potrzebuję: wyświetla podpowiedzi pod polem tekstowym. W tym miejscu jedyną istotną informacją dotyczącą Autocomplete jest to, że, komunikując się z kontrolerem, przekazuje mu treść pola tekstowego (w tym przypadku początek nazwy produktu) w parametrze o nazwie „q”, a przyjmuje ciąg znaków zawierający kolejne pozycje listy podpowiedzi (oddzielone znakiem ‚\n’). Metoda filtrowania produktów, wymieniona w punkcie drugim przedstawionej wyżej listy, prezentuje się więc tak:

public ActionResult FilterProducts(string q)
{
	List<RegisteredProduct> products = _productRepository.GetByName(q);
	string[] contents = new string[products.Count];

	for (int i = 0; i != products.Count; i++)
		contents[i] = products[i].Name + "|" + products[i].Id.ToString(); // znak '|' oddziela treść pola listy od dodatkowych, przypisanych do niego, wartości

	return Content(string.Join("\n", contents));
}

Listę produktów zawartych w szablonie, a właściwie ich identyfikatorów, przechowuję w danych sesji (Session[„ProductIds”]). Dodawanie i usuwanie produktów opiera się zatem głównie na operacjach na tej liście. Metoda dodawania zwraca ponadto dane dodawanego produktu:

public ActionResult AddProduct(int id)
{
	List<int> productIds = Session["ProductIds"] as List<int>; // pobranie wspomnianej listy identyfikatorów

	if (id == 0 || productIds.Contains(id))
		return Json(null); // wielokrotne dodawanie tego samego produktu nie jest dozwolone

	productIds.Add(id);

	RegisteredProduct product = _productRepository.GetById(id); // _productRepository - prywatne repozytorium produktów

	var data = new // dane dodawanego produktu
	{
		Id = product.Id,
		Name = product.Name,
		Pkwiu = product.Pkwiu,
		MeasureUnit = product.MeasureUnit,
		NetPrice = product.NetPrice.Value.ToString("N"),
		VatRate = product.VatRate
	};

	return Json(data);
}

[HttpPost]
public ActionResult RemoveProduct(int id)
{
	List<int> productIds = Session["ProductIds"] as List<int>;
	productIds.Remove(id);

	return null;
}

Teraz pozostaje tylko spiąć sztywny kod HTML i nie-tak-łatwo-dostępne-z-poziomu-html metody C# za pomocą:

3. jQuery
Na tej płaszczyźnie nie pojawi się chyba nic, czego nie opisałem w poprzednim wpisie (no dobrze, pojawi się jedno słowo: autocomplete). Aby jednak zwieńczyć dzieło, pokażę poszczególne funkcje. Na początek kilka linijek ułatwiających dostęp do metod kontrolera:

var url_FilterProducts = '<%: Url.Action("FilterProducts", "PatternEditor") %>';
var url_AddProduct = '<%: Url.Action("AddProduct", "PatternEditor") %>';
var url_RemoveProduct = '<%: Url.Action("RemoveProduct", "PatternEditor") %>';

Wspomniany już kilkukrotnie plugin Autocomplete zaimplementowałem w ten sposób:

$("#newProduct-name") // selekcja pola tekstowego nazwy dodawanego produktu
	.autocomplete(
		url_FilterProducts, // ścieżka do metody zwracającej listę podpowiedzi
		{
			delay: 50, // lista podpowiedzi pojawi się 50ms po wpisaniu znaku w polu tekstowym
			onItemSelect: function (li) { // funkcja wywoływana po wybraniu podpowiedzi, pobiera pole listy (li)
				$.post(url_AddProduct, { "id": li.extra[0] }, handleProductAddition); // użycie AJAX do wywołania metody AddProduct kontrolera, następnie wywołanie funkcji handleProductAddition
				}
		}
	);

Występująca w powyższym kodzie funkcja handleProductAddition odpowiada za dodanie produktu do tabeli – wstawia nowy wiersz w przygotowane na to miejsce (patrz addedProduct w paragrafie HTML). Jej treść wygląda tak:

function handleProductAddition(data) { // funkcja pobiera dane produktu
	if (data) { // patrz szósta linia metody AddProduct (paragraf "C#")
		$("#addedProduct").replaceWith( // wstawienie danych produktu w miejsce pustego wiersza
		'<tr id="product-' + data.Id + '">' + // wiersz otrzymuje swój identyfikator
			'<td>' + data.Name + '</td>' +
			'<td align="center">' + data.Pkwiu + '</td>' +
			'<td>' + data.MeasureUnit + '</td>' +
			'<td align="right">' + data.NetPrice + '</td>' +
			'<td align="right">' + data.VatRate + '</td>' +
			'<td>' +
				'<input type="button" value="Usuń" onclick="removeProduct(' + data.Id + ')"/>' +
				// kliknięcie przycisku wywołuje funkcję removeProduct, jako parametr podając identyfikator produktu, a tym samym wiersza tabeli
			'</td>' +
		'</tr>' +
		'<tr id="addedProduct"></tr>' // pusty wiersz zostaje, przyda się do dodania następnego produktu
		);
	}
	else { // wielokrotne dodawanie tego samego produktu nie jest dozwolone
		showProductError("Ten produkt został już dodany"); // funkcja showProductError wyświetla treść błędu pod tabelą produktów
	}

	$("#newProduct-name").val(""); // wyczyszczenie treści pola tekstowego
	$("#newProduct-name").focus(); // focus pozostaje na polu tekstowym
}

Usuwanie produktu odbywa się o wiele prościej. Po kliknięciu przez użytkownika przycisku Usuń, wywoływana jest następująca funkcja:

function removeProduct(id) { // funkcja pobiera identyfikator usuwanego produktu
	$.post(url_RemoveProduct, { "id": id }); // wywołanie metody RemoveProduct kontrolera (identyfikator produktu przekazywany jest przez parametr o nazwie "id")
	$("tr[id=product-" + id + "]").remove(); // selekcja i usunięcie wiersza zawierającego dane produktu
}

KO!
Uff, to wszystko. Działanie kodu w praktyce można sprawdzić po ściągnięciu stąd. Pojedynek nie był może tak widowiskowy jak walki MMA, jednak liczy się to, że przeciwnik leży na deskach. Mogę teraz przejść nad nim, by zmierzyć się z finalnym bossem – wystawianiem faktur.