mirror of
https://github.com/Noettore/fattureCCSR.git
synced 2025-10-14 19:26:39 +02:00
fattureCCSR: added logging dialog to show action status
Signed-off-by: Ettore Dreucci <ettore.dreucci@gmail.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""ask for an input file (.xlsx) and an output file (.pdf) and downloads and unite every invoice"""
|
||||
|
39
exc.py
39
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)
|
||||
|
148
fatture_ccsr.py
148
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()
|
||||
|
19
logger.py
Normal file
19
logger.py
Normal file
@@ -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)
|
@@ -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"])
|
16
utils.py
Normal file
16
utils.py
Normal file
@@ -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
|
Reference in New Issue
Block a user