master
Glueck Martin 2024-05-28 15:23:53 +02:00
parent 5009cf8ae4
commit 0cd18ab9f8
7 changed files with 734 additions and 458 deletions

View File

@ -1,65 +1,332 @@
from Log_View import Log_View, Log_Handler
import schema
import logging
import logging
import logging.handlers
from rich.logging import RichHandler
import yaml
import time
import argparse
import datetime
import threading
try :
import paho.mqtt.client as mqtt
except :
mqtt = None
try :
import redis
except :
redis = None
from Attr_Dict import Attr_Dict
try :
from cryptography.fernet import Fernet
except :
print ("Decryption supported disabled")
logging.DEBUG5 = logging.DEBUG - 5
logging.addLevelName (logging.DEBUG5, "DEBUG5")
def debug5 (self, msg, *args, **kwargs):
"""
Log 'msg % args' with severity 'DEBUG4'.
To pass exception information, use the keyword argument exc_info with
a true value, e.g.
logger.debug("Houston, we have a %s", "thorny problem", exc_info=1)
"""
if self.isEnabledFor(logging.DEBUG5):
self._log(logging.DEBUG5, msg, args, **kwargs)
# end def debug5
logging.Logger.debug5 = debug5
class App_State :
user_id = None
user = 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"
, 100 : "Article created"
, 110 : "Order Line created"
, 111 : "Order Line updated"
}
@classmethod
def Create_Action (cls, kind, text, order = None) :
if kind not in cls. Action_Kind :
raise ValueError (f"Unknow action kind {kind}")
schema.Action \
( user = cls.user_id
, date = schema.datetime.now ()
, action_kind = kind
, action = text
, order = order
def _setup_topics (self, config = None) :
config = config or getattr (self, "config", {})
mqr = config.mqtt ["topic-root"]
baskets = f"{mqr}/baskets"
self.mqtt = Attr_Dict \
( basket = Attr_Dict
( state = f"{baskets}/{{basket_no}}/state"
, user = f"{baskets}/{{basket_no}}/user"
, changed = f"{baskets}/{{basket_no}}/changed"
, data = f"{baskets}/{{basket_no}}"
)
, baskets = baskets
)
# end def create_action
# end def _setup_topics
@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
# ('<span class="log-time">%(asctime)s</span> '
# '<span class="level-%(levelname)s">%(levelname)s</span> - %(message)s'
# , datefmt = "%Y-%m-%d %H:%M:%S"
# )
# )
lh.setLevel (level)
lh.setFormatter \
( logging.Formatter
( "%(asctime)s - %(message)s"
def Add_Logging_Attributes (cls, parser = None) :
if parser is None :
parser = argparse.ArgumentParser ()
parser.add_argument ("--log-level", type = str, default = "INFO")
parser.add_argument ("--log-file", type = str)
parser.add_argument ("--log-file-level", type = str, default = "INFO")
return parser
# end def Add_Logging_Attributes
@classmethod
def Setup_Logging (cls, cmd, name = None) :
logging.basicConfig \
( level = "DEBUG5"
, format = "%(message)s"
, handlers = []
)
cls.L = logging.getLogger (name)
handler = RichHandler ()
handler.setLevel (cmd.log_level)
cls.L.addHandler (handler)
if cmd.log_file :
handler = logging.handlers.TimedRotatingFileHandler \
(cmd.log_file, when = "midnight", encoding = "utf-8")
formatter = logging.Formatter \
( "%(asctime)s - %(levelname)-7s - %(message)s"
, datefmt = "%Y-%m-%d %H:%M:%S"
)
)
cls.L.addHandler (lh)
return cls._log_view
# end def Log_View
handler.setFormatter (formatter)
if cmd.log_file_level :
handler.setLevel (cmd.log_file_level)
cls.L.addHandler (handler)
return cls.L
# end def Setup_Logging
@staticmethod
def snow (user = None) :
date = datetime.datetime.now ().strftime ("%Y-%m-%d %H-%M-%S")
if user :
return f"{date} -- {user}"
return date
# end def snow
@classmethod
def Load_Yaml (cls, file_name) :
with open (file_name, "r") as f :
return Attr_Dict (yaml.safe_load (f))
# end def Load_Yaml
def load_config (self, file_name) :
with open (file_name, "r") as f :
self.config = Attr_Dict (yaml.safe_load (f))
self.L.info ("Configuration loaded from %s", file_name)
return self.config
# end def load_config
def connect_to_db (self) :
if self.config.get ("database", "redis") == "mqtt" :
return MQTT \
(self.config, self._db_connected, self._db_changes)
return Redis (self.config, self._db_connected, self._db_changes)
# end def connect_to_db
def _db_connected (self, client) : pass
def _db_changes (self, key, value) : pass
def change_basket_user (self, basket_no, user, data) :
if user != data.get ("user") :
self.L.debug \
( "Change basket user for %s from %s to %s"
, basket_no, data.get ("user", "<>"), user
)
data ["user"] = user
bt = self.mqtt.basket
data ["changed"] = self.snow (self.User)
self.client.publish \
( bt.user.format (basket_no = basket_no)
, data ["user"]
)
self.client.publish \
( bt.changed.format (basket_no = basket_no)
, data ["changed"]
)
return True
return False
# end def change_basket_user
def change_basket_state (self, basket_no, state, data) :
old_state = int (data.get ("state", "0"))
if state != old_state :
self.L.debug \
( "Change basket status for %s from %s to %s"
, basket_no, old_state, state
)
data ["state"] = str (state)
bt = self.mqtt.basket
data ["changed"] = self.snow (self.User)
self.client.publish \
( bt.state.format (basket_no = basket_no)
, data ["state"]
)
self.client.publish \
( bt.changed.format (basket_no = basket_no)
, data ["changed"]
)
return True
return False
# end def change_basket_state
_key = b'Mgl1Wae_JILP0rbLazTL5gOpfrp4tqOFOiEsD_IUH8c='
@classmethod
def Decrypt (cls, data: bytes) -> bytes:
"""Decrypt the passed data"""
return Fernet (cls._key).decrypt (data)
# end def Decrypt
@classmethod
def Encrypt (cls, data: bytes) -> bytes:
"""Encrypt the passed data"""
return Fernet (cls._key).encrypt (data.encode ("ascii"))
# end def Encrypt
# end class App_State
class _Database_Interface_ (App_State) :
def __init__ (self, config, connected, changes) :
self.connected = connected
self.changes = changes
# end def __init__
def subscribe (self, key) : pass
def publish (self, key, value) : pass
def delete (self, key): pass
def get_all_values (self, key) : pass
# end class _Database_Interface_
class MQTT (_Database_Interface_) :
def __init__ (self, config, connected, changes) :
super ().__init__ (config, connected, changes)
self.client = client = mqtt.Client ()
client.on_connect = self._on_connect
client.on_message = self._on_message
mqc = config ["mqtt"]
pw = self.Decrypt (mqc.password)
client.username_pw_set (mqc.user, pw)
self.L.info ("Try to connect to MQTT %s:%s", mqc.broker, mqc.port)
client.connect (mqc.broker, port = mqc.port)
self._mqtt_thread = threading.Thread \
(target = self.client.loop_forever, daemon = True)
self._mqtt_thread.start ()
# end def __init__
def _on_connect (self, client, user_data, flags, rc) :
self.L.info ("Connected to MQTT Broker")
if self.connected :
self.connected (self)
# end def _on_connect
def _on_message (self, client, user_data, msg) :
self.L.debug5 ("MQTT Message: %s: %s", msg.topic, msg.payload)
if self.changes :
self.changes (msg.topic, msg.payload)
# end def _on_message
def subscribe (self, key, recursie) :
if recursie :
key = f"{key}/#"
self.client.subscribe (key)
# end def subscribe
def publish (self, key, value) :
self.client.publish (key, payload = value, retain = True)
self.L.debug5 ("Publish to topic %s: %s", key, value)
# end def publish
def delete (self, key) :
self.client.publish (key, payload = None, retain = False)
self.L.debug5 ("Delete topic %s", key)
# end def delete
# end class MQTT
class Redis (_Database_Interface_) :
def __init__ (self, config, connected, changes) :
super ().__init__ (config, connected, changes)
RC = config.redis
host = RC.host
port = RC.get ("port", 6379)
self.L.info ("Try to connect to redis %s:%s", host, port)
self.client = client = redis.StrictRedis \
( host = host
, port = port
, decode_responses = True
)
self.subscribtions = {}
if connected :
connected (self)
threading.Thread (target = self._listen, daemon = True).start ()
# end def __init__
def _listen (self) :
self.L.debug ("Start redis listener",)
pubsub = self.client.pubsub (ignore_subscribe_messages = True)
pubsub.execute_command (b"CLIENT", b"ID")
client_id = pubsub.connection.read_response ()
pubsub.execute_command \
(f"CLIENT TRACKING on REDIRECT {client_id} BCAST")
res = pubsub.connection.read_response ()
self.L.debug ("Redis tracking enabled: %s", res)
pubsub.subscribe ('__redis__:invalidate')
while True :
m = pubsub.get_message ()
if m and m ["channel"] == "__redis__:invalidate" :
if self.changes :
for key in m ["data"] :
for k, r in self.subscribtions.items () :
if ( ( r and key.startswith (k))
or (not r and (key == k))
) :
val = self.client.get (key)
self.changes (key.replace (":", "/"), val)
else :
time.sleep (0.01)
# end def _listen
def get_all_values (self, key) :
key = key.replace ("/", ":")
for k in self.client.keys (f"{key}:*") :
v = self.client.get (k)
yield k.replace (":", "/"), v
# end def get_all_values
def subscribe (self, key, recursive) :
self.subscribtions [key.replace ("/", ":")] = recursive
# end def subscribe
def publish (self, key, value) :
key = key.replace ("/", ":")
self.client.set (key, value)
self.L.debug5 ("Set %s: %s", key, value)
# end def publish
def delete (self, key) :
key = key.replace ("/", ":")
self.client.delete (key)
self.L.debug5 ("Delete key %s", key)
# end def delete
# end class Redis
if __name__ == "__main__" :
import argparse
parser = argparse.ArgumentParser ()
parser.add_argument ("-e", "--encrypt", type = str)
parser.add_argument ("-d", "--decrypt", type = str)
cmd = parser.parse_args ()
if cmd.encrypt :
print (App_State.Encrypt (cmd.encrypt))
if cmd.decrypt :
print (App_State.Decrypt (cmd.decrypt))

View File

@ -1,11 +1,13 @@
from QT import QtWidgets, QT
import schema
from User import User
import yaml
class Login_Dialog (QtWidgets.QDialog) :
def __init__ (self, username = "") :
def __init__ (self, config_file, username = "") :
super ().__init__ ()
cfg = User.Load_Yaml (config_file)
self.users = User (cfg.users)
self.setWindowTitle ("Login")
self.layout = L = QtWidgets.QFormLayout (self)
self.user_name = QtWidgets.QLineEdit ()
@ -26,7 +28,6 @@ class Login_Dialog (QtWidgets.QDialog) :
self.login = login
self.password.editingFinished.connect (self._login)
self.user_name.editingFinished.connect(self._check_login_possible)
self.user_id = None
self.user_name.setText (username)
if username :
self.password.setFocus ()
@ -39,18 +40,14 @@ class Login_Dialog (QtWidgets.QDialog) :
# end def _check_login_possible
def _login (self) :
un = self.user_name.text ()
pw = self.password.text ()
with schema.orm.db_session :
user = schema.User.get (username = un)
if not user :
self.error.setText ("User nicht gefunden")
else :
if user.check_password (pw) :
self.user_id = user.id
self.accept ()
else :
self.error.setText ("Passwort falsch")
un = self.user_name.text ()
pw = self.password.text ()
pw_ok = self.users.check_password (un, pw, True)
self.login.setEnabled (pw_ok)
if not pw_ok :
self.error.setText ("Passwort falsch")
else :
self.accept ()
# end def _login
# end class Login_Dialog

View File

@ -1,12 +1,13 @@
from QT import QtWidgets, QtGui, QT
from QT.Input_Dialog import Input_Dialog
from _With_Table_Widget_ import _With_Table_Widget_
from App_State import App_State
from Action_List import Action_Dialog
import schema
import subprocess
import threading
import time
import pathlib
from lxml import etree
class Order_List (_With_Table_Widget_) :
@ -16,7 +17,8 @@ class Order_List (_With_Table_Widget_) :
("Warenkorb", "Status", "Bearbeiter", "Kommission", "Kunde", "Linie")
State_Map = \
{ 0 : "neu"
{ -1 : "wird gelöscht"
, 0 : "neu"
, 10 : "in Bearbeitung"
, 20 : "im CAD geöffnet"
, 25 : "CNC Erzeugung läuft"
@ -24,6 +26,7 @@ class Order_List (_With_Table_Widget_) :
, 95 : "Zurückgesetzt"
, 99 : "Storno"
}
def __init__ (self, parent = None) :
super ().__init__ (parent)
self._view.setContextMenuPolicy (QT.CustomContextMenu)
@ -34,140 +37,227 @@ class Order_List (_With_Table_Widget_) :
self._reset_order = ACT (self.tr ("Auftrag zurücksetzen"))
self._cancel = ACT (self.tr ("Auftrag stronieren"))
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 = {}
self.selection_model.selectionChanged.connect (self._show_details)
self._view.setSortingEnabled (True)
# end def __init__
@property
def baskets (self) :
return self.parent ().baskets
# end def baskets
@property
def config (self) :
return self.parent ().config
# end def config
@property
def L (self) :
return self.root.L
# end def config
@property
def root (self) :
return self.parent ().parent ().parent ()
# end def root
def _show_details (self, selected, deselected) :
row_data = self._view.selectedItems ()
if row_data :
bno = row_data [0].text ()
basket = self.baskets [bno]
self.parent ()._order_detail.update_view (bno, basket)
# end def _current_changed
def _show_context_menu (self, pos) :
row_data = self._view.selectedItems ()
if row_data :
bno = row_data [0].text ()
with schema.orm.db_session () :
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")
, self.tr
( "Dieser Auftrag wird bereits von jemand anderes "
"bearbeitet.\n"
"Wollen Sie den Auftrag als Bearbeiter übernehmen?"
)
)
if r == QtWidgets.QMessageBox.No :
return
App_State.Create_Action \
( 10
, f"User changed from {order.user.id} to {App_State.user_id}"
, order
)
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.addAction (self._open_cad)
cm.addAction (self._recreate_cnc)
cm.addSeparator ()
cm.addAction (self._send_to_odoo)
cm.addSeparator ()
cm.addAction (self._reset_order)
cm.addAction (self._cancel)
cm.addSeparator ()
cm.addAction (self._actions)
basket = self.baskets [bno]
user = basket.get ("user")
if user and user != App_State.User :
r = QtWidgets.QMessageBox.question \
( self, self.tr ("Fehler")
, self.tr
( "Dieser Auftrag wird bereits von jemand anderes "
"bearbeitet.\n"
"Wollen Sie den Auftrag als Bearbeiter übernehmen?"
)
)
if r == QtWidgets.QMessageBox.No :
return
if self.root.change_basket_user (bno, App_State.User, basket) :
self.update_basket (bno, basket, False, row_data [0].row ())
state = int (basket ["state"])
cm = QtWidgets.QMenu ()
if state < 20 :
cm.addAction (self._open_cad)
cm.addAction (self._recreate_cnc)
cm.addSeparator ()
cm.addAction (self._reset_state)
cm.exec (self._view.mapToGlobal (pos))
cm.addAction (self._send_to_odoo)
cm.addSeparator ()
cm.addAction (self._reset_order)
cm.addAction (self._cancel)
cm.addSeparator ()
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 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)
def update_basket (self, basket_no, data, new, row = None) :
state = int (data.get ("state", "0") or "-1")
if new :
row = self._view.rowCount ()
self._view.setRowCount (row + 1)
else :
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
if row is None :
for row in range (self._view.rowCount ()) :
if self._view.item (row, 0).text () == basket_no :
break
else :
self.root.L.error \
("Could not find row for order %s", basket_no)
return
self._add_read_only_string (row, 0, basket_no)
self._add_read_only_string (row, 1, self.State_Map [state])
self._add_read_only_string (row, 2, data.get ("user", ""))
self._add_read_only_string (row, 3, data.get ("commission", ""))
self._add_read_only_string (row, 4, data.get ("shop_user", ""))
self._add_read_only_string (row, 5, data.get ("line", ""))
# end def update_basket
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 remove_basket (self, basket_no) :
for row in range (self._view.rowCount ()) :
if self._view.item (row, 0).text () == basket_no :
self._view.removeRow (row)
return
# end def remove_basket
def add_xml_element (self, root, tag, text = None, ** kw) :
result = etree.SubElement (root, tag, **kw)
if text is not None :
result.text = str (text)
return text
# end def add_xml_element
def _add_order_line_to_xml_file (self, xml_file : pathlib.Path, xml_element) :
xml = etree.parse (xml_file).getroot ()
bl = xml.xpath ("//BuilderList") [0]
bl.append (xml_element)
ori_name = xml_file.parent / "original" / xml_file.name
if not ori_name.exists () :
ori_name.parent.mkdir (exist_ok = True)
self.L.debug ("Rename %s to %s", xml_file, ori_name)
xml_file.rename (ori_name)
with xml_file.open ("wb") as f :
f.write (etree.tostring (xml, pretty_print = True, encoding = "utf-8"))
self.L.info ("Patched XML file written %s", xml_file)
# end def _add_order_line_to_xml_file
def add_cnc_orderline (self) :
row_data = self._view.selectedItems ()
if not row_data :
return
r = Input_Dialog \
( "CNC Bearbeitung verrechnen"
, _MIN_WIDTH = 400
, article = {"default": "TZ_PROCESSING_FEE", "title" : "Artikel", "required": True}
, count = {"default": 1, "title" : "Anzahl", "required": True, "type" : "int"}
, text = {"default": "Sonderbearbeitung für Position", "title" : "Text", "required": True}
, price = {"default": 60, "title" : "Preis"
, "type" : "int"
}
).run ()
if r :
bno = row_data [0].text ()
basket = self.baskets [bno]
positions = basket ["positions"]
pos = int (positions [-1] ["pos"].split (".") [0])
hpos = int (positions [-1] ["hpos"])
tax = positions [0] ["tax"]
price = r ["price"]
tax_amount= price / 100 * tax
pos += 1
hpos += 1
positions.append \
( { "art_name" : r ["article"]
, "count" : r ["count"]
, "pos" : str (pos)
, "hpos" : str (hpos)
, "text" : r ["text"]
, "tax" : tax
, "price" : price
}
)
self.parent ()._order_detail.update_view (bno, basket)
xset = etree.Element ("Set", LineNo = str (pos))
AXE = self.add_xml_element
AXE (xset, "hierarchicalPos", hpos)
AXE (xset, "Pname", r ["article"])
AXE (xset, "Count", r ["count"])
AXE (xset, "PVarString", f"IDBEXTID:=1|ART_NAME:={r ['article']}")
AXE (xset, "ARTICLE_TEXT_INFO1", r ["text"])
AXE (xset, "ARTICLE_TEXT_INFO2", r ["text"])
AXE (xset, "ARTICLE_TEXT_INFO3")
AXE (xset, "ARTICLE_TEXT_INFO4", 0)
AXE (xset, "ARTICLE_TEXT_INFO5", r ["text"])
AXE (xset, "ARTICLE_TEXT_INFO6", r ["text"])
AXE (xset, "ARTICLE_TEXT_INFO7")
AXE (xset, "ARTICLE_PRICE_INFO1", price)
AXE (xset, "ARTICLE_PRICE_INFO2", price)
AXE (xset, "ARTICLE_PRICE_INFO3", price)
AXE (xset, "ARTICLE_PRICE_INFO4", tax)
AXE (xset, "ARTICLE_PRICE_INFO5", round (tax_amount, 2))
AXE (xset, "ARTICLE_PRICE_INFO6", round (price + tax_amount, 2))
breakpoint ()
OR = pathlib.Path (self.config.directories.order_ready)
AR = pathlib.Path (self.config.directories.sim_root) / "Archive"
## first patch the XML in the archive directory
self._add_order_line_to_xml_file (AR / f"{bno}.xml", xset)
## than patch the file in the order-ready directory
self._add_order_line_to_xml_file (OR / f"{bno}.xml", xset)
return True
return False
# end def add_cnc_orderline
def _open_order_in_cad (self) :
row_data = self._view.selectedItems ()
if row_data :
bno = row_data [0].text ()
with schema.orm.db_session () :
state = schema.State.get ()
order = schema.Order.get (basket_no = bno, active = True)
if order.state < 20 :
order.state = 20
self.update_order (order, False, row_data [0].row ())
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, )
, daemon = True
).start ()
App_State.Create_Action \
( 20
, f"IMOS CAD started {order.basket_no}"
, order
)
bno = row_data [0].text ()
basket = self.baskets [bno]
state = int (basket.get ("state", 0))
if state < 20 :
if self.root.change_basket_state (bno, 20, basket) :
self.update_basket (bno, basket, False, row_data [0].row ())
order_dwg = r"%s\ImOrder\%s\%s.dwg" \
% (self.config.directories.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, )
, daemon = True
).start ()
# end def _open_order_in_cad
def _wait_for_cad_close (self, basket_no) :
App_State.L.info ("Auftrag %s im CAD geöffnet", basket_no)
self.L.info ("Auftrag %s im CAD geöffnet", basket_no)
p = self._cad_processes [basket_no]
while p.poll () is None :
time.sleep (.5)
del self._cad_processes [basket_no]
with schema.orm.db_session () :
order = schema.Order.get (basket_no = basket_no, active = True)
order.state = 10
self.update_order (order, False)
App_State.Create_Action \
( 25
, f"IMOS CAD closed {order.basket_no}: Error code {p.poll ()}"
, order
)
App_State.L.info ("CAD für Auftrag %s beendet", basket_no)
basket = self.baskets [basket_no]
if self.root.change_basket_state (basket_no, 10, basket) :
self.update_basket (basket_no, basket, False)
self.L.info ("Auftrag %s im CAD geschlossen", basket_no)
# end def _wait_for_cad_close
def _start_recreate_cnc (self) :
@ -185,101 +275,80 @@ class Order_List (_With_Table_Widget_) :
</BatchJobs>
</XML>
"""
with schema.orm.db_session () :
state = schema.State.get ()
order = schema.Order.get (basket_no = bno, active = True)
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)
job_file_name = ( pathlib.Path (self.config.directories.sim_root)
/ App_State.sim_batches
/ f"{bno}.xml"
)
txt_file = ( pathlib.Path (self.config.directories.factory_root)
/ App_State.imos_done_dir
/ f"{bno}.txt"
)
with job_file_name.open ("w") as f :
f.write (xml)
if txt_file.exists () :
txt_file.unlink ()
basket = self.baskets [bno]
if self.root.change_basket_state (bno, 25, basket) :
self.update_basket (bno, basket, False, row_data [0].row ())
threading.Thread \
( target = self._wait_for_cnc_done
, args = (bno, )
, daemon = True
).start ()
# 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)
txt_file = ( pathlib.Path (self.config.directories.factory_root)
/ App_State.imos_done_dir
/ f"{basket_no}.txt"
)
err_file_name = ( pathlib.Path (state.imos_sim_root)
err_file_name = ( pathlib.Path (self.config.directories.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}")
self.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, active = True)
order.state = 10
self.update_order (order, False)
App_State.Create_Action (35, text, order)
self.L.info (text.format (basket_no = basket_no))
basket = self.baskets [basket_no]
if self.root.change_basket_state (basket_no, 10, basket) :
self.update_basket (basket_no, basket, False)
# 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 ()
App_State.L.error ("Reset order in webshop %s", bno)
self.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 ()
App_State.L.error ("Cancel Order %s", bno)
self.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 ()
with schema.orm.db_session () :
state = schema.State.get ()
order = schema.Order.get (basket_no = bno, active = True)
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)
bno = row_data [0].text ()
basket = self.baskets [bno]
state = int (basket ["state"])
D = self.config.directories
factory = pathlib.Path (D.factory_root)
for ext in ".xml", ".txt" :
sf = factory / D.order_ready / f"{bno}{ext}"
df = factory / D.pgiq_dir / f"{bno}{ext}"
sf.rename (df)
self.L.info ("Auftrag %s für Odoo freigegeben", bno)
# end def _send_oder_to_odoo
def _show_actions (self) :
row_data = self._view.selectedItems ()
if row_data :
bno = row_data [0].text ()
with schema.orm.db_session () :
order = schema.Order.get (basket_no = bno, active = True)
Action_Dialog \
(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 :
@ -293,15 +362,9 @@ class Order_List (_With_Table_Widget_) :
)
if r == QtWidgets.QMessageBox.No :
return
with schema.orm.db_session () :
order = schema.Order.get (basket_no = bno, active = True)
order.state = 10
self.update_order (order, False)
App_State.Create_Action \
( 15
, f"Status wird auf 10 gesetzt"
, order
)
basket = self.baskets [bno]
if self.root.change_basket_state (bno, 10, basket) :
self.update_basket (bno, basket, False)
# end def _reset_order_state
# end class Order_List
@ -311,40 +374,50 @@ class Order_Detail (_With_Table_Widget_) :
settings_group = "order-detail"
Header = "Auftrags Detail"
Columns = \
("Position", "Artikel", "Preis")
("Position", "Anzahl", "Artikel", "Preis")
def update_view (self, basket_no, basket) :
if basket :
positions = basket.get ("positions", ())
self._view.setRowCount (len (positions))
for i, p in enumerate (positions) :
self._update_read_only_string (i, 0, str (p ["pos"]))
self._update_read_only_string (i, 1, str (p ["count"]))
self._update_read_only_string (i, 2, str (p ["text"]))
self._update_read_only_string (i, 3, "%7.2f" % p ["price"])
# end def update_view
# end class Order_Detail
class Order_Display (QtWidgets.QSplitter) :
def __init__ (self, parent) :
def __init__ (self, config, parent) :
super ().__init__ (parent)
self.config = config
self._order_list = Order_List ()
self._order_detail = Order_Detail ()
self.addWidget (self._order_list)
self.addWidget (self._order_detail)
self.orders = {}
self.baskets = {}
# end def __init__
def update_orders (self) :
orders = set (self.orders.keys ())
with schema.orm.db_session :
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)
self.orders [o.basket_no] = o
o.last_changed = db_lc
else :
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 update_basket (self, basket_no, data) :
new = basket_no not in self.baskets
if data is not None :
if basket_no not in self.baskets :
self.baskets [basket_no] = data
else :
self.baskets [basket_no].update (data)
self._order_list.update_basket (basket_no, self.baskets [basket_no], new)
else : ### remove order
self._order_list.remove_basket (basket_no)
self.baskets.pop (basket_no, None)
# end def update_basket
def add_cnc_orderline (self) :
self._order_list.add_cnc_orderline ()
# end def add_cnc_orderline
def save_settings (self, settings) :
settings.beginGroup ("order-display")
settings.setValue ("geometry", self.saveGeometry ())

View File

@ -6,22 +6,14 @@ from filesystem_watcher import \
WindowsApiEmitterExceptionHandling, Directory_Handler
import time
import json
import datetime
class Order_Handler (Directory_Handler) :
class Order_Handler (Directory_Handler, App_State) :
def __init__ (self, path, client, config) :
super ().__init__ (path)
self.client = client
mqr = config.mqtt ["topic-root"]
baskets = f"{mqr}/baskets"
self.mqtt = Attr_Dict \
( basket_topic = f"{baskets}/{{basket_no}}"
, basket_state = f"{baskets}/{{basket_no}}/state"
, basket_user = f"{baskets}/{{basket_no}}/user"
, basket_changed = f"{baskets}/{{basket_no}}/changed"
, baskets = baskets
)
self.config = config
self._setup_topics (config)
# end def __init__
def on_any_event (self, event) :
@ -39,11 +31,10 @@ class Order_Handler (Directory_Handler) :
file = Path (event.src_path)
if file.suffix == ".xml" :
basket_no = file.stem
for t in self.mqtt.values () :
self.client.publish \
( t.format (basket_no = basket_no)
, payload = None, retain = False
)
for t in self.mqtt.basket.values () :
key = t.format (basket_no = basket_no)
self.client.delete (key)
self.L.debug ("Delete key %s", key)
self.L.info ("Order removed %s", basket_no)
super ().on_any_event (event)
# end def on_any_event
@ -51,24 +42,28 @@ class Order_Handler (Directory_Handler) :
def new_order (self, xml_file : Path) :
try :
self.L.info ("New basket %s", xml_file.name)
xml = etree.parse (xml_file).getroot ()
sim_xml = ( Path ( self.config.directories.sim_root)
/ "Archive" / xml_file.name
)
if not sim_xml.exists () :
self.L.error ("Archive order xml does not exist: %s", sim_xml)
xml = etree.parse (sim_xml).getroot ()
com = xml.xpath ("//TEXT_SHORT") [0].text
user, line = xml.xpath ("//INFO8") [0].text.split ("/")
basket_no = xml_file.stem
bt = self.mqtt.basket
self.client.publish \
( self.mqtt.basket_state.format (basket_no = basket_no)
, payload = "new", retain = True
)
( bt.state.format (basket_no = basket_no), 0)
self.client.publish \
( self.mqtt.basket_changed.format (basket_no = basket_no)
, payload = App_State.snow ("import"), retain = True
( bt.changed.format (basket_no = basket_no)
, App_State.snow ("import")
)
basket = dict \
(user = user, line = line, commission = com)
(shop_user = user, line = line, commission = com)
basket ["positions"] = self._scan_order_pos (xml)
self.client.publish \
( self.mqtt.basket_topic.format (basket_no = basket_no)
, payload = json.dumps (basket), retain = True
( bt.data.format (basket_no = basket_no)
, json.dumps (basket)
)
self.L.info ("Created new basket %s", basket_no)
except Exception as e :
@ -82,13 +77,17 @@ class Order_Handler (Directory_Handler) :
art_name = a.xpath ("Pname") [0].text.strip ()
count = int (a.xpath ("Count") [0].text)
pos = a.get ("LineNo")
hpos = a.xpath ("hierarchicalPos") [0].text
text = a.xpath ("ARTICLE_TEXT_INFO1" ) [0].text.strip ()
price = float (a.xpath ("ARTICLE_PRICE_INFO1") [0].text)
tax = float (a.xpath ("ARTICLE_PRICE_INFO4") [0].text)
result.append \
( dict ( art_name = art_name
, count = count
, pos = pos
, pos = pos
, hpos = hpos
, text = text
, tax = tax
, price = price
)
)
@ -102,8 +101,8 @@ class Order_Watch (App_State) :
order_incomming_dir = r"n:\glueck\watch-me\inbox"
def __init__ (self, config_file) :
self.load_config (config_file)
self.client = self.connect_to_mqtt ()
self.load_config (config_file)
self.client = self.connect_to_db ()
# end def __init__
def _start_watchdog (self) :
@ -139,7 +138,7 @@ if __name__ == "__main__" :
parser.add_argument ("config_file", type = str)
cmd = parser.parse_args ()
_Logger_.L = Order_Watch.Setup_Logging (cmd)
_Logger_.L = App_State.Setup_Logging (cmd)
ow = Order_Watch (cmd.config_file)
ow.run ()

View File

@ -1,16 +1,17 @@
import os
from QT.Main_Window import Main_Window, QtWidgets
from QT import QT
from Log_View import Log_View, Log_Handler
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
import logging
import threading
import json
class Order_Processing (Main_Window) :
class Order_Processing (Main_Window, App_State) :
APP_NAME = "Designbox Orders"
VERSION = "0.5"
@ -20,18 +21,27 @@ class Order_Processing (Main_Window) :
@classmethod
def _load_app_settings (cls, settings) :
settings.beginGroup ("TempSettings")
result = {"database" : settings.value ("database")}
result = {"username" : settings.value ("username")}
result = { "config_file" : settings.value ("config_file")
, "user_name" : settings.value ("user_name")
}
settings.endGroup ()
return result
# end def _load_app_settings
def _setupActions (self, app) :
super ()._setupActions (app)
self._add_action ("test", "Scan Dir", self.scan_orders_directory)
self._add_action \
( "add_cnc_orderline", "CNC Bearbeitung verrechnen"
, self.add_cnc_orderline
)
# end def _setupActions
def _setupGui (self) :
settings = self.settings
settings.beginGroup ("TempSettings")
config_file = settings.value ("config_file")
settings.endGroup ()
self.load_config (config_file)
self.main_menu = QtWidgets.QMenuBar (self)
self.setMenuBar (self.main_menu)
file = QtWidgets.QMenu ("& File")
@ -41,157 +51,53 @@ class Order_Processing (Main_Window) :
for a, l in ( ( self.actions.exit
, (file, self.tool_bar_file)
)
, ( (self.actions.test), (self.tool_bar_file, ))
, ( (self.actions.add_cnc_orderline), (self.tool_bar_file, ))
) :
for x in l :
x.addAction (a)
sp = QtWidgets.QSplitter (self)
self._order_display = Order_Display (self)
self._log = App_State.Log_View (parent = self)
self._order_display = Order_Display (self.config, self)
self.create_log_view (parent = self)
sp.addWidget (self._order_display)
sp.addWidget (self._log)
sp.addWidget (self._log_view)
sp.setOrientation (QT.Vertical)
self.setCentralWidget (sp)
settings = self.settings
settings.beginGroup ("TempSettings")
user_id = settings.value ("user_id")
settings.endGroup ()
with schema.orm.db_session :
App_State.user_id = schema.User [user_id].id
self.scan_orders_directory ()
self._setup_topics ()
self.L.info ("User: %s", App_State.User)
self.client = self.connect_to_db ()
# end def _setupGui
def _find_orders (self, root_path : pathlib.Path) :
result = set ()
for path, dirs, files in root_path.walk () :
for f in files :
if f.endswith (".txt") :
xml_name = (path / f).with_suffix (".xml")
if xml_name.exists () :
result.add (xml_name)
for d in dirs :
result.intersection_update (self._find_orders (root_path / d))
return result
# end def _find_orders
def _db_connected (self, client) :
topic = self.mqtt.baskets
client.subscribe (topic, True)
self.L.debug ("Subscribe to %s", topic)
for k, v in client.get_all_values (topic) :
self._db_changes (k, v)
# end def _mqtt_connected
def scan_orders_directory (self) :
with schema.orm.db_session :
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 (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, sim_archive, xml_file) :
basket_no = xml_file.stem
if True :
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 :
order = schema.Order \
( basket_no = basket_no
, state = 0
, commission = com
, imos_user = user
, production_line = schema.Production_Line.get (short = line)
, active = True
)
schema.Action \
( user = schema.User [App_State.user_id]
, date = schema.datetime.now ()
, action_kind = 1
, action = f"Auftrag importiert {basket_no}"
, order = order
)
def _db_changes (self, key, value) :
bst = self.mqtt.baskets
if key.startswith (bst) :
key = key [len (bst) + 1:]
parts = key.split ("/")
basket_no = parts.pop (0)
if not parts :
if value :
data = json.loads (value)
else :
data = None
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
)
self._scan_order_pos (order, xml)
order.active = True
# end def _update_order_in_database
def _scan_order_pos (self, order, xml) :
bl = xml.xpath ("//BuilderList") [0]
changes = 0
for a in bl.getchildren () :
art_name = a.xpath ("Pname") [0].text.strip ()
count = int (a.xpath ("Count") [0].text)
pos = a.get ("LineNo")
text = a.xpath ("ARTICLE_TEXT_INFO1" ) [0].text.strip ()
price = float (a.xpath ("ARTICLE_PRICE_INFO1") [0].text)
article = schema.Odoo_Article.get (name = art_name)
if not article :
article = schema.Odoo_Article \
( name = art_name
, default_text = art_name
, default_price = 0
)
App_State.Create_Action \
( 100
, f"Artikel {art_name} wurde in der Datenbank angelegt"
, order
)
order_line = schema.Order_Line.get (order = order, position = pos)
if not order_line :
order_line = schema.Order_Line \
( order = order
, position = pos
, count = count
, text = text
, price = price
, odoo_article = article
)
App_State.Create_Action \
( 110
, f"Auftragszeile {pos} für {order.basket_no} wurde in der "
"Datenbank angelegt"
, order
)
changes += 1
else :
if ( (order_line.text != text)
or (order_line.count != count)
or (order_line.price != price)
) :
print ("Update")
order_line.text = text
order_line.price = price
order_line.count = count
changes += 1
App_State.Create_Action \
( 111
, f"Auftragszeile {pos} für {order.basket_no} wurde "
"in der Datenbank geändert"
, order
)
# end def _scan_order_pos
if isinstance (value, bytes) :
value = value.decode ("utf-8")
data = {parts [0] : value}
self.L.debug ("Change basket %s: %s", basket_no, data)
self._order_display.update_basket (basket_no, data)
# end def _mqtt_message
def add_cnc_orderline (self) :
self._order_display.add_cnc_orderline ()
# end def add_cnc_orderline
def _restore_settings (self) :
super ()._restore_settings ()
sp = self.centralWidget ()
@ -201,7 +107,7 @@ class Order_Processing (Main_Window) :
settings.setValue ("windowState", sp.saveState ())
settings.endGroup ()
self._order_display.restore_settings (settings)
self._log.restore_settings (settings)
self._log_view.restore_settings (settings)
# end def _restore_settings
def save_settings (self) :
@ -213,47 +119,61 @@ class Order_Processing (Main_Window) :
sp.restoreState (settings.value ("windowState"))
settings.endGroup ()
self._order_display.save_settings (settings)
self._log.save_settings (settings)
self._log_view.save_settings (settings)
settings.beginGroup ("TempSettings")
settings.setValue ("user_id", App_State.user_id)
settings.setValue ("user_name", App_State.User)
settings.endGroup ()
# end def save_settings
@classmethod
def pre_main_window_action (cls, settings) :
settings.beginGroup ("TempSettings")
db_file = settings.value ("database")
user_id = settings.value ("user_id")
config_file = settings.value ("config_file")
user_name = settings.value ("user_name")
settings.endGroup ()
schema.db.bind (provider = "sqlite", filename = db_file)
schema.db.generate_mapping (create_tables = False)
username = ""
if user_id :
with schema.orm.db_session :
username = schema.User [user_id].username
if False :
login = Login_Dialog (username)
login = Login_Dialog (config_file, user_name)
if login.exec () == 1 :
settings.beginGroup ("TempSettings")
settings.setValue ("user_id", login.user_id)
settings.setValue ("user_name", App_State.User)
settings.endGroup ()
return True
else :
App_State.L.error ("### Login skipped")
cls.L.error ("### Login skipped")
App_State.User = user_name
return True
# end def pre_main_window_action
def create_log_view (self, level = "INFO", parent = None) :
self.L.debug ("Log window created")
self._log_view = Log_View (parent)
lh = Log_Handler (self._log_view)
#lh.setFormatter \
# ( logging.Formatter
# ('<span class="log-time">%(asctime)s</span> '
# '<span class="level-%(levelname)s">%(levelname)s</span> - %(message)s'
# , datefmt = "%Y-%m-%d %H:%M:%S"
# )
# )
lh.setLevel (level)
lh.setFormatter \
( logging.Formatter
( "%(asctime)s - %(message)s"
, datefmt = "%Y-%m-%d %H:%M:%S"
)
)
self.L.addHandler (lh)
return self._log_view
# end def create_log_view
# end class Order_Processing
if __name__ == "__main__" :
import argparse
parser = argparse.ArgumentParser ()
parser.add_argument ("-s","--settings-file", type = str)
parser.add_argument ("-d", "--database", type = str)
parser = Order_Processing.Add_Logging_Attributes ()
parser.add_argument ("-s", "--settings-file", type = str)
parser.add_argument ("-c", "--config-file", 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"
)
App_State.Setup_Logging (cmd)
Order_Processing.run (Order_Processing.load_settings (cmd))

View File

@ -4,3 +4,14 @@ mqtt:
user: imos
password: gAAAAABmTy4fipVAw10oKCE2aLWq79BF9Id5H8-lEEm8tOgcoaGgIJRWwBdR0nj-lrN-70hcLMHVqNudk7FTJjOvE0KOCkDr0A==
topic-root: imos-order
redis:
host: odoo-test
database: redis
users: users.yaml
directories:
order_ready: n:\glueck\watch-me\inbox
factory_root: n:\IMOS\Live-System\Factory
sim_root: n:\IMOS\Live-System\SIM
pgiq_dir: n:\glueck\watch-me\odoo

9
users.yaml Normal file
View File

@ -0,0 +1,9 @@
glueck:
active: true
password: 8625ff9e7b2f0916bf6e9ef6c163d3d6f873a7796146453bf236d52f57cb90a6
user1:
active: true
password: 8625ff9e7b2f0916bf6e9ef6c163d3d6f873a7796146453bf236d52f57cb90a6
user2:
active: true
password: 8625ff9e7b2f0916bf6e9ef6c163d3d6f873a7796146453bf236d52f57cb90a6