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!

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.

Wieczorek zapoznawczy z jQuery, czyli get the $(„# in!

Po kilkudniowej nieobecności wśród cywilizacji, wracam do pracy. Dziś przedstawię rozwiązanie przedstawionego ostatnio problemu.

O co chodziło?
Chodziło o szablony faktur. Dodaję tę funkcjonalność po to, aby użytkownik, jeśli zdarza mu się wystawiać co miesiąc identyczne faktury, nie musiał tworzyć każdej z nich od zera. Szablon przechowuje następujące informacje:
– odbiorca faktury (klient),
– produkty, na które zostanie wystawiona faktura,
– sposób płatnośći (przelew, gotówka itp.),
– waluta płatnośći,
– ewentualne uwagi.
Tworząc widok edycji szablonów, napotkałem na przeszkodę. Chciałem w nim umieścić dwa elementy wymagające wymiany danych między widokiem i jego kontrolerem, bez konieczności odświeżania całej strony:

  1. rozwijana lista klientów – po wyborze klienta, jego dane są wyświetlane w tabeli,
  2. lista produktów – tabela zawierająca informacje o wybranych produktach (z możliwością ich usuwania) i umożliwiająca dodanie produktów do listy (pole tekstowe z zaimplementowanym autouzupełnianiem).

Moja wiedza nie była wystarczająca, by podołać takiemu wyzwaniu, dlatego musiałem ją poszerzyć.

Write less, do more!
Nagłówek tego akapitu to motto biblioteki JavaScript, z którą postanowiłem się zapoznać: jQuery (podlinkowana strona jest godna gorącego polecenia, zawiera absolutnie wszystko, czego potrzeba do nauki). Nie miałem nigdy do czynienia z JavaScript, dlatego początkowo trudno było mi rozczytywać przykłady z dokumentacji (zwłaszcza, że niejedna linijka kodu w nich zawartego zaczyna się od zlepka $(„#, widniejącego w tytule wpisu). Opanowanie podstaw nie okazało się jednak trudne, po kilku godzinach przeszedłem więc do zagadnienia, które interesowało mnie najbardziej, czyli sposobu wykorzystania w jQuery AJAX. I tu nie napotkałem większych trudności, dlatego zabrałem się za ostatni (po znalezieniu rozwiązania i zdobycia odpowiedniej wiedzy) etap walki z moim problemem: kodowanie.

Ostatnie starcie: runda pierwsza
Metody zwracające obiekty JSON postanowiłem oddzielić od tych (umieszczonych w kontrolerze PatternsController) zwracających konkretne widoki, dlatego stworzyłem nowy kontroler: PatternEditorController. Następnie zacząłem komponować opisany wyżej widok edycji szablonów faktur. Na pierwszy ogień poszła realizacja pierwszej z wypunktowanych funkcjonalności: lista klientów i tabela danych wybranego klienta. Garść linii kodu HTML zdefiniowało wygląd tworzonego elementu:

(pochyloną czcionką wpisałem identyfikatory odpowiednich komórek tabeli)
Kilkanaście linii JavaScript, przy pomocy jQuery, narzuciło zachowanie listy customer-name:

var url_GetCustomer = '<%: Url.Action("GetCustomer", "PatternEditor") %>'; // URL do odpowiedniej akcji zwracającej obiekt JSON

$("#customer-name") // selekcja elementu o identyfikatorze customer-name
	.change( // zdarzenie zgłaszane po wyborze którejś z pozycji listy
		function () { // funckja anonimowa wywoływana po zgłoszeniu zdarzenia
			$.post(url_GetCustomer, { "id": this.value }, handleCustomerUpdate); // wykorzystanie AJAX (http://api.jquery.com/jQuery.get/)
		}
	);

function handleCustomerUpdate(customer) {
	// wypełnienie zawartości odpowiednich komórek tabeli
	$("#customer-street").text(customer.Street);
	$("#customer-city").text(customer.City);
	$("#customer-postalCode").text(customer.PostalCode);
	$("#customer-nip").text(customer.Nip);
}

Natomiast pojedyncza metoda C# przejęła odpowiedzialność za pobranie danych klienta i przekazanie ich do widoku:

[HttpPost]
public ActionResult GetCustomer(int id)
{
	RegisteredCustomer customer = _customerRepository.GetById(id); // _customerRepository - prywatne repozytorium klientów

	var data = new
	{
		CustomerName = customer.CustomerName,
		CompanyName = customer.CompanyName,
		Street = customer.Street,
		City = customer.City,
		PostalCode = customer.PostalCode,
		Nip = customer.Nip
	};

	return Json(data);
}

Efekt jest widoczny na załączonym obrazku:

Póki co, ja 1 : 0 MVC 2.

Jak widać, osiągnięcie żądanego efektu okazało się nie być wcale trudne i skomplikowane. Wystarczyło kilka(naście?) godzin, abym mógł dopisać jQuery do dość długiej już listy technologii (choć może w tym przypadku to zbyt duże słowo) poznanych podczas pracy nad projektem. W kolejnym wpisie zrelacjonuję drugą, nieco bardziej skomplikowaną, rundę tego starcia – realizację tabeli produktów. Zapraszam!


EDIT: Ostatnie wakacyjne wyjazdy pochłonęły sporo mojego wolnego czasu, stąd w tym i poprzednim tygodniu tylko po jednym wpisie. Zgodnie z regulaminem konkursu (i jego FAQ), te dwa tygodnie traktuję jako jeden tydzień pracy i jeden „urlopu”. Wykorzystałem zatem drugi z pięciu przysługujących tygodni wolnego.

Postać Docelowa Faktury

Po dwutygodniowym rejsie po Mazurach, czas wrócić do pracy nad projektem. Aby skończyć warstwę logiki biznesowej, muszę jeszcze tylko stworzyć klasę, która zajmie się zamianą przyczajonej w bazie danych, zdigitalizowanej do cna postaci faktury w nie mniej zdigitalizowaną, ale niepomiernie bardziej czytelną dla użytkownika programu, postać dokumentu PDF. Użyję w tym celu biblioteki PdfSharp.

BHP z ostrym narzędziem
Tworzenie dokumentów PDF przy pomocy PdfSharp nie jest trudne. Przygotowanie nowego dokumentu to tylko kilka linijek:

PdfDocument document = new PdfDocument(); // stworzenie nowego dokumentu
PdfPage page = document.AddPage(); // stworzenie nowej strony dokumentu
XGraphics graphics = XGraphics.FromPdfPage(page); // stworzenie obiektu odpowiedzialnego za wygląd strony

Obiektem odpowiedzialnym za umieszczanie grafiki i treści na stronach dokumentu jest instancja klasy XGraphics. Na wygląd faktury będą się składać wyłącznie prostokąty i tekst, dlatego przedstawię sposób generowania tylko tych elementów.
Za tworzenie prostokątów odpowiada metoda DrawRectangle. Przykładowo, kod:

XPen pen = new XPen(XColors.Black, 1); // obramowanie prostokąta
XBrush brush = XBrushes.Red; // wypełnienie prostokąta
XRect rect = new XRect(0, 0, 60, 20); // położenie i wymiary prostokąta (x, y, szerokość, wysokość)
graphics.DrawRectangle(pen, brush, rect); // narysowanie prostokąta

narysuje w lewym górnym rogu strony czerwony prostokąt z czarnym obramowaniem.
Umieszczaniem na stronie tekstu zajmuje się metoda DrawString. I tak, kod:

XFont font = new XFont("Arial", 10, XFontStyle.Bold); // krój, rozmiar i styl czcionki
graphics.DrawString("Poufne", font, XBrushes.Black, rect, XStringFormats.Center); // dodanie czarnego napisu w środku stworzonego wcześniej prostokąta

sprawi, że nasz dokument, niezależnie od dalszej treści, będzie mógł być wykorzystany jako rekwizyt w dowolnym filmie szpiegowskim.

Przyda mi się także metoda MeasureString, zwracająca rozmiar napisu pisanego zadaną czcionką:

XSize size = graphics.MeasureString("<napis do zmierzenia>", czcionka);

Zapisanie dokumentu na dysku to nic więcej, niż napisanie:

document.Save("<nazwa pliku>");

Te metody (i ich przeciążenia) w zupełności wystarczą do wystawienia faktury VAT.

Very Arduous Task
Przepisy określają informacje, jakie muszą się znaleźć na fakturze:

  • nazwy i adresy sprzedawcy i nabywcy oraz ich numery NIP
  • datę dokonania sprzedaży oraz datę wystawienia dokumentu
  • napis Faktura VAT oraz numer kolejny faktury
  • nazwę towaru lub usługi (przedmiotu transakcji)
  • jednostkę miary i ilość sprzedanych towarów lub rodzaj wykonanych usług
  • cenę jednostkową towaru lub usługi bez kwoty podatku (cenę jednostkową netto)
  • wartość towarów lub wykonanych usług, których dotyczy sprzedaż, bez kwoty podatku (wartość sprzedaży netto)
  • stawki podatku
  • sumę wartości sprzedaży netto towarów lub wykonanych usług z podziałem na poszczególne stawki podatku, zwolnionych z podatku oraz niepodlegających opodatkowaniu
  • kwotę podatku od sumy wartości sprzedaży netto, z podziałem na kwoty dotyczące poszczególnych stawek podatku
  • wartość sprzedaży towarów lub wykonanych usług wraz z kwotą podatku (wartość sprzedaży brutto), z podziałem na kwoty dotyczące poszczególnych stawek podatku, zwolnionych z podatku lub niepodlegających opodatkowaniu
  • kwotę należności ogółem wraz z należnym podatkiem (brutto), wyrażoną cyframi i słownie
  • źródło: wikipedia

    Nie opisują jednak, jak dokładnie faktury mają wyglądać. Te wystawiane za pomocą InvoiceInvoker będą podobne do tworzonych przez program inFakt (jak już wcześniej wspominałem, zamierzam się czasem na nim wzorować), co, mam nadzieję, nie jest wykroczeniem. Po wielu godzinach tworzenia prostokątów, zmieniania ich rozmiarów i przesuwania o piksel, a także wypełniania tekstem o żmudnie dopasowywanych czcionkach, uzyskałem kod generujący akceptowalnie wyglądające dokumenty. Oto wyniki testów:
    [uwaga: arytmetyka nie była przedmiotem testów, dlatego nie należy szukać sensu w liczbach widniejących na fakturach]
    test_CreatesNormalInvoice
    test_HandlesVeryLongStrings
    test_SplitsProducts
    test_SplitsGeneralAmountsInfo
    test_SplitsPaymentInfo
    test_SplitsRemarks
    test_SplitsSignatures.

    Kodu klasy nie zamieszczam z oczywistego powodu: jest długi i nudny. Wytrwałym śmiałkom podpowiem jednak, że można go znaleźć tutaj. Wszystkim, którzy szukają sposobu na przeistoczenie kodu w dokument PDF, mogę natomiast polecić dokładniejsze zapoznanie się z PdfSharp.
    Wygląda na to, że warstwa logiki biznesowej jest już ukończona. Wkraczam więc na niepewny grunt: ASP.NET MVC, którego opanowanie jest jednym z głównych celów mojego udziału w konkursie. Stay tuned!

    Numer kolejny faktury

    Punkt drugi założeń projektowych (które można znaleźć tutaj) głosi:
    2. numer faktury definiowany szablonem,
    Czas zająć się tą funkcjonalnością. Projektując bazę danych zdecydowałem, że zarówno numer faktury jak i jego szablon będą łańcuchami znaków. Spójrzmy na przykład: „1/08/2010” – numer pierwszej faktury w sierpniu 2010. Według reguł, które zaraz przedstawię, szablon takiego numeru wygląda następująco: „N/MM/RR”.

    Struktura szablonu numeru faktury
    Numer kolejny faktury, jak można wywnioskować ze wstępu, powinien zawierać:
    – informację (zwaną dalej numerem) mówiącą, którą fakturą w danym miesiącu jest dana faktura,
    – miesiąc wystawienia faktury,
    – rok wystawienia faktury.
    Każdemu z tych elementów przypisałem literę: ‚N’ oznacza numer, ‚M’ – miesiąc, ‚R’ – rok. Użytkownik powinien móc zdecydować, czy miesiąc (przykładowo) sierpień będzie oznaczany jako „8”, czy „08”, i czy rok 2010 to „2010”, czy „10”. Podobnie, to od widzimisię użytkownika powinno zależeć, czy pierwsza faktura danego miesiąca otrzyma numer „1”, „01”, czy może „001”. Wszystkie te kwestie rozwiązałem w prosty i, mam nadzieję, intuicyjny sposób:
    – pojawienie się „M” w formacie oznacza, że (przykładowo) sierpień zostanie zapisany jako „8”,
    – pojawienie się „MM” – jako „08”,
    – „R” – rok 2010 zostanie zapisany jako „10”,
    – „RR” – jako „2010”,
    – „N” – pierwsza faktura miesiąca otrzyma numer „1”,
    – „NN” – numer „01”,
    – „NNN” – numer „001”, „NNNN” – „0001” i tak dalej.
    Aby nie było bałaganu, numer, miesiąc i rok mogą (i muszą) wystąpić tylko raz – poprawany jest szablon „N/M/R”, a niepoprawne: „N/R”, „N/M/R/R”. Co więcej, symbole te muszą być oddzielone separatorami, na przykład, jak we wszystkich powyższych przykładach, slashem.
    Klasę obsługującą numery faktur nazwałem InvoiceNumber. Działa ona w obie strony (tworzy numer kolejny faktury zgodny z zadanym szablonem na podstawie numeru, miesiąca i daty, a także odczytuje numer, miesiąc i datę na podstawie numeru kolejnego i jego szablonu) i wygląda tak:

    using System.Collections.Generic;
    using System.Linq;
    
    namespace InvoiceInvoker.Logic
    {
    	public class InvoiceNumber
    	{
    		public int Number { get; private set; }
    		public int Month { get; private set; }
    		public int Year { get; private set; }
    		public string Format { get; private set; }
    		public List<string> formatArray = new List<string>();
    
    		public InvoiceNumber(int number, int month, int year, string format = "N/MM/RR")
    		{
    			Number = number;
    			Month = month;
    			Year = year;
    			Format = CheckFormat(format) ? format : "N/MM/RR"; // default format is N/MM/RR
    			SetFormatArray();
    		}
    
    		public InvoiceNumber(string invoiceNumber, string format = "N/MM/RR")
    		{
    			Format = CheckFormat(format) ? format : "N/MM/RR"; // default format is N/MM/RR
    			SetFormatArray();
    			ReadFromString(invoiceNumber);
    		}
    
    		public static bool CheckFormat(string format)
    		{
    			// examples of proper formats: "N/M/R", "RR-MM-NN"
    			// N - invoice number
    			// M - month
    			// R - year
    
    			// format must contain letters: N, M, R
    			if (format.Contains('N') == false) return false;
    			if (format.Contains('M') == false) return false;
    			if (format.Contains('R') == false) return false;
    
    			// format cannot contain digits
    			if (format.Any(c => char.IsDigit(c))) return false;
    
    			int firstIndexOfN = format.IndexOf('N');
    			int lastIndexOfN = format.LastIndexOf('N');
    			int firstIndexOfM = format.IndexOf('M');
    			int lastIndexOfM = format.LastIndexOf('M');
    			int firstIndexOfR = format.IndexOf('R');
    			int lastIndexOfR = format.LastIndexOf('R');
    
    			// letters N must form a sequence, e.g. "NNN/M/R"
    			string sequence = format.Substring(firstIndexOfN, lastIndexOfN - firstIndexOfN + 1);
    			if (sequence.Any(c => c != 'N')) return false;
    
    			// letters M, R can only occur in the following combinations:
    			// ...X...
    			// ...XX...
    			// e.g. formats "N/M/M/R", "N:M:MM:R" are not allowed
    			if (lastIndexOfM - firstIndexOfM > 1) return false; // allows only one occurance of the following: "...N...", "...NN..."
    			if (lastIndexOfR - firstIndexOfR > 1) return false;
    
    			// letters N, M, R cannot occur next to each other
    			if (firstIndexOfM - lastIndexOfN == 1) return false; // disqualifies "NM" sequence
    			if (firstIndexOfN - lastIndexOfM == 1) return false; // disqualifies "MN" sequence
    			if (firstIndexOfR - lastIndexOfM == 1) return false; // disqualifies "MR" sequence
    			if (firstIndexOfM - lastIndexOfR == 1) return false; // disqualifies "RM" sequence
    			if (firstIndexOfR - lastIndexOfN == 1) return false; // disqualifies "NR" sequence
    			if (firstIndexOfN - lastIndexOfR == 1) return false; // disqualifies "RN" sequence
    
    			return true;
    		}
    
    		private void SetFormatArray()
    		{
    			formatArray.Add(Format[0].ToString());
    
    			for (int i = 1; i != Format.Length; i++)
    			{
    				if (Format[i] == 'N' || Format[i] == 'M' || Format[i] == 'R') // current char is a symbol of number
    				{
    					if (Format[i] == Format[i - 1]) // current char is a part of number sequence
    						formatArray[formatArray.Count - 1] += Format[i];
    					else							// current char starts a number sequence
    						formatArray.Add(Format[i].ToString());
    				}
    				else														  // current char is a symbol of separator
    				{
    					if (Format[i - 1] == 'N' || Format[i - 1] == 'M' || Format[i - 1] == 'R') // current char starts a separator sequence
    						formatArray.Add(Format[i].ToString());
    					else																	  // current char is a part of separator sequence
    						formatArray[formatArray.Count - 1] += Format[i];
    				}
    			}
    		}
    
    		private void ReadFromString(string invoiceNumber)
    		{
    			string temp = invoiceNumber;
    
    			for (int index = 0; index != formatArray.Count; index++)
    			{
    				if (formatArray[index].Contains('N')) // section under the index is the number section
    				{
    					string number = string.Empty;
    
    					// e.g. if temp == "11/12/13" ...
    					while (temp.Length > 0 && char.IsDigit(temp[0]))
    					{
    						number += temp[0];
    						temp = temp.Remove(0, 1);
    					}
    					// ... then number == "11" and temp == "/12/13"
    
    					Number = int.Parse(number);
    				}
    				else if (formatArray[index].Contains('M')) // section under the index is the month section
    				{
    					string month = string.Empty;
    
    					while (temp.Length > 0 && char.IsDigit(temp[0]))
    					{
    						month += temp[0];
    						temp = temp.Remove(0, 1);
    					}
    
    					Month = int.Parse(month);
    				}
    				else if (formatArray[index].Contains('R')) // section under the index is the year section
    				{
    					string year = string.Empty;
    
    					while (temp.Length > 0 && char.IsDigit(temp[0]))
    					{
    						year += temp[0];
    						temp = temp.Remove(0, 1);
    					}
    
    					Year = int.Parse(year);
    
    					if (formatArray[index] == "R") // e.g if year in string is "10" ...
    						Year += 2000;			   // ... then the property should be 2010
    				}
    				else									   // section under the index is a separator section
    				{
    					temp = temp.Remove(0, formatArray[index].Length);
    				}
    			}
    		}
    
    		public override string ToString()
    		{
    			string result = string.Empty;
    
    			for (int index = 0; index != formatArray.Count; index++)
    			{
    				if (formatArray[index].Contains('N')) // section under the index is the number section
    				{
    					int amountOfZeros = formatArray[index].Length - Number.ToString().Length; // e.g. if formatArray[index] == "NNNNN" and Number == 1 ...
    					for (int i = 0; i < amountOfZeros; i++)
    						result += "0";
    
    					result += Number.ToString();											  // ... then the number is converted into "00001"
    				}
    				else if (formatArray[index] == "M") // section under the index is the month section
    				{
    					result += Month.ToString();
    				}
    				else if (formatArray[index] == "MM") // section under the index is the month section
    				{
    					if (Month < 10)				// e.g. if Month == 1 ...
    						result += "0";
    
    					result += Month.ToString(); // ... then it's converted into "01"
    				}
    				else if (formatArray[index] == "R") // section under the index is the year section
    				{
    					int year = Year - 2000;	   // e.g. if Year == 2001 ...
    
    					if (year < 10)
    						result += "0";
    
    					result += year.ToString(); // ... then it's converted into "01"
    				}
    				else if (formatArray[index] == "RR") // section under the index is the year section
    				{
    					result += Year.ToString();
    				}
    				else								 // section under the index is a separator section
    				{
    					result += formatArray[index];
    				}
    			}
    
    			return result;
    		}
    	}
    }
    

    Kolejną funkcjonalnością będzie najprawdopodobniej graficzna reprezentacja faktury w formacie PDF – ale o tym we wrześniu (patrz ostatni akapit poprzedniego wpisu).

    Słownie złotych:

    Zgodnie z obowiązującym w Polsce prawem, faktura powinna zawierać przynajmniej:
    (…)
    – kwotę należności ogółem wraz z należnym podatkiem (brutto), wyrażoną cyframi i słownie.

    Tak podaje wikipedia. Dziś zajmę się pogrubionym fragmentem cytatu: zamianą kwoty pieniędzy określonej waluty na jej słowną reprezentację. Zakładam, że wystawiając faktury na kwoty powyżej 999999,99 (czyli milion i więcej) jednostek monetarnych, użytkownik – z nieopisaną satysfakcją – wpisze kwotę słownie własnoręcznie.

    Liczby => słowa
    Na początek zajmę się zamianą liczb całkowitych na słowa. Zastosuję poniższy algorytm:
    1. podział liczby na grupy cyfr (przykładowo liczbę 123456 podzielę na 123 i 456, a 12345 – na 12 i 345),
    2. zamiana bardziej znaczącej grupy na słowa,
    3. dodanie słowa „tysiące” w odpowiedniej formie,
    4. zamiana mniej znaczącej grupy na słowa.

    W implementacji algorytmu przyda mi się extension method zamieniająca liczbę na wektor jej cyfr:

    private static int[] ToDigitArray(this int number)
    {
    	// przykładowo zamienia 123456 na { 6, 5, 4, 3, 2, 1 } 
    	// (najmniej znacząca cyfra -> indeks 0)
    
    	string str = number.ToString();
    	int[] digitArray = new int[str.Length];
    
    	for (int i = 0; i != digitArray.Length; i++)
    		digitArray[i] = int.Parse(str[str.Length - 1 - i].ToString());
    
    	return digitArray;
    }
    

    Metody zamieniające kolejne cyfry na słowa wyglądają, tu fajerwerków nie będzie, tak:
    (zamienia jedności:)

    private static string ConvertUnits(int number)
    {
    	switch (number)
    	{
    		case 1: return "jeden";
    		case 2: return "dwa";
    		case 3: return "trzy";
    		case 4: return "cztery";
    		case 5: return "pięć";
    		case 6: return "sześć";
    		case 7: return "siedem";
    		case 8: return "osiem";
    		case 9: return "dziewięć";
    		default: return string.Empty;
    	}
    }
    

    (zamienia „nastki”:)

    private static string ConvertTeens(int number)
    {
    	switch (number)
    	{
    		case 11: return "jedenaście";
    		case 12: return "dwanaście";
    		case 13: return "trzynaście";
    		case 14: return "czternaście";
    		case 15: return "pięnaście";
    		case 16: return "szesnaście";
    		case 17: return "siedemnaście";
    		case 18: return "osiemnaście";
    		case 19: return "dziewiętnaście";
    		default: return string.Empty;
    	}
    }
    

    (zamienia dziesiątki:)

    private static string ConvertTens(int number)
    {
    	switch (number)
    	{
    		case 1: return "dziesięć";
    		case 2: return "dwadzieścia";
    		case 3: return "trzydzieści";
    		case 4: return "czterdzieści";
    		case 5: return "pięćdziesiąt";
    		case 6: return "sześćdziesiąt";
    		case 7: return "siedemdziesiąt";
    		case 8: return "osiemdziesiąt";
    		case 9: return "dziewięćdziesiąt";
    		default: return string.Empty;
    	}
    }
    

    (zamienia setki:)

    private static string ConvertHundreds(int number)
    {
    	switch (number)
    	{
    		case 1: return "sto";
    		case 2: return "dwieście";
    		case 3: return "trzysta";
    		case 4: return "czterysta";
    		case 5: return "pięćset";
    		case 6: return "sześćset";
    		case 7: return "siedemset";
    		case 8: return "osiemset";
    		case 9: return "dziewięćset";
    		default: return string.Empty;
    	}
    }
    

    Zamiana grup cyfr (o których mowa w pkt. 1 algorytmu) na słowa wygląda więc tak,

    private static string GetHundreds(int[] numberArray)
    {
    	string result;
    
    	switch (numberArray.Length)
    	{
    		case 1:	// np. 9
    			result = ConvertUnits(numberArray[0]);
    			break;
    		case 2: // np. 99
    			if (numberArray[1] == 1) // np. 91
    				result = ConvertTeens(10 + numberArray[0]);
    			else
    				result = ConvertTens(numberArray[1]) + " " + ConvertUnits(numberArray[0]);
    			break;
    		case 3: // np. 999
    			if (numberArray[1] == 1) // np. 919
    				result = ConvertHundreds(numberArray[2]) + " " + ConvertTeens(10 + numberArray[0]);
    			else
    				result = ConvertHundreds(numberArray[2]) + " " + ConvertTens(numberArray[1]) + " " + ConvertUnits(numberArray[0]);
    			break;
    		default:
    			result = string.Empty;
    			break;
    	}
    
    	return result;
    }
    

    metoda używana przy trzecim kroku algorytmu – tak,

    private static string GetGrammaticalThousands(int lastDigit)
    {
    	switch (lastDigit)
    	{
    		case 1:
    			return "tysiąc";
    		case 2:
    		case 3:
    		case 4:
    			return "tysiące";
    		default:
    			return "tysięcy";
    	}
    }
    

    a cały algorytm – tak (number to liczba pobrana w argumencie):

    string result = string.Empty;
    int[] digitArray = number.ToDigitArray();
    
    if (digitArray.Length == 6) // e.g. 123456
    {
    	int[] moreSignificantDigits = { digitArray[3], digitArray[4], digitArray[5] }; // e.g. { 3, 2, 1 }
    	int[] lessSignificantDigits = { digitArray[0], digitArray[1], digitArray[2] }; // e.g. { 6, 5, 4 }
    
    	if (digitArray[4] == 1) // e.g. 919999
    		result = GetHundreds(moreSignificantDigits) + " tysięcy " + GetHundreds(lessSignificantDigits);
    	else
    		result = GetHundreds(moreSignificantDigits) + " " + GetGrammaticalThousands(digitArray[3]) + " " + GetHundreds(lessSignificantDigits);
    }
    else if (digitArray.Length == 5) // e.g. 12345
    {
    	int[] moreSignificantDigits = { digitArray[3], digitArray[4] }; // e.g. { 2, 1 }
    	int[] lessSignificantDigits = { digitArray[0], digitArray[1], digitArray[2] }; // e.g. { 5, 4, 3 }
    
    	if (digitArray[4] == 1) // 91999
    		result = GetHundreds(moreSignificantDigits) + " tysięcy " + GetHundreds(lessSignificantDigits);
    	else
    		result = GetHundreds(moreSignificantDigits) + " " + GetGrammaticalThousands(digitArray[3]) + " " + GetHundreds(lessSignificantDigits);
    }
    else if (digitArray.Length == 4) // e.g. 1234
    {
    	int[] moreSignificantDigits = { digitArray[3] }; // e.g. { 4 }
    	int[] lessSignificantDigits = { digitArray[0], digitArray[1], digitArray[2] }; // e.g. { 3, 2, 1 }
    
    	result = GetHundreds(moreSignificantDigits) + " " + GetGrammaticalThousands(digitArray[3]) + " " + GetHundreds(lessSignificantDigits);
    }
    else
    {
    	result = GetHundreds(digitArray);
    }
    

    Dla number == 0 metoda konwertująca (o niezbyt zaskakującej nazwie Convert) zwraca „zero”, a dla number >= 1,000,000 – „wpisz ręcznie”.

    Kwota => słowa
    Zamiana kwoty pieniężnej na słowa to już sprawa prosta:

    public static string ConvertMoney(decimal amount, string currency)
    {
    	string result = Convert((int)amount); // konwersja części całkowitej
    	result += " " + currency + " "; // dodanie waluty
    	decimal decimals = (amount - decimal.Floor(amount)) * 100; // wyłuskanie części ułamkowej
    	result += ((int)decimals).ToString(); // dodanie części ułamkowej
    	result += "/100";
    
    	return result;
    }
    

    W ten sposób kwota 123456,78 PLN jest zamieniana na: „sto dwadzieścia trzy tysiące czterysta pięćdziesiąt sześć PLN 78/100”.

    Na koniec małe usprawiedliwienie. Nie można zaprzeczyć, że wpis jest rażąco lakoniczy i, co tu kryć, pisany na szybko. Jest to spowodowane tym, że aktualnie się przeprowadzam. Z jednego miejsca zamieszkania – w trzy (szczegóły tej sytuacji ciężko byłoby opisać w kilku słowach…). Dodatkowo, post ten napisałem na kilka dni przed jego opublikowaniem, ponieważ w chwili, gdy czytelnik beznamiętnie przewija kolejne kawały zaprezentowanego przeze mnie kodu, ja beztrosko bujam się na falach któregoś z jezior mazurskich. Taki natłok wydarzeń uniemożliwia mi regularne blogowanie, dlatego czeka mnie przynajmniej tydzień „urlopu” od konkursu.

    Księga pierwsza: projektowanie bazy danych

    Zacznę od pytania: co chcę przechowywać w bazie? I natychmiast odpowiem: na pewno faktury. Zerknięcie na założenia projektowe (pkt. 1) pozwala rozszerzyć odpowiedź. Potrzebował będę również takich tabel jak: klienci, produkty, szablony faktur. Przydałoby się również miejsce na przechowanie danych o użytkownikach programu – tabela sprzedawcy.
    Mam już ogólny zarys bazy danych, więc zadaję kolejne pytanie: co powinna zawierać faktura VAT? Odpowiedź, już nie natychmiastową, przynoszą: wikipedia, serwis Moja firma, aplikacja inFakt (na której, nie ukrywam, zamierzam się podczas swojej pracy wzorować) i przykładowa faktura. Takie zagłębienie się w arkana ekonomii daje mi wiedzę potrzebną do stworzenia tabeli Invoices:
    1. Id [int] – identyfikator faktury
    2. SellerId [int] – sprzedawca [relacja]
    3. CustomerId [int] – klient [relacja]
    4. Number [nvarchar] – numer kolejny faktury
    5. CreationDate [datetime] – data wystawienia faktury
    6. SaleDate [datetime] – data sprzedaży
    7. PaymentType [nvarchar] – sposób płatności
    8. PaymentDeadline [datetime] – termin płatności
    9. PaymentCurrency [nvarchar] – waluta
    10. ToPay [money] – do zapłaty
    11. ToPayInWords [nvarchar] – słownie do zapłaty
    12. Paid [money] – zapłacono
    13. LeftToPay [money] – pozostało do zapłaty
    14. Remarks [nvarchar] – uwagi
    15. Status [nvarchar] – status faktury (utworzona / wystawiona / zapłacona)
    EDIT: 16. RegisteredSellerId [int] – użytkownik programu [relacja] /EDIT
    Nie ma tu pola mówiącego o produktach, ponieważ produkty zostaną połączone z fakturą relacją wiele (produktów) do jednej (faktury).

    Jak na razie idzie jak po maś taniej margarynie, a takiej nie można ufać. Dlatego przełknijmy i przetrawmy spokojnie to, co już zaprojektowałem. W utworzonej tabeli pojawiają się perspektywy relacji (np. relacja produkty – faktury), co nie powinno dziwić, ale powinno zastanowić. Wyobraźmy sobie bowiem taką sytuację: użytkownik wystawia fakturę na jakiś produkt, niech to będzie ta tania margaryna. Po miesiącu postanawia jednak podnieść jej cenę. Przy obecnej strukturze bazy danych, taka operacja spowodowałaby zmianę kwoty faktury sprzed miesiąca – hańba! Problem leży w tym, że produkty zdefiniowane przez użytkownika (podatne na edycję) są jednocześnie produktami przypisywanymi do faktur (ich parametrów zmieniać nie należy). Rozwiązaniem może być zastosowanie dwóch tabel produktów:
    1. produkty definiowane przez użytkownika,
    2. produkty przypisywane do faktur, niezmienne (niedostępne dla użytkownika);
    i tak też uczynię. Oczywiście podobnego zabiegu wymagają tabele klientów i sprzedawców.

    Możemy zatem zająć się zawartością kolejnych tabel. Podlinkowane wcześniej źródła wiedzy w zupełności wystarczą, w końcu baza będzie zawierać (prawie) jedynie dane potrzebne do wystawienia faktury. Godzinę (lektury, buszowania po inFakt i projektowania) później, mam (tabele Registered to tabele dla danych definiowanych przez użytkownika, pozostałe – dla danych „niezmiennych”, przypisywanych do faktur):

    RegisteredSellers:
    1. Id [int] – identyfikator sprzedawcy
    2. FirstName [nvarchar] – imię sprzedawcy
    3. LastName [nvarchar] – nazwisko sprzedacy
    4. CompanyName [nvarchar] – nazwa firmy sprzedawcy
    5. Street [nvarchar] – ulica [adres firmy]
    6. City [nvarchar] – miasto [adres firmy]
    7. PostalCode [nvarchar] – kod pocztowy [adres firmy]
    8. BankName [nvarchar] – nazwa banku
    9. BankAccountNumber [nvarchar] – numer konta firmy
    10. BankSwift [nvarchar] – swift banku (nieobowiązkowy)
    11. Nip [nvarchar] – NIP sprzedawcy
    12. Regon [nvarchar] – REGON firmy (nieobowiązkowy)
    13. InvoiceNumberFormat [nvarchar] – format numerów faktur (patrz pkt. 2 założeń projektu)
    14. LastInvoiceNumber [nvarchar] – numer kolejny ostatniej faktury

    Sellers: (opis kolumn jw.; zawiera tylko dane, które znajdą się na fakturze)
    1. Id [int]
    2. SellerName [nvarchar]
    3. CompanyName [nvarchar]
    4. Street [nvarchar]
    5. City [nvarchar]
    6. PostalCode [nvarchar]
    7. BankName [nvarchar]
    8. BankAccountNumber [nvarchar]
    9. BankSwift [nvarchar]
    10. Nip [nvarchar]

    RegisteredCustomers:
    1. Id [int] – identyfikator klienta
    2. CustomerName [nvarchar] – imię i nazwisko klienta
    3. CompanyName [nvarchar] – nazwa firmy klienta
    4. Street [nvarchar] – ulica [adres firmy]
    5. City [nvarchar] – miasto [adres firmy]
    6. PostalCode [nvarchar] – kod pocztowy [adres firmy]
    7. Nip [nvarchar] – NIP klienta
    8. Email [nvarchar] – e-mail klienta (nieobowiązkowy)
    9. Www [nvarchar] – strona www klienta (nieobowiązkowa)
    10. Phone [nvarchar] – telefon klienta (niebowiązkowy)
    11. Remarks [nvarchar] – uwagi (nieobowiązkowe)
    EDIT: 12. RegisteredSellerId [int] – użytkownik programu [relacja] /EDIT

    Customers: (opis kolumn jw.; zawiera tylko dane, które znajdą się na fakturze)
    1. Id [int]
    2. CustomerName [nvarchar]
    3. CompanyName [nvarchar]
    4. Street [nvarchar]
    5. City [nvarchar]
    6. PostalCode [nvarchar]
    7. Nip [nvarchar]

    RegisteredProducts: (opis kolumn niżej)
    1. Id [int]
    2. Name [nvarchar]
    3. Pkwiu [nvarchar]
    4. MeasureUnit [nvarchar]
    5. NetPrice [money]
    6. VatRate [nvarchar]
    EDIT: 7. RegisteredSellerId [int] – użytkownik programu [relacja] /EDIT

    Products: (wyjątkowo zawiera więcej kolumn niż tabela Registered)
    – Id [int] – identyfikator produktu
    – InvoiceId [int] – faktura [relacja]
    – Name [nvarchar] – nazwa produktu
    – Pkwiu [nvarchar] – klasyfikacja PKWiU produktu
    – MeasureUnit [nvarchar] – jednostka miary produktu
    – Quantity [money] – ilość (typ money dla uniknięcia konfliktu typów przy mnożeniu)
    – NetPrice [money] – cena netto produktu
    – NetValue [money] – wartość netto produktu (ilość * cena)
    – VatRate [nvarchar] – stawka VAT produktu
    – VatValue [money] – wartość VAT (wartość netto * stawka VAT)
    – GrossValue [money] – wartość brutto produktu (wartość netto + wartość VAT)

    Trochę się tego uzbierało, nie ma co. Ostatnie elementy podam na sucho, bez barwnego wprowadzenia, bo nie sądzę, żeby którykolwiek z czytelników dobrnął aż tutaj.

    InvoicePatterns: (tabela zawierająca szablony faktur)
    1. Id [int] – identyfikator szablonu
    2. RegisteredSellerId [int] – sprzedawca (z tabeli Registered) [relacja]
    3. RegisteredCustomerId [int] – klient (jw.) [relacja]
    4. PaymentType [nvarchar] – sposób płatności
    5. PaymentCurrency [nvarchar] – waluta
    6. Remarks [nvarchar] – uwagi

    InvoicePatternRegisteredProducts: (realizuje relację RegisteredProducts – InvoicePatterns)
    – InvoicePatternId [int] – szablon faktury [relacja]
    – RegisteredProductId [int] – produkt (z tabeli Registered) [relacja]

    Na deser (spodziewam się, że niewielu śmiałków dotrwa do deseru – niech żałują!) wypiszę zastosowane relacje:

    1. Sellers – Invoices (jeden do jednego)
    2. Customers – Invoices (jeden do jednego)
    3. Invoices – Products (jeden do wielu)

    4. RegisteredSellers – InvoicePatterns (jeden do wielu)
    5. RegisteredCustomers – InvoicePatterns (jeden do wielu)
    6. RegisteredProducts – InvoicePatterns (wiele do wielu)

    Et voila! Winszuję wytrwałym czytelnikom, zapowiadając jednocześnie, że kolejne wpisy nie będą tak długie i nużące.

    EDIT:
    Przejażdżka rowerem i świeże powietrze sprowadziły na mnie olśnienie: w żadnym miejscu nie przypisałem faktury do użytkownika! Niedopatrzenie naprawiłem dodając kolumnę RegisteredSellerId [int] do tabeli Invoices i relację RegisteredSellers – Invoices (jeden do wielu).
    Posłuszny celnej sugestii czytelnika tomash2310, zamieszczam również miły oku schemat bazy danych.

    EDIT 2:
    Zdałem sobie sprawę, że błąd popełniłem nie tylko w przypadku faktur, ale również klientów i produktów. Dodałem zatem odpowiednie kolumny (RegisteredSellerId [int] do tabel: RegisteredCustomers i RegisteredProducts) i relacje (RegisteredSellers – RegisteredCustomers i RegisteredSellers – RegisteredProducts). Zaktualizowałem też graficzny schemat bazy danych, kończąc tym samym, przynajmniej tymczasowo, pracę nad jej strukturą:

    database