Pandas i Excel – analiza raportu tankowań

Aktualizacja wcześniejszego mojego projektu i dodanie zmian polegających na uwzględnieniu wszystkich rodzajów paliw zatankowanych w danym okresie.

Program będzie wyświetlał na początku ilości zatankowanego paliwa z podziałem na jego rodzaje rodzaje np.

*** Zestawienie paliwa za okres od 1 do 31.10.2023 ***

1 GAZ LPG. 173.09 ltr

2 OLEJ NAPĘDOWY CN27102011. 6318.06 ltr

Wpisz numer opcji lub q aby wyjść:

Po wpisaniu wybranej opcji pojawia się zestawienie wybranego paliwa z podziałem na samochody np.

*** Zestawienie dla paliwa: OLEJ NAPĘDOWY CN27102011. ***

   Num. rej.    Ilość
1   AAA00123   722.00
2   AAA04225   192.02
3    AAA2X22    59.00
4    AAA52YT   578.03
5    AAA5P19    71.00
6    AAA5W41   841.00
7    AAA85K3  1209.01
8    AAA85YY   879.00
9    AAAFX95   826.00
10   AAAPM20    56.00
11   AAARG00   187.00
12   AAARU44   698.00

Zakupione paliwo:  6318.06 ltr

Wpisz numer rej. lub p aby powrócić do głównego menu:

Po wybraniu dowolnego fragmentu numeru rejestracyjnego, bez rozróżnienia wielkości liter pokazuje się kolejny ekran, np.

*** Tankowania dla pojazdu: AAARG00 ***

Data         Ilość        Netto        Brutto
02-11-2023   43.00 ltr    5.12           6.30
06-11-2023   42.00 ltr    5.24           6.44
07-11-2023   20.00 ltr    5.29           6.51
10-11-2023   27.00 ltr    5.20           6.40
14-11-2023   27.00 ltr    5.17           6.36
21-11-2023   28.00 ltr    5.09           6.26

 Suma tankowań dla pojazdu: 187.00 ltr

Wpisz p aby powrócić do wcześniejszego menu:

Pobrany plik programu Excel zawiera między innymi następujące kolumny: „Dane kontrahenta”, „Numer WZ”, „Data”, „Godzina”, „Licznik”, „Numer rejestracyjny”, „Numer karty”, „Nazwa towaru”, „Odbiorca” itd.

Każdy wiersz zawiera dane o konkretnym tankowaniu.

Korzystać będę tylko z danych z paru kolumn tzn. „Data”, „Numer rejestracyjny”, „Nazwa towaru” – zawierająca rodzaj pobranego paliwa, „Ilość” – ilość zatankowanego paliwa, oraz „Cena brutto” i „Cena netto”.

W tym celu tworzę klasę Reports(), która będzie zawierać wczytane raporty tankowań:

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
'''
@ author: Sławomir Kwiatkowski
@ date: 2023.11.18
'''
class Reports():
   pass

if __name__ == '__main__':
    Reports()

W metodzie __init__() dokonuję inicjalizacji dla modułu colorama, następnie metoda: load_last_report() wczytuje ostatni plik Excela zawierający najnowsze raporty tankowań, a następnie uruchomiona jest metoda main() klasy tzn.

def __init__(self):
    colorama.init(autoreset=True)
    last_report = self.load_last_report()
    self.main(last_report)

Metoda load_last_report() pobiera najnowszy plik excela z podkatalogu 'Dane’ i zwraca ramkę Pandas tzn.

def load_last_report(self):
    current_dir = os.path.dirname(os.path.abspath(__file__))
    data_dir = os.path.join(current_dir, 'Dane')
    reports = glob.glob(os.path.join(data_dir, '*.xlsx'))
    last_report = max(reports, key=os.path.getctime)
    path_to_last_report = os.path.join(data_dir, last_report)
    return  pd.read_excel(path_to_last_report)

Należy uzupełnić wymagane importy tzn.

import os
import glob
import colorama
import pandas as pd

Wydzieliłem prywatną metodę do „czyszczenia” konsoli tzn.

def _clear_screen(self):
    os.system('cls' if os.name == 'nt' else 'clear')

