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!
Cześć,
A co ze znanym problemem “groszowych różnic w podatku VAT” związanych z dokładnością obliczeń (sum) i dokładnością wyświetlania? Jak liczony jest sumaryczny VAT? Od Sumy netto czy sumowany jest na poszczególnych pozycjach?
Miało być: “czy sumowane są poszczególne wartości z każdej pozycji” 😉
Rzeczywiście, prosty test potwierdził podatność programu na ten problem. Równie prosta poprawka naprawiła niedociągnięcie.
Dzięki za czujność