diff --git a/downloader.py b/downloader.py index e69de29..b8177a2 100644 --- a/downloader.py +++ b/downloader.py @@ -0,0 +1 @@ +"""ask for an input file (.xlsx) and an output file (.pdf) and downloads and unite every invoice""" diff --git a/exc.py b/exc.py index 86d9999..e6ee437 100644 --- a/exc.py +++ b/exc.py @@ -1,7 +1,42 @@ """Define Python user-defined exceptions""" -class Error(Exception): +class FattureSanRossoreError(Exception): """Base class for other exceptions""" -class WrongFileExtension(Error): + +class FileError(FattureSanRossoreError): + """Basic exception for errors raised by files""" + def __init__(self, file_path, msg=None): + if msg is None: + msg = "An error occurred with file %s" % file_path + super(FileError, self).__init__(msg) + self.file_path = file_path + +class NoFileExtensionError(FileError): + """Raised when a file has no exception""" + def __init__(self, file_path): + super(NoFileExtensionError, self).__init__(file_path, msg="File %s has no extension!" % file_path) +class WrongFileExtensionError(FileError): """Raised when a file extension is not accepted""" + def __init__(self, file_path, file_ext, allowed_ext): + super(WrongFileExtensionError, self).__init__(file_path, msg="Cannot accept file %s extension %s. Allowed extensions are %s" % (file_path, file_ext, allowed_ext)) + self.file_ext = file_ext + +class NoFileError(FileError): + """Raised when file_path is None or an empty string""" + def __init__(self): + super(NoFileError, self).__init__(None, msg="Not setted or empty file path!") + + +class ActionError(FattureSanRossoreError): + """Basic exception for errors raised by actions""" + def __init__(self, action, msg=None): + if msg is None: + msg = "An error occurred with %s action" % action + super(ActionError, self).__init__(msg) + self.action = action + +class InvalidActionError(ActionError): + """Raised when an invalid action is used""" + def __init__(self, action): + super(InvalidActionError, self).__init__(action, "Invalid action %s" % action) diff --git a/fatture_ccsr.py b/fatture_ccsr.py index f7a16f0..2dbd4e0 100644 --- a/fatture_ccsr.py +++ b/fatture_ccsr.py @@ -1,42 +1,91 @@ """This utility is used for downloading or converting to TRAF2000 invoices from a .csv or .xml report file""" -import os +import logging import wx -import converter +import traf2000_converter import exc +import utils +import logger + +DOWNLOAD_ACTION = 1 +CONVERT_ACTION = 2 + +class LogHandler(logging.StreamHandler): + """logging stream handler""" + def __init__(self, textctrl): + logging.StreamHandler.__init__(self) + self.textctrl = textctrl + + def emit(self, record): + """constructor""" + msg = self.format(record) + self.textctrl.WriteText(msg + "\n") + self.flush() + +class LogDialog(wx.Dialog): + """logging panel""" + def __init__(self, parent, title, action): + super(LogDialog, self).__init__(parent, wx.ID_ANY, title) + if action == DOWNLOAD_ACTION: + self.logger = logger.downloader_logger + elif action == CONVERT_ACTION: + self.logger = logger.converter_logger + else: + raise exc.InvalidActionError(action) + + log_text = wx.TextCtrl(self, wx.ID_ANY, size=(300, 200), style=wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL) + log_handler = LogHandler(log_text) + log_handler.setLevel(logging.INFO) + self.logger.addHandler(log_handler) + + self.btn = wx.Button(self, wx.ID_OK, "Chiudi") + self.btn.Disable() + + sizer = wx.BoxSizer(wx.HORIZONTAL) + sizer.Add(log_text, 0, wx.ALL, 2) + + if action == CONVERT_ACTION: + self.nc_logger = logger.note_credito_logger + nc_text = wx.TextCtrl(self, wx.ID_ANY, size=(300, 200), style=wx.TE_MULTILINE|wx.TE_READONLY|wx.HSCROLL) + nc_handler = LogHandler(nc_text) + self.nc_logger.addHandler(nc_handler) + + sizer.Add(nc_text, 0, wx.ALL, 2) + + self.SetSizerAndFit(sizer) class LoginDialog(wx.Dialog): """login dialog for basic auth download""" def __init__(self, parent, title): - """constructor""" - super(LoginDialog, self).__init__(parent, wx.ID_ANY, title, size=(350, 150)) + """constructor""" + super(LoginDialog, self).__init__(parent, wx.ID_ANY, title) self.logged_id = False - + user_sizer = wx.BoxSizer(wx.HORIZONTAL) user_lbl = wx.StaticText(self, wx.ID_ANY, "Username:") user_sizer.Add(user_lbl, 0, wx.ALL|wx.CENTER, 2) - self.username = wx.TextCtrl(self) + self.username = wx.TextCtrl(self, size=(265, -1)) user_sizer.Add(self.username, 0, wx.ALL, 2) pass_sizer = wx.BoxSizer(wx.HORIZONTAL) pass_lbl = wx.StaticText(self, wx.ID_ANY, "Password:") pass_sizer.Add(pass_lbl, 0, wx.ALL|wx.CENTER, 2) - self.password = wx.TextCtrl(self, wx.ID_ANY, style=wx.TE_PASSWORD|wx.TE_PROCESS_ENTER) + self.password = wx.TextCtrl(self, size=(265, -1), style=wx.TE_PASSWORD|wx.TE_PROCESS_ENTER) pass_sizer.Add(self.password, 0, wx.ALL, 2) login_btn = wx.Button(self, label="Login") - login_btn.Bind(wx.EVT_BUTTON, self.onLogin) + login_btn.Bind(wx.EVT_BUTTON, self.on_login) main_sizer = wx.BoxSizer(wx.VERTICAL) main_sizer.Add(user_sizer, 0, wx.ALL, 2) main_sizer.Add(pass_sizer, 0, wx.ALL, 2) main_sizer.Add(login_btn, 0, wx.ALL|wx.CENTER, 2) - self.SetSizer(main_sizer) + self.SetSizerAndFit(main_sizer) - def onLogin(self, _): + def on_login(self, _): """check credentials and login""" if self.username not in ("", None) and self.password.GetValue() not in ("", None): self.logged_id = True @@ -45,74 +94,89 @@ class LoginDialog(wx.Dialog): class FattureCCSRFrame(wx.Frame): """main application frame""" def __init__(self, parent, title): + self._initial_locale = wx.Locale(wx.LANGUAGE_DEFAULT, wx.LOCALE_LOAD_DEFAULT) + self.input_file_path = None self.input_file_ext = None self.output_file_path = None + self.log_dialog = None - super(FattureCCSRFrame, self).__init__(parent, wx.ID_ANY, title, size=(500, 120)) + super(FattureCCSRFrame, self).__init__(parent, wx.ID_ANY, title, size=(650, 150)) panel = wx.Panel(self) - vbox = wx.BoxSizer(wx.VERTICAL) - hbox1 = wx.BoxSizer(wx.HORIZONTAL) - hbox2 = wx.BoxSizer(wx.HORIZONTAL) + main_sizer = wx.BoxSizer(wx.VERTICAL) + input_sizer = wx.BoxSizer(wx.HORIZONTAL) + btn_sizer = wx.BoxSizer(wx.HORIZONTAL) - input_file_text = wx.StaticText(panel, wx.ID_ANY, "Seleziona il file scaricato dal portare della CCSR") + input_file_text = wx.StaticText(panel, wx.ID_ANY, "Seleziona il file scaricato dal portale della CCSR") + input_file_text.SetFont(wx.Font(10, wx.FONTFAMILY_DEFAULT, wx.FONTSTYLE_NORMAL, wx.FONTWEIGHT_BOLD)) - self.input_file_picker = wx.FilePickerCtrl(panel, 101, "", "Seleziona il .xlsx, .csv o .xml", "*.xlsx;*.csv;*.xml", style=wx.FLP_DEFAULT_STYLE|wx.FLP_USE_TEXTCTRL) - hbox1.Add(self.input_file_picker, 1, wx.EXPAND, 0) + input_file_doc = wx.StaticText(panel, wx.ID_ANY, "Per scaricare le fatture selezionare il file .xlsx mentre per convertire i record in TRAF2000 selezionare il file .xml o .csv") + + self.input_file_picker = wx.FilePickerCtrl(panel, 101, "", "Seleziona il .xlsx, .csv o .xml", "*.xlsx;*.csv;*.xml", style=wx.FLP_DEFAULT_STYLE) + input_sizer.Add(self.input_file_picker, 1, wx.ALL|wx.EXPAND, 2) self.input_file_picker.Bind(wx.EVT_FILEPICKER_CHANGED, self.file_picker_changed) - self.download_btn = wx.Button(panel, 201, "Scarica Fatture") - hbox2.Add(self.download_btn, 0, wx.ALIGN_CENTER) + self.download_btn = wx.Button(panel, DOWNLOAD_ACTION, "Scarica Fatture") + btn_sizer.Add(self.download_btn, 0, wx.ALL|wx.CENTER, 2) self.download_btn.Bind(wx.EVT_BUTTON, self.btn_onclick) self.download_btn.Disable() - self.traf2000_btn = wx.Button(panel, 202, "Genera TRAF2000") - hbox2.Add(self.traf2000_btn, 0, wx.ALIGN_CENTER) + self.traf2000_btn = wx.Button(panel, CONVERT_ACTION, "Genera TRAF2000") + btn_sizer.Add(self.traf2000_btn, 0, wx.ALL|wx.CENTER, 2) self.traf2000_btn.Bind(wx.EVT_BUTTON, self.btn_onclick) self.traf2000_btn.Disable() - self.output_file_dialog = wx.FileDialog(panel, "Scegli dove salvare il file TRAF2000", defaultFile="TRAF2000", style=wx.FD_SAVE | wx.FD_OVERWRITE_PROMPT) + self.login_dlg = LoginDialog(self, "Inserisci le credenziali di login al portale della CCSR") + self.output_file_dialog = wx.FileDialog(panel, "Scegli dove salvare il file TRAF2000", defaultFile="TRAF2000", style=wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT) - vbox.Add(input_file_text, 0, wx.ALIGN_CENTER_HORIZONTAL) - vbox.Add(hbox1, 0, wx.EXPAND) - vbox.Add(hbox2, 0, wx.ALIGN_CENTER_HORIZONTAL) + main_sizer.Add(input_file_text, 0, wx.ALL|wx.CENTER, 2) + main_sizer.Add(input_file_doc, 0, wx.ALL|wx.CENTER, 2) + main_sizer.Add(input_sizer, 0, wx.ALL|wx.EXPAND, 2) + main_sizer.Add(btn_sizer, 0, wx.ALL|wx.CENTER, 2) - panel.SetSizer(vbox) + panel.SetSizer(main_sizer) - self.Centre() self.Show() def file_picker_changed(self, event): """event raised when the input file path is changed""" self.input_file_path = event.GetEventObject().GetPath() - if self.input_file_path in (None, ""): - print("ERROR: No input file selected!") - return - self.input_file_ext = os.path.splitext(self.input_file_path)[1] + try: + self.input_file_ext = utils.file_extension(self.input_file_path, (".xml", ".csv", ".xlsx")) + except exc.NoFileError as handled_exception: + print(handled_exception.args[0]) + except exc.NoFileExtensionError as handled_exception: + print(handled_exception.args[0]) + except exc.WrongFileExtensionError as handled_exception: + print(handled_exception.args[0]) if self.input_file_ext == ".xlsx": self.download_btn.Enable() elif self.input_file_ext in (".csv", ".xml"): self.traf2000_btn.Enable() else: - print("ERROR: wrong file extension: " + self.input_file_ext) - return - + self.traf2000_btn.Disable() def btn_onclick(self, event): """event raised when a button is clicked""" btn_id = event.GetEventObject().GetId() - if btn_id == 201: - login_dlg = LoginDialog(self, "Inserisci le credenziali di login al portale della CCSR") - login_dlg.ShowModal() - if login_dlg.logged_id: + if btn_id == DOWNLOAD_ACTION: + self.login_dlg.ShowModal() + if self.login_dlg.logged_id: print("Download") - elif btn_id == 202: + elif btn_id == CONVERT_ACTION: if self.output_file_dialog.ShowModal() == wx.ID_OK: self.output_file_path = self.output_file_dialog.GetPath() + self.log_dialog = LogDialog(self, "Log", CONVERT_ACTION) + self.log_dialog.Show() try: - converter.convert(self.input_file_path, self.output_file_path) - except exc.WrongFileExtension as e: - print(e.args[0]) + traf2000_converter.convert(self.input_file_path, self.output_file_path) + except exc.NoFileError as handled_exception: + print(handled_exception.args[0]) + except exc.NoFileExtensionError as handled_exception: + print(handled_exception.args[0]) + except exc.WrongFileExtensionError as handled_exception: + print(handled_exception.args[0]) + self.log_dialog.btn.Enable() if __name__ == "__main__": app = wx.App() diff --git a/logger.py b/logger.py new file mode 100644 index 0000000..6d3d5bd --- /dev/null +++ b/logger.py @@ -0,0 +1,19 @@ +"""Create logging handler""" +import logging + +#_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + +downloader_logger = logging.getLogger("downloader") +downloader_logger.setLevel(logging.DEBUG) + +converter_logger = logging.getLogger("converter") +converter_logger.setLevel(logging.DEBUG) +#_converter_ch = logging.StreamHandler() +#_converter_ch.setFormatter(_formatter) +#converter_logger.addHandler(_converter_ch) + +note_credito_logger = logging.getLogger("note_credito") +note_credito_logger.setLevel(logging.INFO) +#_note_credito_ch = logging.StreamHandler() +#_note_credito_ch.setFormatter(_formatter) +#note_credito_logger.addHandler(_note_credito_ch) diff --git a/converter.py b/traf2000_converter.py similarity index 92% rename from converter.py rename to traf2000_converter.py index e3825bf..a6cf110 100755 --- a/converter.py +++ b/traf2000_converter.py @@ -1,14 +1,14 @@ """ask for an input file and an output file and generates the TRAF2000 records from a .csv or .xml""" -import os import datetime import csv import xml.etree.ElementTree import unidecode -import exc +import utils +import logger -def import_csv(csv_file_path): +def import_csv(csv_file_path: str) -> dict: """Return a dict containing the invoices info""" fatture = dict() with open(csv_file_path, newline="") as csv_file: @@ -48,9 +48,10 @@ def import_csv(csv_file_path): else: fatture[num_fattura]["importoTotale"] += importo fatture[num_fattura]["righe"][linea[14]] = importo + logger.converter_logger.info("Importata fattura n. %s", num_fattura) return fatture -def import_xml(xml_file_path): +def import_xml(xml_file_path: str) -> dict: """Return a dict containing the invoices info""" fatture = dict() @@ -89,36 +90,35 @@ def import_xml(xml_file_path): "righe": righe, } fatture[num_fattura] = fattura_elem + logger.converter_logger.info("Importata fattura n. %s", num_fattura) return fatture -def convert(input_file_path, out_file_path): +def convert(input_file_path: str, out_file_path: str): """Output to a file the TRAF2000 records""" - input_file_ext = os.path.splitext(input_file_path)[1] + + input_file_ext = utils.file_extension(input_file_path, (".xml", ".csv")) if input_file_ext == ".csv": fatture = import_csv(input_file_path) elif input_file_ext == ".xml": fatture = import_xml(input_file_path) - else: - raise exc.WrongFileExtension("Expected .csv or .xml but received " + input_file_ext) - with open(out_file_path, "w") as traf2000_file: - print("Note di credito:\n") + logger.note_credito_logger.info("Note di credito:") for fattura in fatture.values(): if fattura["tipoFattura"] != "Fattura" and fattura["tipoFattura"] != "Nota di credito": - print("Errore: il documento " + fattura["numFattura"] + " può essere FATTURA o NOTA DI CREDITO") + logger.converter_logger.error("Errore: il documento %s può essere FATTURA o NOTA DI CREDITO", fattura["numFattura"]) continue if len(fattura["cf"]) != 16 and len(fattura["cf"]) == 11: - print("Errore: il documento " + fattura["numFattura"] + " non ha cf/piva") + logger.converter_logger.error("Errore: il documento %s non ha cf/piva", fattura["numFattura"]) continue if fattura["tipoFattura"] == "Nota di credito": # As for now this script doesn't handle "Note di credito" - print(fattura["numFattura"]) + logger.note_credito_logger.info(fattura["numFattura"]) continue linea = ["04103", "3", "0", "00000"] # TRF-DITTA + TRF-VERSIONE + TRF-TARC + TRF-COD-CLIFOR @@ -236,6 +236,7 @@ def convert(input_file_path, out_file_path): linea.append('S') # TRF-RIF-FATTURA linea.append('S' + ' '*2 + 'S' + ' '*2) # TRF-RISERVATO-B + TRF-MASTRO-CF + TRF-MOV-PRIVATO + TRF-SPESE-MEDICHE + TRF-FILLER linea.append('\n') + logger.converter_logger.info("Creato record #0 per fattura n. %s", fattura["numFattura"]) #RECORD 5 per Tessera Sanitaria linea.append('04103' + '3' + '5') # TRF5-DITTA + TRF5-VERSIONE + TRF5-TARC @@ -255,6 +256,7 @@ def convert(input_file_path, out_file_path): linea.append((' ' + ' ' + ' ')*49) # TRF-A21CO-TIPO + TRF-A21CO-TIPO-SPESA + TRF-A21CO-FLAG-SPESA linea.append(' ' + 'S' + ' '*76) # TRF-SPESE-FUNEBRI + TRF-A21CO-PAGAM + FILLER + FILLER linea.append('\n') + logger.converter_logger.info("Creato record #5 per fattura n. %s", fattura["numFattura"]) #RECORD 1 per num. doc. originale linea.append('04103' + '3' + '1') # TRF1-DITTA + TRF1-VERSIONE + TRF1-TARC @@ -273,7 +275,9 @@ def convert(input_file_path, out_file_path): linea.append(' '*8) # TRF-CK-RCHARGE linea.append('0'*(15-len(fattura["numFattura"])) + fattura["numFattura"]) # TRF-XNUM-DOC-ORI linea.append(' ' + '00' + ' '*1090) # TRF-MEM-ESIGIB-IVA + TRF-COD-IDENTIFICATIVO + TRF-ID-IMPORTAZIONE + TRF-XNUM-DOC-ORI-20 + SPAZIO + FILLER + logger.converter_logger.info("Creato record #1 per fattura n. %s", fattura["numFattura"]) linea = ''.join(linea) + '\n' traf2000_file.write(linea) + logger.converter_logger.info("Convertita fattura n. %s", fattura["numFattura"]) diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..bddd732 --- /dev/null +++ b/utils.py @@ -0,0 +1,16 @@ +"""Some useful utilities""" + +import os + +import exc + +def file_extension(file_path: str, allowed_ext: set = None) -> str: + """Return the file extension if that's in the allowed extension set""" + if file_path in (None, ""): + raise exc.NoFileError() + file_ext = os.path.splitext(file_path)[1] + if file_ext in (None, ""): + raise exc.NoFileExtensionError + if allowed_ext is not None and file_ext not in allowed_ext: + raise exc.WrongFileExtensionError + return file_ext