W metodzie main() klasy Reports wyświetlam krótki nagłówek copyright, a następnie wypisuję zsumowane paliwo, w zależności od rodzaju paliwa tzn.

    def main(self, report):
        self._clear_screen()
        copyright = {'author:': 'Sławomir Kwiatkowski', 'Date:': '18-11-2023' }
        for item in copyright:
            print(f'{colorama.Fore.RED}  {item} {copyright[item]}'.rjust(120)) 
        self.main_header(report['Data'].min(), report['Data'].max())
        fuel_grp = report.groupby('Nazwa towaru')
        fuel_grp_sums = fuel_grp['Ilość'].sum()
        for i, sum in enumerate(fuel_grp_sums.items(), start=1):
            print(i, sum[0], f'{sum[1]:.2f}',  'ltr', '\n')

W metodzie main() następnie wczytuję opcję wybraną przez użytkownika tzn. numer wybranego paliwa, dla którego wyświetlić szczegółowe dane lub „q” aby wyjść z programu tzn.

    while True:
        choice = input('Wpisz numer opcji lub q aby wyjść: ')
        if choice == 'q': 
            self._clear_screen()
            raise SystemExit
        if choice.isnumeric() and choice!='0':
            try:
                fuel= fuel_grp_sums.index[int(choice)-1]
                self.fuel_description(report, fuel)
                break
            except:
                pass
        self.main(report)

Metoda main() zawiera też wywołanie nagłówka tzn. main_header(), który wyświetla napis o zakresie dat, z którego pochodzą dane tankowań tzn. data pierwszego tankowania i data ostatniego tankowania:

def main_header(self, start_date, end_date):
    print(colorama.Fore.YELLOW +'*** Zestawienie paliwa za okres od {} do {}.{}.{} ***\n'
          .format(start_date.day,
                  end_date.day,
                  end_date.month,
                  end_date.year
                  ))

Jeśli użytkownik wybierze numer paliwa, dla którego chce wyświetlić szczegółowy opis, to wyświetla się kolejny ekran, zawierające dane o sumach tankowań wybranego paliwa dla poszczególnych pojazdów tzn.

def fuel_description(self, report, fuel):
    self._clear_screen()
    print(colorama.Fore.YELLOW + f'*** Zestawienie dla paliwa: {fuel} ***\n')
    filt = report['Nazwa towaru'] == fuel
    cars_group = report[filt].groupby('Numer rejestracyjny')
    cars_group_sums = cars_group['Ilość'].sum().reset_index()
    cars_group_sums.index +=1
    cars_group_sums.rename(columns={'Numer rejestracyjny': 'Num. rej.'}, inplace=True)
    print(cars_group_sums, '\n')
    print('Zakupione paliwo: ', cars_group_sums.sum()[1], 'ltr \n')

Metoda ta zawiera również możliwość wybrania fragmentu numeru rejestracyjnego, aby wyświetlić szczegóły tankowań danego paliwa dla wybranego auta lub przejście do wcześniejszego ekranu po wciśnięciu „p”.

    while True:
        choice = input('Wpisz numer rej. lub p aby powrócić do głównego menu: ')
        if choice == 'p': 
            break
        for key, value in cars_group['Ilość']:
            if choice.upper() in key:
                self.car_description(key, value, report, fuel)
                break
        self.fuel_description(report, fuel)    
    self.main()

Po wybraniu dowolnego fragmentu numeru rejestracyjnego, metoda car_description() wyświetla szczegółowe dane dla danego auta tzn. datę tankowania, ilość zatankowanego paliwa oraz cenę netto i brutto tzn.

def car_description(self, key, value, report, fuel):
    self._clear_screen()
    print(colorama.Fore.YELLOW + f'*** Tankowania dla pojazdu: {key} ***\n')
    header = ['Data', 'Ilość', 'Netto', 'Brutto']
    for item in header:
        print(f'{item:<13}', end="")
    print()
    total = 0
    line = 1
    for i, v in zip(value.index, value.values):
        output = f'{report.loc[i, "Data"]:%d-%m-%Y}' \
                    f'{v:>8.2f} ltr' \
                    f'{report.loc[i, "Cena netto"]:>8.2f}' \
                    f'{report.loc[i, "Cena brutto"]:>15.2f}'
        total += v
        print(colorama.Fore.BLACK + colorama.Back.WHITE + output
              ) if line % 2 else print(output)
        line += 1
    print(f'\n Suma tankowań dla pojazdu: {total:.2f} ltr \n')
            
    while True:
        choice = input('Wpisz p aby powrócić do wcześniejszego menu: ')
        if choice == 'p': 
            break
        self.car_description(key, value, report, fuel)
    self.fuel_description(report, fuel)