order-processing/Order_Display.py

439 lines
18 KiB
Python

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 subprocess
import threading
import time
import pathlib
from lxml import etree
class Order_List (_With_Table_Widget_) :
settings_group = "order-list"
Header = "Aufträge"
Columns = \
("Warenkorb", "Status", "Bearbeiter", "Kommission", "Kunde", "Linie")
State_Map = \
{ -1 : "wird gelöscht"
, 0 : "neu"
, 10 : "in Bearbeitung"
, 20 : "im CAD geöffnet"
, 25 : "CNC Erzeugung läuft"
, 90 : "Odoo"
, 95 : "Zurückgesetzt"
, 99 : "Storno"
}
def __init__ (self, parent = None) :
super ().__init__ (parent)
self._view.setContextMenuPolicy (QT.CustomContextMenu)
self._view.customContextMenuRequested.connect (self._show_context_menu)
ACT = QtGui.QAction
self._open_cad = ACT (self.tr ("Im CAD öffnen"))
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 ans Odoo schicken"))
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._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 ()
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._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_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 :
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_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 ()
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) :
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]
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) :
row_data = self._view.selectedItems ()
if row_data :
bno = row_data [0].text ()
xml = f"""<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<XML Type="JOBPROCESS">
<BatchJobs>
<BatchJob>
<OrderNo>{bno}</OrderNo>
<BatchName>STANDARD</BatchName>
<Source>IMOSADMIN</Source>
</BatchJob>
</BatchJobs>
</XML>
"""
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) :
txt_file = ( pathlib.Path (self.config.directories.factory_root)
/ App_State.imos_done_dir
/ f"{basket_no}.txt"
)
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)
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"
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 ()
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 ()
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 ()
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 _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
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
class Order_Detail (_With_Table_Widget_) :
settings_group = "order-detail"
Header = "Auftrags Detail"
Columns = \
("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, 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.baskets = {}
# end def __init__
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 ())
settings.setValue ("windowState", self.saveState ())
settings.endGroup ()
self._order_list .save_settings (settings)
self._order_detail.save_settings (settings)
# end def save_settings
def restore_settings (self, settings) :
settings.beginGroup ("order-display")
self.restoreGeometry (settings.value ("geometry" ))
self.restoreState (settings.value ("windowState"))
settings.endGroup ()
self._order_list .restore_settings (settings)
self._order_detail.restore_settings (settings)
# end def restore_settings
# end class Order_Display