Exodus

Na początek zapowiedzniane w poprzednim wpisie kilka linijek o rozwiązaniu problemu z automatyczną walidacją danych – a właściwie jej brakiem. Rozwiązaniem okazała się być zmiana nagłówka metody kontrolera z takiego:
(generowanego automatycznie, tutaj dla scenariusza tworzenia nowego produktu)

[HttpPost]
public ActionResult Create(FormCollection collection)

na taki:

[HttpPost]
public ActionResult Create(ProductsViewModel viewModel)

gdzie ProductsViewModel jest klasą, na której bazuje silnie typowany widok dodawania produktu:

public class ProductsViewModel
{
	public ProductModel Product { get; set; } // patrz typ RegisteredProductModel przedstawiony w poprzednim wpisie
	public List<string> VatRates { get; set; } // stawki VAT możliwe do przypisania produktowi
}

Tak jak się spodziewałem – banał. Coż, tak bywa z rzeczami na tyle prostymi, że nie są jasno omówione w tutorialach. Przejdźmy jednak do tematu przewodniego dzisiejszego wpisu:

Pierwsza uruchamialna wersja programu
Po pięciu tygodniach pracy mogę wreszcie sprawdzić działanie kodu w praktyce – nie tylko, jak dotychczas, poprzez testy jednostkowe. Przez kilka dni nauki maglowałem tabelę RegisteredProducts bazy danych. Stworzyłem jej model, kontoler i widoki, realizujące przeglądanie, dodawanie, edytowanie i usuwanie produktów. Następnie wziąłem się za mechanizm rejestracji i logowania użytkowników. Jest on implementowany automatycznie, więc pozostało mi tylko zapoznać się z jego działaniem.
Dopiero teraz mogłem przemyśleć sposób przypisywania encji bazy danych konkretnym użytkownikom. Jako że nazwa użytkownika jest unikalna, zdecydowałem się na dodanie do tabeli RegisteredSellers kolumny UserName (co widać na załączonym w poprzednim akapicie schemacie) i używania jej jako łącznika między użytkownikiem, a encją. Po zarejestrowaniu nowego użytkownika, do bazy danych dodawany jest nowy obiekt RegisteredSeller – z jego nazwą umieszczoną w polu UserName.
Zarejestrowany użytkownik jest przenoszony na stronę swojego profilu, gdzie może podać dane takie jak imię, nazwisko, czy nazwa i adres firmy. Ma też do dyspozycji wspomnianą wcześniej stronę produktów, z możliwością filtrowania po nazwie i cenie. Niezalogowani użytkownicy mogą zobaczyć jedynie stronę O programie. Na razie to wszystko, aplikację w obecnej formie można pobrać tutaj.
Należałoby jeszcze wspomnieć o sposobie przechowywania (podczas sesji) informacji o zalogowanym użytkowniku. W dowolnym (oprócz konstruktorów) miejscu kontrolerów mam dostęp do jego nazwy:

string userName = User.Identity.Name;

Bardziej przydałby mi się jednak identyfikator encji RegisteredSeller powiązanej z użytkownikiem. Do repozytorium sprzedawców (RegisteredSellerRepository) dodałem więc taką metodę:

public int GetIdByUserName(string userName)
{
	using (MainDataContext dataContext = new MainDataContext(_connectionString))
	{
		return dataContext.RegisteredSellers.SingleOrDefault(seller => seller.UserName == userName).Id;
	}
}

Kontroler produktów uzupełniłem natomiast o metodę wywoływaną przed wykonaniem każdej akcji:

protected override void OnActionExecuting(ActionExecutingContext filterContext)
{
	if (Session["SellerId"] == null)
		Session["SellerId"] = new RegisteredSellerRepository().GetIdByUserName(User.Identity.Name);

	_sellerId = (int)Session["SellerId"]; // _sellerId - prywatne pole
	_repository = new RegisteredProductRepository(_sellerId); // _repository - prywatne pole

	base.OnActionExecuting(filterContext);
}

