Publiczna ankieta „Daj się poznać”

Dzisiaj wpis krótki, o charakterze informacyjnym.
Przez ostatni tydzień trwało zamknięte głosowanie w konkursie Daj się poznać. W głosowaniu tym wyłonionych zostało siedemnastu uczestników, którzy przez najbliższe (co najmniej) 7 dni walczyć będą o pierwszeństwo wyboru nagród rzeczowych. Walka ta będzie bezkontaktowa, tzn. prowadzona nie przez samych zainteresowanych, a przez czytelników ich blogów – czyli, miedzy innymi, Ciebie.

Garść linków:
zasady głosowania,
„karta wyborcza”,
posty na niniejszym blogu związane z konkursem.

Gorąco zachęcam do wzięcia udziału w ankiecie!

PS. Deklaracje poczynione w pierwszym akapicie tego wpisu oczywiście pozostają w mocy.

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!