diff --git a/App_State.py b/App_State.py index b34b83c..497949a 100644 --- a/App_State.py +++ b/App_State.py @@ -1,14 +1,27 @@ +from Log_View import Log_View, Log_Handler import schema +import logging class App_State : user_id = None + imos_done_dir = "IMOS-Done" + imos_order_dir = "ImOrder" + sim_batches = "Batches" + pgiq_dir = "OrderXML" + L = logging.getLogger () + Action_Kind = \ { 1 : "Create Order" + , 2 : "Update Order" , 10 : "Change owner" + , 15 : "Force order state" , 20 : "IMOS CAD started" , 25 : "IMOS CAD closed" + , 30 : "CNC generation started" + , 35 : "CNC generation finished" + , 40 : "Odoo Freigabe" } @classmethod def Create_Action (cls, kind, text, order = None) : @@ -23,4 +36,27 @@ class App_State : ) # end def create_action + @classmethod + def Log_View (cls, level = logging.INFO, parent = None) : + cls.L.info ("Log window created") + cls._log_view = Log_View (parent) + lh = Log_Handler (cls._log_view) + #lh.setFormatter \ + # ( logging.Formatter + # ('%(asctime)s ' + # '%(levelname)s - %(message)s' + # , datefmt = "%Y-%m-%d %H:%M:%S" + # ) + # ) + lh.setLevel (level) + lh.setFormatter \ + ( logging.Formatter + ( "%(asctime)s - %(levelname)s - %(message)s" + , datefmt = "%Y-%m-%d %H:%M:%S" + ) + ) + cls.L.addHandler (lh) + return cls._log_view + # end def Log_View + # end class App_State diff --git a/Log_View.py b/Log_View.py new file mode 100644 index 0000000..e04c832 --- /dev/null +++ b/Log_View.py @@ -0,0 +1,40 @@ +from QT import QtWidgets, QtCore, QT +import logging + +class Log_Handler (logging.Handler) : + + def __init__(self, widget) : + super ().__init__ () + self._widget = widget + # end __init__ + + def emit (self, record) : + msg = self.format (record) + self._widget.append_log.emit (msg) + # end def emit + +# end class Log_Handler + +class Log_View (QtWidgets.QTextEdit) : + + append_log = QtCore.Signal (str) + + def __init__ (self, * args, ** kw) : + super ().__init__ (* args, ** kw) + self.setReadOnly (True) + self.append_log.connect (self.append) + # end def __init__ + + def save_settings (self, settings) : + settings.beginGroup ("log-view") + settings.setValue ("geometry", self.saveGeometry ()) + settings.endGroup () + # end def save_settings + + def restore_settings (self, settings) : + settings.beginGroup ("log-view") + self.restoreGeometry (settings.value ("geometry" )) + settings.endGroup () + # end def restore_settings + +# end class Log_View \ No newline at end of file diff --git a/Order_Display.py b/Order_Display.py index 4932d0b..ef319ce 100644 --- a/Order_Display.py +++ b/Order_Display.py @@ -6,6 +6,7 @@ import schema import subprocess import threading import time +import pathlib class Order_List (_With_Table_Widget_) : @@ -18,6 +19,7 @@ class Order_List (_With_Table_Widget_) : { 0 : "neu" , 10 : "in Bearbeitung" , 20 : "im CAD geöffnet" + , 25 : "CNC Erzeugung läuft" , 90 : "Odoo" , 95 : "Zurückgesetzt" , 99 : "Storno" @@ -31,14 +33,16 @@ class Order_List (_With_Table_Widget_) : self._recreate_cnc = ACT (self.tr ("CNC Programm neu erzeugen")) self._reset_order = ACT (self.tr ("Auftrag zurücksetzen")) self._cancel = ACT (self.tr ("Auftrag stronieren")) - self._send_to_odoo = ACT (self.tr ("Auftrag ins Odoo schicken")) + self._send_to_odoo = ACT (self.tr ("Auftrag ans Odoo schicken")) self._actions = ACT (self.tr ("Aktionsliste")) + self._reset_state = ACT (self.tr ("Status zurücksetzen")) self._open_cad.triggered.connect (self._open_order_in_cad) self._recreate_cnc.triggered.connect (self._start_recreate_cnc) self._reset_order.triggered.connect (self._reset_order_in_webshop) self._cancel.triggered.connect (self._cancel_order) self._send_to_odoo.triggered.connect (self._send_oder_to_odoo) self._actions.triggered.connect (self._show_actions) + self._reset_state.triggered.connect (self._reset_order_state) self._cad_processes = {} # end def __init__ @@ -47,7 +51,7 @@ class Order_List (_With_Table_Widget_) : if row_data : bno = row_data [0].text () with schema.orm.db_session () : - order = schema.Order.get (basket_no = bno) + order = schema.Order.get (basket_no = bno, active = True) if order.user and order.user.id != App_State.user_id : r = QtWidgets.QMessageBox.question \ ( self, self.tr ("Fehler") @@ -67,8 +71,8 @@ class Order_List (_With_Table_Widget_) : order.user = schema.User [App_State.user_id] self.update_order (order, False, row_data [0].row ()) state = order.state + cm = QtWidgets.QMenu () if state < 20 : - cm = QtWidgets.QMenu () cm.addAction (self._open_cad) cm.addAction (self._recreate_cnc) cm.addSeparator () @@ -76,34 +80,52 @@ class Order_List (_With_Table_Widget_) : cm.addSeparator () cm.addAction (self._reset_order) cm.addAction (self._cancel) - cm.addSeparator () - cm.addAction (self._actions) - cm.exec (self._view.mapToGlobal (pos)) + cm.addSeparator () + cm.addAction (self._actions) + cm.addSeparator () + cm.addAction (self._reset_state) + cm.exec (self._view.mapToGlobal (pos)) # end def _show_context_menu def update_order (self, order, new, row = None) : - if new : - row = self._view.rowCount () - self._view.setRowCount (row + 1) - self._add_read_only_string (row, 0, str (order.basket_no)) - self._add_read_only_string (row, 1, self.State_Map [order.state]) - if order.user : - self._add_read_only_string (row, 2, order.user.username) - self._add_read_only_string (row, 3, order.commission) - self._add_read_only_string (row, 4, order.imos_user) - self._add_read_only_string (row, 5, str (order.production_line)) + if order.active : + state = self.State_Map [order.state] + if new : + row = self._view.rowCount () + self._view.setRowCount (row + 1) + self._add_read_only_string (row, 0, str (order.basket_no)) + self._add_read_only_string (row, 1, state) + if order.user : + self._add_read_only_string (row, 2, order.user.username) + self._add_read_only_string (row, 3, order.commission) + self._add_read_only_string (row, 4, order.imos_user) + self._add_read_only_string (row, 5, str (order.production_line)) + else : + if row is None : + for row in range (self._view.rowCount ()) : + if self._view.item (row, 0).text () == order.basket_no : + break + else : + App_State.L.error \ + ("Could not find row for order %s", order.basket_no) + return + self._update_read_only_string (row, 1, state) + self._update_read_only_string (row, 2, order.user.username) else : - if row is None : - for row in range (self._view.rowCount ()) : - if self._view.item (row, 0).text () == order.basket_no : - break - else : - print ("Could not find order") - return - self._update_read_only_string (row, 1, self.State_Map [order.state]) - self._update_read_only_string (row, 2, order.user.username) + for row in range (self._view.rowCount ()) : + if self._view.item (row, 0).text () == order.basket_no : + self._view.removeRow (row) + return order.basket_no # end def update_order + def remove_orders (self, orders) : + for bno in orders : + for row in range (self._view.rowCount ()) : + if self._view.item (row, 0).text () == bno : + self._view.removeRow (row) + break + # end def remove_orders + def _open_order_in_cad (self) : row_data = self._view.selectedItems () if row_data : @@ -114,10 +136,10 @@ class Order_List (_With_Table_Widget_) : if order.state < 20 : order.state = 20 self.update_order (order, False, row_data [0].row ()) - self._cad_processes [bno] = subprocess.Popen \ - ( (r"%s\%s\%s.dwg" % (state.imos_order_dir, bno, bno), ) - , shell = True - ) + order_dwg = r"%s\ImOrder\%s\%s.dwg" \ + % (state.imos_factory_root, bno, bno) + self._cad_processes [bno] = p = subprocess.Popen \ + ( ( order_dwg, ), shell = True ) threading.Thread \ ( target = self._wait_for_cad_close , args = (bno, ) @@ -141,7 +163,7 @@ class Order_List (_With_Table_Widget_) : self.update_order (order, False) App_State.Create_Action \ ( 25 - , f"IMOS CAD closed {order.basket_no}" + , f"IMOS CAD closed {order.basket_no}: Error code {p.poll ()}" , order ) # end def _wait_for_cad_close @@ -150,28 +172,100 @@ class Order_List (_With_Table_Widget_) : row_data = self._view.selectedItems () if row_data : bno = row_data [0].text () - print (bno) + xml = f""" + + + + {bno} + STANDARD + IMOSADMIN + + + +""" + with schema.orm.db_session () : + state = schema.State.get () + order = schema.Order.get (basket_no = bno) + job_file_name = ( pathlib.Path (state.imos_sim_root) + / App_State.sim_batches + / f"{bno}.xml" + ) + txt_file = ( pathlib.Path (state.imos_factory_root) + / App_State.imos_done_dir + / f"{bno}.txt" + ) + order.state = 25 + with job_file_name.open ("w") as f : + f.write (xml) + if txt_file.exists () : + txt_file.unlink () + self.update_order (order, False, row_data [0].row ()) + threading.Thread \ + ( target = self._wait_for_cnc_done + , args = (bno, ) + , daemon = True + ).start () + App_State.Create_Action \ + ( 30, f"Start CNC generation for {bno}", order) # end def _start_recreate_cnc + def _wait_for_cnc_done (self, basket_no) : + with schema.orm.db_session () : + state = schema.State.get () + txt_file = ( pathlib.Path (state.imos_factory_root) + / App_State.imos_done_dir + / f"{basket_no}.txt" + ) + err_file_name = ( pathlib.Path (state.imos_sim_root) + / App_State.sim_batches + / "Error" + / f"{basket_no}.xml" + ) + while not txt_file.exists () and not err_file_name.exists (): + time.sleep (0.5) + App_State.L.debug (f"wait for {txt_file}/{err_file_name}") + if err_file_name.exists () : + text = f"Fehler bei der CNC Erzeugung for {basket_no}" + else : + text = f"CNC Erzeugung für {basket_no} abgeschlossen" + with schema.orm.db_session () : + order = schema.Order.get (basket_no = basket_no) + order.state = 10 + self.update_order (order, False) + App_State.Create_Action (35, text, order) + # end def _wait_for_cnc_done + def _reset_order_in_webshop (self) : row_data = self._view.selectedItems () if row_data : bno = row_data [0].text () - print (bno) + App_State.L.error ("Reset order in webshop %s", bno) # end def _reset_order_in_webshop def _cancel_order (self) : row_data = self._view.selectedItems () if row_data : bno = row_data [0].text () - print (bno) + App_State.L.error ("Cancel Order %s", bno) # end def _cancel_order def _send_oder_to_odoo (self) : row_data = self._view.selectedItems () if row_data : bno = row_data [0].text () - print (bno) + with schema.orm.db_session () : + state = schema.State.get () + order = schema.Order.get (basket_no = bno) + factory = pathlib.Path (state.imos_factory_root) + for ext in ".xml", ".txt" : + sf = factory / App_State.imos_done_dir / f"{bno}{ext}" + df = factory / App_State.pgiq_dir / f"{bno}{ext}" + sf.rename (df) + App_State.L.info ("Auftrag %s für Odoo freigegeben", bno) + App_State.Create_Action \ + (40, f"Auftrag {bno} für Odoo freigegeben", order) + order.active = False + self.update_order (order, False) # end def _send_oder_to_odoo def _show_actions (self) : @@ -184,6 +278,30 @@ class Order_List (_With_Table_Widget_) : (order.actions.order_by (schema.orm.desc (schema.Action.date))).exec () # end def _show_actions + def _reset_order_state (self) : + row_data = self._view.selectedItems () + if row_data : + bno = row_data [0].text () + r = QtWidgets.QMessageBox.question \ + ( self, self.tr ("Fehler") + , self.tr + ( "Soll der Status wirklich auf In Bearbeitung " + "gesetzt werden?" + ) + ) + if r == QtWidgets.QMessageBox.No : + return + with schema.orm.db_session () : + order = schema.Order.get (basket_no = bno) + order.state = 10 + self.update_order (order, False) + App_State.Create_Action \ + ( 15 + , f"Status wird auf 10 gesetzt" + , order + ) + # end def _reset_order_state + # end class Order_List class Order_Detail (_With_Table_Widget_) : @@ -207,8 +325,9 @@ class Order_Display (QtWidgets.QSplitter) : # end def __init__ def update_orders (self) : + orders = set (self.orders.keys ()) with schema.orm.db_session : - for o in schema.Order.select () : + for o in schema.Order.select (active = True) : db_lc = o.actions.order_by (schema.Action.date).first ().date if o.basket_no not in self.orders : self._order_list.update_order (o, True) @@ -218,6 +337,10 @@ class Order_Display (QtWidgets.QSplitter) : if db_lc > self.orders [o.basket_no].last_changed : self._order_list.update_order (o, False) self.orders [o.basket_no].last_changed = db_lc + orders.discard (o.basket_no) + self._order_list.remove_orders (orders) + for o in orders : + self.orders.pop (o, None) # end def update_orders def save_settings (self, settings) : diff --git a/designbox_orders.py b/designbox_orders.py index 2fae52e..1717f43 100644 --- a/designbox_orders.py +++ b/designbox_orders.py @@ -1,12 +1,14 @@ import os from QT.Main_Window import Main_Window, QtWidgets +from QT import QT from Order_Display import Order_Display from Login import Login_Dialog from App_State import App_State import designbox_orders_rc import schema import pathlib -from lxml import etree +from lxml import etree +import logging class Order_Processing (Main_Window) : @@ -42,8 +44,14 @@ class Order_Processing (Main_Window) : , ( (self.actions.test), (self.tool_bar_file, )) ) : for x in l : - x.addAction (a) - self.setCentralWidget (Order_Display (self)) + x.addAction (a) + sp = QtWidgets.QSplitter (self) + self._order_display = Order_Display (self) + self._log = App_State.Log_View (parent = self) + sp.addWidget (self._order_display) + sp.addWidget (self._log) + sp.setOrientation (QT.Vertical) + self.setCentralWidget (sp) settings = self.settings settings.beginGroup ("TempSettings") user_id = settings.value ("user_id") @@ -68,22 +76,29 @@ class Order_Processing (Main_Window) : def scan_orders_directory (self) : with schema.orm.db_session : - state = schema.State.get () - orders = self._find_orders (pathlib.Path (state.imos_export_dir)) + state = schema.State.get () + sim_archive = pathlib.Path (state.imos_sim_root) / "Archive" + iroot = pathlib.Path (state.imos_factory_root) + orders = self._find_orders (iroot / App_State.imos_done_dir) with schema.orm.db_session : for o in orders : - self._update_order_in_database (o) - self.centralWidget ().update_orders () + self._update_order_in_database (sim_archive, o) + orders = set (o.stem for o in orders) + for o in schema.Order.select (active = True) : + if o.basket_no not in orders : + o.active = False + self._order_display.update_orders () # end def scan_orders_directory - def _update_order_in_database (self, xml_file) : + def _update_order_in_database (self, sim_archive, xml_file) : basket_no = xml_file.stem if True : - order = schema.Order.get (basket_no = basket_no) + order = schema.Order.get (basket_no = basket_no, active = True) + xml_file = sim_archive / xml_file.name + xml = etree.parse (xml_file).getroot () + com = xml.xpath ("//TEXT_SHORT") [0].text + user,line = xml.xpath ("//INFO8") [0].text.split ("/") if not order : - xml = etree.parse (xml_file).getroot () - com = xml.xpath ("//TEXT_SHORT") [0].text - user,line = xml.xpath ("//RETAILER") [0].text.split ("/") order = schema.Order \ ( basket_no = basket_no , state = 0 @@ -96,20 +111,52 @@ class Order_Processing (Main_Window) : ( user = schema.User [App_State.user_id] , date = schema.datetime.now () , action_kind = 1 - , action = "Auftrag importiert" + , action = f"Auftrag importiert {basket_no}" , order = order ) + else : + pl = schema.Production_Line.get (short = line) + if ( (order.commission != com) + or (order.imos_user != user) + or (order.production_line != pl) + ) : + order.set ( commission = com + , imos_user = user + , production_line = pl + ) + schema.Action \ + ( user = schema.User [App_State.user_id] + , date = schema.datetime.now () + , action_kind = 2 + , action = + f"Auftrag geändert fom XML {basket_no}" + , order = order + ) + order.active = True # end def _update_order_in_database def _restore_settings (self) : super ()._restore_settings () - self.centralWidget ().restore_settings (self.settings) + sp = self.centralWidget () + settings = self.settings + settings.beginGroup ("main-splitter") + settings.setValue ("geometry", sp.saveGeometry ()) + settings.setValue ("windowState", sp.saveState ()) + settings.endGroup () + self._order_display.restore_settings (settings) + self._log.restore_settings (settings) # end def _restore_settings def save_settings (self) : settings = self.settings + sp = self.centralWidget () super ().save_settings () - self.centralWidget ().save_settings (settings) + settings.beginGroup ("main-splitter") + sp.restoreGeometry (settings.value ("geometry" )) + sp.restoreState (settings.value ("windowState")) + settings.endGroup () + self._order_display.save_settings (settings) + self._log.save_settings (settings) settings.beginGroup ("TempSettings") settings.setValue ("user_id", App_State.user_id) settings.endGroup () @@ -135,7 +182,7 @@ class Order_Processing (Main_Window) : settings.endGroup () return True else : - print ("### Login skipped") + App_State.L.error ("### Login skipped") return True # end def pre_main_window_action @@ -147,4 +194,9 @@ if __name__ == "__main__" : parser.add_argument ("-s","--settings-file", type = str) parser.add_argument ("-d", "--database", type = str) cmd = parser.parse_args () + logging.basicConfig \ + ( level = logging.DEBUG + , format = "%(asctime)s - %(levelname)s - %(message)s" + , datefmt = "%Y-%m-%d %H:%M:%S" + ) Order_Processing.run (Order_Processing.load_settings (cmd)) diff --git a/schema.py b/schema.py index c209b6c..21afffd 100644 --- a/schema.py +++ b/schema.py @@ -88,17 +88,15 @@ class Order_Line (db.Entity) : class State (db.Entity) : - imos_export_dir = orm.Required (str) - imos_order_dir = orm.Required (str) - sim_archive = orm.Required (str) - pgiq_import_dir = orm.Required (str) + imos_factory_root = orm.Required (str) + imos_sim_root = orm.Required (str) last_directory_scan = orm.Required (datetime) def __str__ (self) : return f""" -IMOS dir {self.imos_export_dir} -PGIQ dir {self.pgiq_import_dir} -Last scan {self.last_directory_scan} +IMOS Facory root {self.imos_factory_root} +IMOS SIM toot {self.imos_sim_root} +Last scan {self.last_directory_scan} """.strip () # end def __str__