Zaznaczony fragment będzie powtarzał się w każdym kontrolerze, a wykorzystany zostanie podczas pojedynczej sesji tylko raz, przy wykonaniu pierwszej w programie akcji, dlatego takie rozwiązanie niezbyt mi się podoba. Na razie nie znalazłem jednak lepszego – może Czytelnicy takie znają?

Na chleb – „pep”, a na ASP – „a-ef-e”, czyli raczkowanie w ASP.NET MVC

Mój tort jest już prawie gotowy: spodni biszkopt równomiernie nasiąknął danymi, a środkowa masa osiągnęła spójną konsystencję. Pozostało jeszcze tylko przygotować ostatnią warstwę, dzięki której całość będzie prezentować się iście smakowicie.
Ale dość naśladowania mistrza – przedmiot dzisiejszego wpisu wymaga klarownego języka.

Model – Vidok – Contoler
Struktura MVC polega na podziale pracy aplikacji na trzy segmenty:
– Model – dostarcza informacje o danych, na których program będzie pracować,
– View – odpowiada za formę, czyli to, co widzi użytkownik,
– Controller – realizuje interakcję z użytkownikiem i przepływ danych z Model do View.

Tę właśnie strukturę realizuje Microsoft ASP.NET MVC. Przystępne omówienie tej technologii można znaleźć u źródeł.

Pierwsze kroki
Jak dotąd tworzyłem jedynie programy desktopowe. Aplikacje sieciowe są dla mnie nowością, dlatego skorzystałem z życzliwiości wuja Google i, oprócz informacji teoretycznych, znalazłem sensowanie wyglądający tutorial, z którego postanowiłem skorzystać. Po krótkim przygotowaniu teoretycznym, stworzyłem próbny projekt i wziąłem się za praktykę. Wstępne ujarzmianie kontrolerów i widoków poszło gładko, problem pojawił się natomiast przy próbie wykorzystania projektu InvoiceInvoker jako modelu. Treść problemu była taka: „SQL Server Compact is not intended for ASP.NET development”. Moja reakcja: taka. Nie pozostało nic innego, jak stworzyć nową bazę danych o tych samych tabelach, tym razem używając „normalnej” edycji SQL Server, i ruszyć dalej. Udało mi się oprogramować scenariusze przeglądania, dodawania, edytowania i usuwania produktów (tabela RegisteredProducts bazy danych). Moją euforię zgasił jednak kolejny problem, tym razem niewynikający (mam nadzieję) bezpośrednio z mojego niedopatrzenia: nie potrafię skłonić aplikacji do przeprowadzania walidacji danych wprowadzanych przez użytkownika. Jako że model bazy danych znajduje się w bibliotece zewnętrznej (InvoiceInvoker.Logic.dll), stworzyłem zastępczy model produktu:

using System.ComponentModel;
using System.ComponentModel.DataAnnotations;
using InvoiceInvoker.Logic;

namespace MvcApplication1.Models
{
	public class RegisteredProductModel
	{	
		public int Id { get; set; }
		public int RegisteredSellerId { get; set; }

		[DisplayName("Nazwa")]
		[Required(ErrorMessage="Name required")]
		[StringLength(60, ErrorMessage="Name must be under 60 characters")]
		public string Name { get; set; }

		[DisplayName("PKWiU")]
		[StringLength(20, ErrorMessage = "PKWiU must be under 20 characters")]
		public string Pkwiu { get; set; }

		[DisplayName("Jednostka miary")]
		[Required(ErrorMessage = "Measure unit required")]
		[StringLength(10, ErrorMessage = "Measure unit must be under 10 characters")]
		public string MeasureUnit { get; set; }

		[DisplayName("Cena netto")]
		[Required(ErrorMessage = "Net price required")]
		[Range(0, (double)decimal.MaxValue, ErrorMessage = "Net price must be greater than or equal to 0")]
		public decimal NetPrice { get; set; }

		[DisplayName("Stawka VAT")]
		[Required(ErrorMessage = "Vat rate required")]
		[StringLength(10, ErrorMessage = "Vat rate must be under 10 characters")]
		public string VatRate { get; set; }
	}
}

Według wspomnianego tutorialu, a także tego artykułu, dodanie atrybutów z przestrzeni nazw System.ComponentModel wystarcza do osiągnięcia automatycznej walidacji. Nie u mnie. Właściwość ModelState mojego kontrolera, zachowując pokerową twarz, niezmiennie spełnia warunek: StateModel.IsValid == true.

Próbowałem co najmniej kilku sposobów, jednak w dalszym ciągu nie osiągnąłem tego właściwego, który, jak to bywa w takich przypadkach, zapewne okaże się banalny. Jeśli w końcu znajdę źródło problemu i jego rozwiązanie, poświęcę temu kilka linijek kolejnego wpisu, w którym, mam nadzieję, zaprezentuję również pierwszą uruchamialną wersję programu. Zapraszam!

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 liczb: walidacja NIPu i REGONu

    Po ukończeniu warstwy dostępu do danych, mogę zabrać się za logikę biznesową. Na początek – walidacja numerów NIP i REGON.
    Czym jest NIP, wie mniej więcej każdy. Czym jest REGON, zapewne bardziej „mniej” niż „więcej”. Z kolei wiedzę o tym, co oznaczają poszczególne cyfry tych numerów, moża śmiało uznać za tajemną. Dziś, na potrzeby projektu, uchylę rąbka tajemnicy: zajmę się ich ostatnimi cyframi. Są to cyfry kontrolne, wyznaczane algorytmicznie. Aby je wyliczyć, należy:
    1. pomnożyć każdą z poprzednich cyfr przez odpowiednie wagi,
    2. zsumować wyniki mnożeń,
    3. obliczyć resztę z dzielenia przez 11.

    NIP
    Przyjrzyjmy się bliżej numerowi NIP. Aby określić, czy ciąg znaków jest poprawnym Numerem Identyfikacji Podatkowej, należy przede wszystkim sprawdzić, czy w ogóle jest numerem (linia nr 5 kodu, który zaraz przedstawię). Konkretniej: numerem dziesięciocyfrowym (linia nr 7). Dopiero teraz można zastosować wyżej przedstawiony algorytm – wspomniane w punkcie pierwszym wagi to kolejno: 6, 5, 7, 2, 3, 4, 5, 6, 7. Programistom wygodniej będzie spojrzeć na kod:

    public static bool ValidateNip(string nipString)
    {
    	long nip;
    
    	if (long.TryParse(nipString, out nip))
    	{
    		int[] digitArray = nip.ToDigitArray();
    
    		if (digitArray.Length == 10)
    		{
    			int sum = digitArray[0] * 6
    					+ digitArray[1] * 5
    					+ digitArray[2] * 7
    					+ digitArray[3] * 2
    					+ digitArray[4] * 3
    					+ digitArray[5] * 4
    					+ digitArray[6] * 5
    					+ digitArray[7] * 6
    					+ digitArray[8] * 7;
    
    			if (sum % 11 == digitArray[9])
    				return true;
    		}
    	}
    
    	return false;
    }
    

    W podświetlonej (zaciemnionej?) linii używam extension method zamieniającej liczbę na wektor jej cyfr:

    private static int[] ToDigitArray(this long number)
    {
    	// przykładowo zamienia 123456 na { 1, 2, 3, 4, 5, 6 }
    	// (najbardziej 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[i].ToString());
    
    	return digitArray;
    }
    

    REGON
    Numer Rejestru Gospodarki Narodowej może składać się z dziewięciu lub czternastu cyfr. Wagi, o których mowa w algorytmie wyznaczania cyfry kontrolnej, wynoszą odpowiednio:
    1. 8, 9, 2, 3, 4, 5, 6, 7 dla REGONu 9-cyfrowego,
    2. 2, 4, 8, 5, 0, 9, 7, 3, 6, 1, 2, 4, 8 dla REGONu – 14-cyfrowego.
    Cały proces sprawdzania popraności tego numeru wygląda więc tak:

    public static bool ValidateRegon(string regonString)
    {
    	long regon;
    
    	if (long.TryParse(regonString, out regon))
    	{
    		int[] digitArray = regon.ToDigitArray();
    
    		if (digitArray.Length == 9)
    		{
    			int sum = digitArray[0] * 8
    					+ digitArray[1] * 9
    					+ digitArray[2] * 2
    					+ digitArray[3] * 3
    					+ digitArray[4] * 4
    					+ digitArray[5] * 5
    					+ digitArray[6] * 6
    					+ digitArray[7] * 7;
    
    			int mod = sum % 11;
    
    			if (mod == 10)
    				mod = 0;
    
    			if (mod == digitArray[8])
    				return true;
    		}
    		else if (digitArray.Length == 14)
    		{
    			int sum = digitArray[0] * 2
    					+ digitArray[1] * 4
    					+ digitArray[2] * 8
    					+ digitArray[3] * 5
    					+ digitArray[4] * 0
    					+ digitArray[5] * 9
    					+ digitArray[6] * 7
    					+ digitArray[7] * 3
    					+ digitArray[8] * 6
    					+ digitArray[9] * 1
    					+ digitArray[10] * 2
    					+ digitArray[11] * 4
    					+ digitArray[12] * 8;
    
    			int mod = sum % 11;
    
    			if (mod == 10)
    				mod = 0;
    
    			if (mod == digitArray[13])
    				return true;
    		}
    	}
    
    	return false;
    }
    

    Jak widać, pierwsze kroki po warstwie logiki biznesowej to raczej spacer, niż bieg przez płotki. Na horyzoncie majaczą się jednak takie problemy, jak zamiana liczb na słowa, czy obsługa szablonów numerów faktur – o tym w przyszłych wpisach, zapraszam!

    Żółwie tempo rozszerzania się rtęci

    Jako że udało mi się już napisać solidny kęs kodu, postanowiłem wreszcie opublikować projekt na CodePlex i stworzyć lokalne repozytorium. Ale zanim o tym…

    Struktura projektu
    Nie wspomniałem jeszcze ani słowem o strukturze utworzonej w Visual Studio solucji. Teraz, kiedy jest już ona publcznie dostępna, wypadałoby objaśnić zastosowany podział na projekty. Prezentuje się on tak:

    solution

    W projekcie InvoiceInvoker.Logic umieściłem bazę danych i repozytoria odpowiedzialne za dostęp do nich, a także logikę biznesową. Właściwie powinienem przeznaczyć na te warstwy dwa projekty – jeśli któraś z nich rozrośnie się ponad spodziewane rozmiary, zapewne tak uczynię. Natomiast projekt InvoiceInvoker.Logic.Tests zawiera testy jednostkowe, pisane pod NUnit. W niedalekiej przyszłości do już istniejącego duetu dołączy projekt odpowiedzialny za interakcję z użytkownikiem.

    TortoiseHg
    Zrelacjonuję teraz pokrótce pionierską wyprawę plików projektu do repozytorium (w systemie Mercurial) utworzonego automatycznie przez CodePlex. Zacząć należało od stworzenia repozytorium lokalnego. Pomógł mi w tym prosty tutorial. Nie obeszło się jednak bez problemów.
    Początkowo w głównym katalogu projektu utworzyłem podkatalog Repository i to do niego sklonowałem repozytorium z CodePlex. Po chwili zorientowałem się, że jednak nie o to chodzi – aby Żółw zuważył swym leniwym okiem moje pliki, musiałem je kopiować do utworzonego katalogu. Sklonowałem więc repozytorium do głównego katalogu – IvoiceInvoker. Stworzył się w nim podkatalog invoiceinvoker i dopiero tam repozytorium. Miałem więc sytuację analogiczną do tej z podkatalogiem Repository. Po chwili konsternacji wpadłem na rozwiązanie: przeniosłem całą zawartość głównego katalogu w inne miejsce i sklonowałem repozytorium z CodePlex do pustego już InvoiceInvoker – tym razem się udało (wniosek: jeśli TortoiseHg klonuje repozytorium do niepustego katalogu, to tworzy w nim podkatalog – i tam repozytorium). Pozostało tylko przerzucić z powrotem pliki projektu do katalogu głównego i zabrać się za pierwszy commit.
    (W tym miejscu wyrażam ubolewanie nad brakiem synonimów słów: katalog, repozytorium. Konsekwencją tego braku jest, co czytelnik z pewnością zauważył, nachalne występowanie wymienionych słów w każdym zdaniu powyższego akapitu…)

    Mercurial
    Popełniając pierwszy commit, byłem chyba zbyt przepełniony euforią wywołaną przez pomyślne utworzenie lokalnego repozytorium – na CodePlex’owe repo wysłałem wszystkie pliki projektu, włącznie ze skompilowanym kodem. Kiedy (już po raz drugi) zorientowałem się, że to nie o to chodzi, zacząłem szukać sposobu na usunięcie niepotrzebnych plików z commita. Większość tutoriali w sieci pokazuje tylko taką sekwencję: add -> commit -> push. Po jakimś czasie poszukiwań i badań metodą prób i błędów, doszedłem do, jakże banalnego, rozwiązania. Wystarczyło zauważyć przycisk Forget – i użyć go po zaznaczeniu niepotrzebnych plików.
    Oprócz katalogów zawierających projekty, wrzuciłem także katalog libraries – znajdować się w nim będą wykorzystywane w projekcie biblioteki zewnętrzne (na razie tylko nunit.framework). Z kolei plik InvoiceInvoker.config zawiera ścieżkę do bazy danych znajdującej się w katalogu InvoiceInvoker.Logic. Ze ścieżki tej korzysta NUnit przeprowadzając testy jednostkowe, dlatego chętni do testowania – ponieważ ścieżka jest bezwzględna – powinni ją odpowiednio edytować.

    Takim to sposobem utworzyłem i okiełznałem swoje pierwsze repozytorium. Zajęło mi to kilka godzin, co jest niewątpliwie żółwim tempem, jednak teraz moja rtęć będzie się rozszerzać coraz sprawniej i szybciej.

    Spojrzenie w DAL

    Po stworzeniu bazy danych, nadszedł wreszcie czas na pierwsze linie kodu. Na początek – generowanego automatycznie.

    Object-relational mapping
    Wśród technologii wybranych do realizacji projektu wymieniłem LINQ to SQL – i tego właśnie narzędzia ORM będę używał.
    (Dygresja: spotkałem się z dwoma tłumaczeniami nazwy Language-Integrated Query na język polski: zintegrowany język zapytań, język zapytań zintegrowanych. Czy poprawnym tłumaczeniem nie byłoby: zapytanie (zapytania) zintegrowane z językiem?)
    Aby wprawić Linq2Sql w ruch, należy stworzyć odpowiedni DataContext – klasę, która będzie realizowała połączenie z bazą danych. W tym celu wysłużę się programem SqlMetal. Jest to narzędzie generujące potrzebny mi kod na podstawie pliku bazy danych. Jako że nie trawię aplikacji konsolowych, korzystam z SqlMetal Builder (interfejsu graficznego dla SqlMetal). Chwała automatyzacji, ponad trzy tysiące linii kodu napisały się same w kilka sekund.

    Repository Pattern
    Kolej na kodowanie własnoręczne. W tworzeniu warstwy dostępu do danych wykorzystam wzorzec projektowy repository. Opiera się on na klasach reprezentujących repozytorium dla każdego obiektu biznesowego. Repozytoria powinny udostępniać standardowe operacje CRUD, a także inne, właściwe dla poszczególnych obiektów biznesowych, operacje (np. selekcji).
    Tworzę zatem bazowy interfejs repozytorium:

    namespace InvoiceInvoker.Logic.RepositoryInterfaces
    {
    	public interface IRepository<T>
    	{
    		T GetById(int id);
    		void Add(T item);
    		void Remove(int itemId);
    		void Update(T item);
    	}
    }
    

    Niektóre obiekty wymagają rozszerzenia tego interfejsu. Przykładowo, faktury:

    public interface IInvoiceRepository : IRepository<Invoice>
    {
    	List<Invoice> GetAll();
    	List<Invoice> GetByStatus(string status);
    	List<Invoice> GetByDate(DateTime dateFrom, DateTime dateTo);
    	List<Invoice> GetByCustomer(string companyName);
    	List<Invoice> GetByProduct(string productName);
    	List<Invoice> GetByValue(decimal valueFrom, decimal valueTo);
    	List<Invoice> GetByExpression(Func<Invoice, bool> expression);
    }
    

    Implementacja repozytoriów
    Korzystanie z klasy DataContext rodzi pytanie: jaki powinien być czas życia pojedynczego jej obiektu? Jedna instancja na cały projekt, na jedno repozytorium, czy na jedną operację? MSDN podaje:
    In general, a DataContext instance is designed to last for one „unit of work” however your application defines that term. A DataContext is lightweight and is not expensive to create. A typical LINQ to SQL application creates DataContext instances at method scope (…)
    Mam zatem odpowiedź. Mogę przejść do implementacji poszczególnych repozytoriów. Przykładowe metody repozytorium faktur:

    public class InvoiceRepository : InvoiceInvoker.Logic.RepositoryInterfaces.IInvoiceRepository
    {
    	private int registeredSellerId;
    	private string connectionString;
    	private DataLoadOptions loadOptions;
    
    	public InvoiceRepository(int registeredSellerId)
    	{
    		this.registeredSellerId = registeredSellerId;
    
    		connectionString = ConfigurationManager.ConnectionStrings["ConnectionString"].ConnectionString;
    
    		loadOptions = new DataLoadOptions();
    		loadOptions.LoadWith<Invoice>(x => x.Customer);
    		loadOptions.LoadWith<Invoice>(x => x.Products);
    		loadOptions.LoadWith<Invoice>(x => x.Seller);
    	}
    
    	public List<Invoice> GetAll()
    	{
    		using (MainDataContext dataContext = new MainDataContext(connectionString))
    		{
    			dataContext.LoadOptions = loadOptions;
    			return dataContext.Invoices.Where(i => i.RegisteredSellerId == registeredSellerId).ToList();
    		}
    	}
    
    	// ...
    
    	public List<Invoice> GetByProduct(string productName)
    	{
    		using (MainDataContext dataContext = new MainDataContext(connectionString))
    		{
    			dataContext.LoadOptions = loadOptions;
    			var query = from invoice in dataContext.Invoices
    						where invoice.RegisteredSellerId == registeredSellerId &&
    							  invoice.Products.Any(x => x.Name.StartsWith(productName))
    						orderby invoice.CreationDate descending
    						select invoice;
    			return query.ToList();
    		}
    	}
    
    	public List<Invoice> GetByExpression(Func<Invoice, bool> expression)
    	{
    		using (MainDataContext dataContext = new MainDataContext(connectionString))
    		{
    			dataContext.LoadOptions = loadOptions;
    			var mainQuery = from invoice in dataContext.Invoices
    						where invoice.RegisteredSellerId == registeredSellerId
    						orderby invoice.CreationDate descending
    						select invoice;
    			return mainQuery.Where(expression).ToList();
    		}
    	}
    
    	// ...
    
    	public void Add(Invoice item)
    	{
    		using (MainDataContext dataContext = new MainDataContext(connectionString))
    		{
    			dataContext.Invoices.InsertOnSubmit(item);
    			dataContext.SubmitChanges();
    		}
    	}
    
    	// ...
    }
    

    Zaznaczone linie pokazaują łatwość, wygodę i swobodę, cechujące proces konstruowania zapytań z użyciem LINQ i Lambda Expressions (na szczególne podkreślenie zasługuje linia trzydziesta siódma). Tonę swobody zapewnia również metoda GetByExpression, pozwalająca uzyskać listę faktur spełniających zupełnie dowolne kryteria… Chociaż może „dowolne” to za dużo powiedziane – wszystkie metody grupowej selekcji zwracają tylko faktury przypisane do konkretnego sprzedawcy (pole registeredSellerId).
    Wspomnę jeszcze, że nie każda tabela bazy danych doczekała się własnego repozytorium. Nie przewiduję możliwości bezpośredniego grzebania patykiem w wątpiach tabel Sellers, Customers i Products (patrz poprzedni post), dlatego nie stworzyłem ich repozytoriów (choć być może w przyszłości pojawią się jakieś klasy mające dostęp do tych tabel).

    Tak oto powstała pierwsza warstwa aplikacji. Autor lirycznie odchodzi w siną dal stronę zachodzącego słońca.

    PS. Upubliczniłem wreszcie projekt na CodePlex. Dodałem też listy Done i To do na stronie projektu. Jak pokazuje lista Done, do kodowania mam większy zapał, niż do blogowania – zaległości postaram się jednak w nadchodzących dniach nadrobić, więc zapraszam do śledzenia bloga!

    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

    InvoiceInvoker: inwokacja

    Dziś początek konkursu Daj się poznać – pracę czas zacząć!

    IT, dziedzino moja, ty jesteś jak…
    …no dobra, bez przesady. Jak zapowiedziałem, startuję z programem służącym do wystawiania i przechowywania faktur (VAT). Projekt zyskał już nazwę: InvoiceInvoker (w kategorii o tej samej nazwie będę umieszczał wpisy konkursowe) i konto na CodePlex (na razie nieupublicznione). Kolejnym etapem będzie sformułowanie kilku podstawowych założeń projektowych.

    Dane
    Na początek założenia raczej bardziej ogólne, niż ściśle programistyczne: faktury powinny być zgodne z obowiązującymi przepisami, produkty – mieć przypisaną odpowiednią stawkę VAT i klasyfikację PKWiU (tym na szczęście zajmie się użytkownik), a numery NIP i REGON – być sprawdzane pod względem poprawności. Czeka mnie więc trochę lektury i kontaktu ze światem przepisów gospodarczych. Kluczowe informacje postaram się zamieszczać na blogu, aby pokazać, że programowanie to nie tylko kodowanie, ale i poszerzanie ogólnej wiedzy.
    Czas na garść założeń czysto programistycznych. Użytkownik powinien mieć możliwość zdefiniowania stałych klientów i produktów, a także szablonów faktur – tak, aby wystawienie faktury za stałe comiesięczne zamówienie wymagało trzech kliknięć LPM. Powinien też móc wybrać format numeru faktury (np. 1/8/10 lub 01-08-2010).
    Do przechowywania danych użyję Microsoft SQL Server Compact 3.5. Dobierać się do nich będę za pomocą LINQ to SQL.

    Funkcjonalności
    Na razie do głowy przychodzi mi tylko jedna funkcjonalność programu, o której warto wspomnieć, a która nie pasuje do kategorii Dane. Jest to przemiana mrocznych wnętrzności bazy danych w miły oku (przynajmniej oku wystawiającego; klienta – niekoniecznie) dokument PDF. Google w 0.18 s znajduje na to miliony sposobów, w tym 7, z których na pewno coś wybiorę.

    Technologie
    Jako że organizator konkursu zachęca do użycia w projekcie konkursowym technologii, z którymi uczestnik nie miał dotąd styczności, zdecydowałem się na skorzystanie z ASP.NET MVC. Dotychczas tworzyłem tylko programy desktopowe i od jakiegoś czasu chciałem to zmienić – teraz mam świetną okazję.
    W przeprowadzaniu testów jednostkowych pomoże mi natomiast framework NUnit.
    Niewymagający komentarza miszmasz pozostałych wybranych technologii / aplikacji / komponentów: Visual Studio 2010, LINQ to SQL, CodePlex, Windows 7, .NET 4.0, Mercurial, SQL Server Compact 3.5.

    To by było na tyle w kwestii podstawowych założeń projektu, kolejne będą się rodzić w miarę postępu prac nad nim.

    …Witryna na wciąż otwarta przechodniom ogłasza,
    Że gościnna i wszystkich w gościnę zaprasza.

    Zapraszam serdecznie do śledzenia mojego udziału w konkursie. Wszelkie sugestie – dotyczące tematyki wpisów bądź samego bloga – mile widziane!