Compare commits

...

8 Commits
v1.0.0 ... main

Author SHA1 Message Date
Thomas Türk f5a4342bcf Verbindungsaufbau nur wenn nötig 2023-11-13 12:36:21 +01:00
Thomas Türk e7fe3fb037 APplusJob implementiert 2023-08-23 17:29:38 +02:00
Thomas Türk 599339a270 getWebClient implementiert 2023-07-27 11:56:20 +02:00
Thomas Türk b05b5de039 add Changelog and Version info to documentation 2023-05-06 22:19:51 +02:00
Thomas Türk 3566c9ba3e apply flake8, remove Python 3.10 Syntax
make sure that the package works with older Python versions:

- replace matches with if-then-else
- Replace "|" with "Union"
- Remove "TypeAbbrev"

Make sure taht flake8 produces few warnings.

Add github action for automatic checks.
2023-05-06 21:54:58 +02:00
Thomas Türk d88469e711 ScriptTool erweitert und Beispiel hinzugefügt 2023-05-06 19:28:37 +02:00
Thomas Türk 77e472e016 Einbindung von Beispielen in Doku angepasst 2023-05-04 18:53:00 +02:00
Thomas Türk 4637a8c579 README erweitert 2023-05-04 17:12:07 +02:00
36 changed files with 2128 additions and 1094 deletions

2
.flake8 Normal file
View File

@ -0,0 +1,2 @@
[flake8]
max-line-length = 250

43
.github/workflows/python-package.yml vendored Normal file
View File

@ -0,0 +1,43 @@
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.7", "3.8", "3.9", "3.10"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
sudo apt update
sudo apt install -y unixodbc
python -m pip install --upgrade pip
python -m pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
pip install -e .
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest .

19
Changelog.md Normal file
View File

@ -0,0 +1,19 @@
# Changelog
## 23.08.2023 v1.1.1
- `APplusJob` für einfachen Zugriff auf `p2core.Job` implementiert
- Funktion für Ausführung von DB-Anpass Dateien implementiert
## 27.07.2023 v1.1.0
- implementiere Zugriff auf ASMX Seiten
- `getClient` -> `getAppClient` umbenannt
- `getWebClient` implementiert
- dies benötigt Paket `requests-negotiate-sspi`, das leider nur für Windows verfügbar ist
## 06.05.2023 v1.0.1
- Code-Cleanup mit Hilfe von flake8
- Bugfix: neue Python 3.10 Syntax entfernt
- kleinere Verbesserungen an Doku
## 04.05.2023 v1.0.0
erste veröffentlichte Version

View File

@ -1,21 +1,21 @@
# PyAPplus64
## Beschreibung
Das Paket `PyAPplus64` enthält eine Sammlung von Python Tools für die Interaktion mit dem ERP-System APplus 6.4.
Es sollte auch für andere APplus Versionen nützlich sein.
## Beschreibung
Das Paket `PyAPplus64` enthält eine Sammlung von Python Tools für die Interaktion mit dem ERP-System APplus 6.4.
Es sollte auch für andere APplus Versionen nützlich sein.
Zielgruppe sind APplus-Administratoren und Anpassungs-Entwickler. Die Tools erlauben u.a.
- einfacher Zugriff auf SOAP-Schnittstelle des App-Servers
+ damit Zugriff auf SysConfig
+ Zugriff auf Tools `nextNumber` für Erzeugung der nächsten Nummer für ein Business-Object
+ ...
+ ...
- Zugriff auf APplus DB per direktem DB-Zugriff und mittels SOAP
+ automatischer Aufruf von `completeSQL`, um per App-Server SQL-Statements um z.B. Mandanten erweitern zu lassen
+ Tools für einfache Benutzung von `useXML`, d.h. für das Einfügen, Löschen und Ändern von Datensätzen
mit Hilfe des APP-Servers. Genau wie bei Änderungen an Datensätzen über die Web-Oberfläche und im Gegensatz
zum direkten Zugriff über die Datenbank werden dabei evtl. zusätzliche
Checks ausgeführt, bestimmte Felder automatisch gesetzt oder bestimmte Aktionen angestoßen.
mit Hilfe des APP-Servers. Genau wie bei Änderungen an Datensätzen über die Web-Oberfläche und im Gegensatz
zum direkten Zugriff über die Datenbank werden dabei evtl. zusätzliche
Checks ausgeführt, bestimmte Felder automatisch gesetzt oder bestimmte Aktionen angestoßen.
- das Duplizieren von Datensätzen
+ zu kopierende Felder aus XML-Definitionen werden ausgewertet
+ Abhängige Objekte können einfach ebenfalls mit-kopiert werden
@ -35,20 +35,34 @@ aus, dass im Laufe der Zeit weitere Features hinzukommen.
## Warnung
`PyAPplus64` erlaubt den schreibenden Zugriff auf die APplus Datenbank und beliebige
Aufrufe von SOAP-Methoden. Unsachgemäße Nutzung kann Ihre Daten zerstören. Benutzen Sie
`PyAPplus64` daher bitte vorsichtig.
`PyAPplus64` erlaubt den schreibenden Zugriff auf die APplus Datenbank und beliebige
Aufrufe von SOAP-Methoden. Unsachgemäße Nutzung kann Ihre Daten zerstören. Benutzen Sie
`PyAPplus64` daher bitte vorsichtig.
## Lizenz
## Installation
PyAPplus64 wurde auf PyPi veröffentlicht. Es lässt sich daher einfach mittel `pip` installieren
````
pip install PyAPplus64
````
Zur Nutzung von ASMX-Seiten ist die Authentifizierungsmethode Negotiate nötig. Für diese muss `requests-negotiate-sspi` installiert werden,
was aber leider nur unter Windows verfügbar ist.
````
pip install requests-negotiate-sspi
````
`PyAPplus64` wurde unter MIT license veröffentlicht.
## Links
- Homepage https://www.thomas-tuerk.de/de/pyapplus64
- Doku
+ PDF https://www.thomas-tuerk.de/assets/PyAPplus64/pyapplus64.pdf
+ HTML https://www.thomas-tuerk.de/assets/PyAPplus64/html/index.html
- GIT-Repository https://git.thomas-tuerk.de/thtuerk/PyAPplus64
- PyPI https://pypi.org/project/PyAPplus64/
- [PyPi](https://pypi.org/project/PyAPplus64/)
- Doku [PDF](https://www.thomas-tuerk.de/assets/PyAPplus64/pyapplus64.pdf), [HTML](https://www.thomas-tuerk.de/assets/PyAPplus64/html/index.html)
- [GIT-Repository](https://git.thomas-tuerk.de/thtuerk/PyAPplus64)
- [GitHub](https://github.com/thtuerk/PyAPplus64)
## Lizenz / Mitarbeit
Ich habe PyAPplus64 unter MIT License veröffentlicht. Diese Lizenz gibt Ihnen weitreichende Rechte für die Nutzung von PyAPplus64, auch im kommerziellen Kontext. Ich bitte aber dringend darum, Ihre Änderungen, Erweiterungen und Fehlerkorrekturen auch anderen zur Verfügung zu stellen. Dafür können Sie die üblichen Methoden auf Github nutzen oder mir ([Thomas Türk](mailto:kontakt@thomas-tuerk.de)) eine eMail mit den Änderungen schicken.

View File

@ -3,8 +3,8 @@ Abhängigkeiten
pyodbc
------
Für die Datenbankverbindung wird ``pyodbc`` (``python -m pip install pyodbc``) verwendet.
Der passende ODBC Treiber, MS SQL Server 2012 Native Client, wird zusätzlich benötigt.
Für die Datenbankverbindung wird ``pyodbc`` (``python -m pip install pyodbc``) verwendet.
Der passende ODBC Treiber, MS SQL Server 2012 Native Client, wird zusätzlich benötigt.
Dieser kann von Microsoft bezogen werden.
@ -13,18 +13,25 @@ zeep
Die Soap-Library ``zeep`` wird benutzt (``python -m pip install zeep``).
requests-negotiate-sspi
-----------------------
Die Authentifzierungsmethode Negotiate Wird für Zugriffe auf ASMX-Seiten benutzt (``python -m pip install requests-negotiate-sspi``).
Leider ist dies nur unter Windows verfügbar. Alle anderen Funktionen können aber auch ohne
dieses Paket benutzt werden.
PyYaml
------
Die Library ``pyyaml`` wird für Config-Dateien benutzt (``python -m pip install pyyaml``).
Sphinx
Sphinx
------
Diese Dokumentation ist mit Sphinx geschrieben.
``python -m pip install sphinx``. Dokumentation ist im Unterverzeichnis
`docs` zu finden. Sie kann mittels ``make.bat html`` erzeugt werden,
dies ruft intern ``sphinx-build -M html source build`` auf. Die Dokumentation
Diese Dokumentation ist mit Sphinx geschrieben.
``python -m pip install sphinx``. Dokumentation ist im Unterverzeichnis
`docs` zu finden. Sie kann mittels ``make.bat html`` erzeugt werden,
dies ruft intern ``sphinx-build -M html source build`` auf. Die Dokumentation
der Python-API sollte evtl. vorher
mittels ``sphinx-apidoc -T -f ../src/PyAPplus64 -o source/generated`` erzeugt
oder aktualisiert werden. Evtl. können 2 Aufrufe von ``make.bat html`` sinnvoll
@ -36,5 +43,12 @@ Die erzeugte Doku findet sich im Verzeichnis ``build/html``.
Pandas / SqlAlchemy / xlsxwriter
--------------------------------
Sollen Excel-Dateien mit Pandas erzeugt, werden, so muss Pandas, SqlAlchemy und xlsxwriter installiert sein
(`python -m pip install pandas sqlalchemy xlsxwriter`).
Sollen Excel-Dateien mit Pandas erzeugt, werden, so muss Pandas, SqlAlchemy und xlsxwriter installiert sein
(`python -m pip install pandas sqlalchemy xlsxwriter`).
PySimpleGUI und andere
----------------------
Einige Beispiele benutzen PySimpleGUI (``python -m pip install pysimplegui``)
sowie teilweise spezielle Bibliotheken etwa zum Pretty-Printing von SQL (``python -m pip install sqlparse sqlfmt``). Dies
sind aber Abhängigkeiten von Beispielen, nicht der Bibliothek selbst.

View File

@ -4,10 +4,11 @@ typische Anwendungsfälle
einfache Admin-Aufgaben
-----------------------
Selten auftretende Admin-Aufgaben lassen sich gut mittels Python-Scripten automatisieren.
Es ist sehr einfach möglich, auf die DB, aber auch auf SOAP-Schnittstelle zuzugreifen.
Ich habe dies vor allem für Wartungsarbeiten an Anpassungstabellen, die für eigene Erweiterungen
entwickelt wurden, genutzt.
Selten auftretende Admin-Aufgaben lassen sich gut mittels Python-Scripten
automatisieren. Es ist sehr einfach möglich, auf die DB, aber auch auf
SOAP-Schnittstelle der APP-Serverse zuzugreifen. Zudem ist rudimentärer Zugriff
auf ASMX-Seiten implementiert. Ich habe dies vor allem für Wartungsarbeiten an
Anpassungstabellen genutzt, die für eigene Erweiterungen entwickelt wurden.
Als triviales Beispiel sucht folgender Code alle `DOCUMENTS` Einträge in
Artikeln (angezeigt als `Bild` in `ArtikelRec.aspx`), für die Datei, auf die
@ -17,6 +18,7 @@ ausgegeben und das Feld `DOCUMENTS` gelöscht. Das Löschen erfolgt dabei über
.. literalinclude:: ../../examples/check_dokumente.py
:language: python
:lines: 9-
:linenos:
Man kann alle Python Bibliotheken nutzen. Als Erweiterung wäre es in obigem Script
@ -41,6 +43,7 @@ welche Materialen wie oft für Artikel benutzt werden:
.. literalinclude:: ../../examples/adhoc_report.py
:language: python
:lines: 9-
:linenos:
Dieses kurze Script nutzt Standard-Pandas Methoden zur Erzeugung der Excel-Datei. Allerdings
@ -56,7 +59,7 @@ der für die Umgebung korrekte Mandant automatisch verwendet wird.
Anbindung eigener Tools
-----------------------
Ursprünglich wurde `PyAPplus64` für die Anbindung einer APplus-Anpassung geschrieben. Dieses ist
Ursprünglich wurde `PyAPplus64` für die Anbindung einer APplus-Anpassung geschrieben. Diese Anpassung ist
als Windows-Service auf einem eigenen Rechner installiert und überwacht dort ein bestimmtes Verzeichnis.
Bei Änderungen an Dateien in diesem Verzeichnis (Hinzufügen, Ändern, Löschen) werden die Dateien verarbeitet
und die Ergebnisse an APplus gemeldet. Dafür werden DB-Operationen aber auch SOAP-Calls benutzt.
@ -83,10 +86,10 @@ Zugriff auf die Sysconf möglich::
print (server.sysconf.getList("STAMM", "EULAENDER"))
Dank der Bibliothek `zeep` ist es auch sehr einfach möglich, auf beliebige SOAP-Methoden zuzugreifen.
Beispielsweise kann auf die Sys-Config auch händisch, d.h. durch direkten Aufruf einer SOAP-Methode,
zugegriffen werden::
Beispielsweise kann auf die Sys-Config auch händisch, d.h. durch direkten Aufruf einer SOAP-Methode
des APP-Servers zugegriffen werden::
client = server.server_conn.getClient("p2system", "SysConf");
client = server.getAppClient("p2system", "SysConf");
print (client.service.getString("STAMM", "MYLAND"))

View File

@ -14,6 +14,8 @@ sys.path.append('../src/')
project = 'PyAPplus64'
copyright = '2023, Thomas Tuerk'
author = 'Thomas Tuerk'
version = '1.1.2'
release = '1.1.2'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
@ -21,12 +23,12 @@ author = 'Thomas Tuerk'
extensions = [
'sphinx.ext.duration',
'sphinx.ext.doctest',
'sphinx.ext.autodoc',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
]
templates_path = ['_templates']
exclude_patterns = [] # type: ignore
exclude_patterns = [] # type: ignore
language = 'de'
@ -53,4 +55,4 @@ latex_elements = {
autodoc_type_aliases = {
'SqlValue': 'SqlValue'
}
}

View File

@ -13,24 +13,36 @@ das Deploy-, das Test- und das Prod-System. Ein Beispiel ist im Unterverzeichnis
.. literalinclude:: ../../examples/applus-server.yaml
:language: yaml
:lines: 9-
:linenos:
Damit nicht in jedem Script immer wieder neu die Konfig-Dateien ausgewählt werden müssen, werden die Konfigs für
das Prod-, Test- und Deploy-System in ``examples/applus_configs.py`` hinterlegt. Diese wird in allen Scripten importiert,
so dass das Config-Verzeichnis und die darin enthaltenen Configs einfach zur Verfügung stehen.
das Prod-, Test- und Deploy-System in ``examples/applus_configs.py`` hinterlegt. Diese Datei wird in allen Scripten importiert,
so dass das Config-Verzeichnis und die darin enthaltenen Configs einfach zur Verfügung stehen. Zudem werden in dieser Datei auch alle verwendeten
Kombinationen aus System und Umgebung hinterlegt. So kann in Scripten auch eine Auswahl des Systems implementiert werden.
.. literalinclude:: ../../examples/applus_configs.py
:language: python
:lines: 9-
:linenos:
``read_settings.py``
-----------------------
Einfaches Beispiel für Auslesen der SysConf und bestimmter Einstellungen.
.. literalinclude:: ../../examples/read_settings.py
:language: python
:lines: 9-
:linenos:
``check_dokumente.py``
-----------------------
Einfaches Beispiel für lesenden und schreibenden Zugriff auf APplus Datenbank.
.. literalinclude:: ../../examples/check_dokumente.py
:language: python
:lines: 6-
:lines: 9-
:linenos:
@ -40,7 +52,7 @@ Sehr einfaches Beispiel zur Erstellung einer Excel-Tabelle aus einer SQL-Abfrage
.. literalinclude:: ../../examples/adhoc_report.py
:language: python
:lines: 7-
:lines: 9-
:linenos:
@ -60,14 +72,52 @@ Die GUI wird um die Erzeugung von Excel-Dateien mit Mengenabweichungen gebaut.
.. literalinclude:: ../../examples/mengenabweichung_gui.pyw
:language: python
:lines: 7-
:lines: 9-
:linenos:
``complete_sql.pyw``
--------------------
Beispiel, wie ein einfacher APP-Server Aufruf über eine GUI zur Verfügung gestellt und mittels
Python-Bibliotheken erweitert werden kann. Zudem wird demonstriert, wie eine Auswahl verschiedenere
Systeme und Umgebungen realisiert werden kann.
.. literalinclude:: ../../examples/complete_sql.pyw
:language: python
:lines: 9-
:linenos:
``importViewUDF.py``
--------------------
Folgende Scripte erlauben den einfachen Import von DB-Anpass-Dateien, Views und UDFs über den Windows-Explorer.
Werden Verknüpfungen zu den Scripten ``importViewUDFDeploy.pyw`` und ``importViewUDFTest.pyw`` in ``%appdata%\Microsoft\Windows\SendTo`` abgelegt,
so können eine oder mehrerer solcher Dateien mittels _Kontextmenü (Rechtsklick) - Senden an_ an APplus zur Verarbeitung übergeben werden.
Dabei ist es wichtig, dass sich die Dateien im für den jeweiligen Typ passenden Verzeichnis befinden.
.. literalinclude:: ../../examples/importViewUDF.py
:language: python
:lines: 9-
:linenos:
Wrapper für Deploy-System:
.. literalinclude:: ../../examples/importViewUDFDeploy.pyw
:language: python
:lines: 9-
:linenos:
Wrapper für Test-System:
.. literalinclude:: ../../examples/importViewUDFTest.pyw
:language: python
:lines: 9-
:linenos:
``copy_artikel.py``
-----------------------
Beispiel, wie Artikel inklusive Arbeitsplan und Stückliste dupliziert werden kann.
.. literalinclude:: ../../examples/copy_artikel.py
:language: python
:lines: 21-
:lines: 22-
:linenos:

View File

@ -20,6 +20,14 @@ PyAPplus64.applus\_db module
:undoc-members:
:show-inheritance:
PyAPplus64.applus\_job module
-----------------------------
.. automodule:: PyAPplus64.applus_job
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.applus\_scripttool module
------------------------------------

View File

@ -10,10 +10,11 @@ import PyAPplus64
import applus_configs
import pathlib
def main(confFile : pathlib.Path, outfile : str) -> None:
server = PyAPplus64.applus.applusFromConfigFile(confFile)
# Einfache SQL-Anfrage
def main(confFile: pathlib.Path, outfile: str) -> None:
server = PyAPplus64.applus.applusFromConfigFile(confFile)
# Einfache SQL-Anfrage
sql1 = ("select Material, count(*) as Anzahl from ARTIKEL "
"group by MATERIAL having MATERIAL is not null "
"order by Anzahl desc")
@ -21,7 +22,7 @@ def main(confFile : pathlib.Path, outfile : str) -> None:
# Sql Select-Statements können auch über SqlStatementSelect zusammengebaut
# werden. Die ist bei vielen, komplizierten Bedingungen teilweise hilfreich.
sql2 = PyAPplus64.SqlStatementSelect("ARTIKEL")
sql2 = PyAPplus64.SqlStatementSelect("ARTIKEL")
sql2.addFields("Material", "count(*) as Anzahl")
sql2.addGroupBy("MATERIAL")
sql2.having.addConditionFieldIsNotNull("MATERIAL")
@ -33,4 +34,4 @@ def main(confFile : pathlib.Path, outfile : str) -> None:
if __name__ == "__main__":
main(applus_configs.serverConfYamlTest, "myout.xlsx")
main(applus_configs.serverConfYamlTest, "myout.xlsx")

View File

@ -16,7 +16,10 @@ appserver : {
env : "default-umgebung" # hier wirklich Umgebung, nicht Mandant verwenden
}
webserver : {
baseurl : "http://some-server/APplusProd6/"
baseurl : "http://some-server/APplusProd6/",
user : null, # oft "ASOL.Projects", wenn nicht gesetzt, wird aktueller Windows-Nutzer verwendet
userDomain : null, # Domain für ASOL.PROJECTS
password : null # das Passwort
}
dbserver : {
server : "some-server",

View File

@ -7,11 +7,26 @@
# https://opensource.org/licenses/MIT.
import pathlib
from PyAPplus64.applus import APplusServerConfigDescription
basedir = pathlib.Path(__file__)
basedir = basedir = pathlib.Path(__file__) # Adapt to your needs
configdir = basedir.joinpath("config")
serverConfYamlDeploy = configdir.joinpath("applus-server-deploy.yaml")
serverConfYamlTest = configdir.joinpath("applus-server-test.yaml")
serverConfYamlProd = configdir.joinpath("applus-server-prod.yaml")
serverConfDescProdEnv1 = APplusServerConfigDescription("Prod/Env1", serverConfYamlProd, env="Env1")
serverConfDescProdEnv2 = APplusServerConfigDescription("Prod/Env2", serverConfYamlProd, env="Env2")
serverConfDescTestEnv1 = APplusServerConfigDescription("Test/Env1", serverConfYamlTest, env="Env1")
serverConfDescTestEnv2 = APplusServerConfigDescription("Test/Env2", serverConfYamlTest, env="Env2")
serverConfDescDeploy = APplusServerConfigDescription("Deploy", serverConfYamlDeploy)
serverConfDescs = [
serverConfDescProdEnv1,
serverConfDescProdEnv2,
serverConfDescTestEnv1,
serverConfDescTestEnv2,
serverConfDescDeploy
]

View File

@ -9,23 +9,29 @@
import pathlib
import PyAPplus64
import applus_configs
from typing import Optional
def main(confFile : pathlib.Path, docDir:str, updateDB:bool) -> None:
server = PyAPplus64.applus.applusFromConfigFile(confFile)
sql = PyAPplus64.sql_utils.SqlStatementSelect("ARTIKEL");
sql.addFields("ID", "ARTIKEL", "DOCUMENTS");
sql.where.addConditionFieldStringNotEmpty("DOCUMENTS");
def main(confFile: pathlib.Path, updateDB: bool, docDir: Optional[str] = None) -> None:
server = PyAPplus64.applus.applusFromConfigFile(confFile)
if docDir is None:
docDir = str(server.scripttool.getInstallPathWebServer().joinpath("DocLib"))
sql = PyAPplus64.sql_utils.SqlStatementSelect("ARTIKEL")
sql.addFields("ID", "ARTIKEL", "DOCUMENTS")
sql.where.addConditionFieldStringNotEmpty("DOCUMENTS")
for row in server.dbQueryAll(sql):
doc = pathlib.Path(docDir + row.DOCUMENTS)
if not doc.exists():
print("Bild '{}' für Artikel '{}' nicht gefunden".format(doc, row.ARTIKEL))
if updateDB:
upd = server.mkUseXMLRowUpdate("ARTIKEL", row.ID)
upd.addField("DOCUMENTS", None)
upd.update()
for row in server.dbQueryAll(sql):
doc = pathlib.Path(docDir + row.DOCUMENTS);
if not doc.exists():
print("Bild '{}' für Artikel '{}' nicht gefunden".format(doc, row.ARTIKEL))
if updateDB:
upd = server.mkUseXMLRowUpdate("ARTIKEL", row.ID);
upd.addField("DOCUMENTS", None);
upd.update();
if __name__ == "__main__":
main(applus_configs.serverConfYamlTest, "somedir\\WebServer\\DocLib", False)
main(applus_configs.serverConfYamlTest, False)

105
examples/complete_sql.pyw Normal file
View File

@ -0,0 +1,105 @@
# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
#
# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64).
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
import PySimpleGUI as sg # type: ignore
import PyAPplus64
import applus_configs
import pathlib
from typing import Tuple, Optional, Union
try:
import sqlparse
except:
pass
try:
import sqlfmt.api
except:
pass
def prettyPrintSQL(format, sql):
try:
if format == "sqlfmt":
mode = sqlfmt.api.Mode(dialect_name="ClickHouse")
sqlPretty = sqlfmt.api.format_string(sql, mode)
return sqlPretty.replace("N '", "N'") # fix String Constants
elif format == "sqlparse-2":
return sqlparse.format(sql, reindent=True, keyword_case='upper')
elif format == "sqlparse":
return sqlparse.format(sql, reindent_aligned=True, keyword_case='upper')
else:
return sql
except e:
print (str(e))
return sql
def main() -> None:
monospaceFont = ("Courier New", 12)
sysenvs = applus_configs.serverConfDescs[:];
sysenvs.append("-");
layout = [
[sg.Button("Vervollständigen"), sg.Button("aus Clipboard", key="import"), sg.Button("nach Clipboard", key="export"), sg.Button("zurücksetzen", key="clear"), sg.Button("Beenden"),
sg.Text('System/Umgebung:'), sg.Combo(sysenvs, default_value="-", key="sysenv", readonly=True), sg.Text('Formatierung:'), sg.Combo(["-", "sqlfmt", "sqlparse", "sqlparse-2"], default_value="sqlparse", key="formatieren", readonly=True)
],
[sg.Text('Eingabe-SQL')],
[sg.Multiline(key='input', size=(150, 20), font=monospaceFont)],
[sg.Text('Ausgabe-SQL')],
[sg.Multiline(key='output', size=(150, 20), font=monospaceFont, horizontal_scroll=True)]
]
# server = PyAPplus64.applusFromConfigFile(confFile, user=user, env=env)
# systemName = server.scripttool.getSystemName() + "/" + server.scripttool.getMandant()
window = sg.Window("Complete SQL", layout)
oldSys = None
while True:
event, values = window.read()
if event == sg.WIN_CLOSED or event == 'Beenden':
break
elif event == 'clear':
window['input'].update("")
window['output'].update("")
elif event == 'import':
try:
window['input'].update(window.TKroot.clipboard_get())
except:
window['input'].update("")
window['output'].update("")
elif event == 'export':
try:
window.TKroot.clipboard_clear()
window.TKroot.clipboard_append(window['output'].get())
except:
pass
elif event == 'Vervollständigen':
sqlIn = window['input'].get()
try:
if sqlIn:
sys = window['sysenv'].get()
if sys != oldSys:
oldSys = sys
if sys and sys != "-":
server = sys.connect()
else:
server = None
if server:
sqlOut = server.completeSQL(sqlIn)
else:
sqlOut = sqlIn
sqlOut = prettyPrintSQL(window['formatieren'].get(), sqlOut)
else:
sqlOut = ""
except Exception as e:
sqlOut = "ERROR: " + str(e)
sg.popup_error_with_traceback("Fehler bei Vervollständigung", e)
window['output'].update(value=sqlOut)
window.close()
if __name__ == "__main__":
main()

View File

@ -15,7 +15,7 @@
#
# Dies ist für Administrationszwecke gedacht. Anwendungsbeispiel wäre,
# dass ein Artikel mit langem Arbeitsplan und Stückliste im Test-System erstellt wird.
# Viele der Positionen enthalten Nachauflöse-Scripte, die im Test-System
# Viele der Positionen enthalten Nachauflöse-Scripte, die im Test-System
# getestet werden. Diese vielen Scripte per Hand zu kopieren ist aufwändig
# und Fehleranfällig und kann mit solchen Admin-Scripten automatisiert werden.
@ -23,19 +23,20 @@ import pathlib
import PyAPplus64
import applus_configs
import logging
import yaml
import yaml
from typing import Optional
def main(confFile:pathlib.Path, artikel:str, artikelNeu:str|None=None) -> None:
def main(confFile: pathlib.Path, artikel: str, artikelNeu: Optional[str] = None) -> None:
# Server verbinden
server = PyAPplus64.applus.applusFromConfigFile(confFile)
server = PyAPplus64.applus.applusFromConfigFile(confFile)
# DuplicateBusinessObject für Artikel erstellen
dArt = PyAPplus64.duplicate.loadDBDuplicateArtikel(server, artikel);
dArt = PyAPplus64.duplicate.loadDBDuplicateArtikel(server, artikel)
# DuplicateBusinessObject zur Demonstration in YAML konvertieren und zurück
dArtYaml = yaml.dump(dArt);
print(dArtYaml);
dArtYaml = yaml.dump(dArt)
print(dArtYaml)
dArt2 = yaml.load(dArtYaml, Loader=yaml.UnsafeLoader)
# Neue Artikel-Nummer bestimmen und DuplicateBusinessObject in DB schreiben
@ -44,16 +45,15 @@ def main(confFile:pathlib.Path, artikel:str, artikelNeu:str|None=None) -> None:
artikelNeu = server.nextNumber("Artikel")
if not (dArt is None):
dArt.setFields({"artikel" : artikelNeu})
res = dArt.insert(server);
print(res);
dArt.setFields({"artikel": artikelNeu})
res = dArt.insert(server)
print(res)
if __name__ == "__main__":
# Logger Einrichten
logging.basicConfig(level=logging.INFO)
logging.basicConfig(level=logging.INFO)
# logger = logging.getLogger("PyAPplus64.applus_db");
# logger.setLevel(logging.ERROR)
main(applus_configs.serverConfYamlTest, "my-artikel", artikelNeu="my-artikel-copy")

75
examples/importViewUDF.py Normal file
View File

@ -0,0 +1,75 @@
# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
#
# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64).
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
import PySimpleGUI as sg # type: ignore
import pathlib
import PyAPplus64
from PyAPplus64 import applus
from PyAPplus64 import sql_utils
import applus_configs
import traceback
import pathlib
import sys
def importViewsUDFs(server, views, udfs, dbanpass):
res = ""
try:
if views or udfs:
for env in server.scripttool.getAllEnvironments():
res = res + server.importUdfsAndViews(env, views, udfs);
res = res + "\n\n";
for xml in dbanpass:
res = res + "Verarbeite " + xml + "\n"
xmlRes = server.updateDatabase(xml);
if (xmlRes == ""): xmlRes = "OK";
res = res + xmlRes
res = res + "\n\n"
sg.popup_scrolled("Importiere", res)
except:
sg.popup_error("Fehler", traceback.format_exc())
def importIntoSystem(server, system):
try:
if (len(sys.argv) < 2):
sg.popup_error("Keine Datei zum Import übergeben")
return
views = []
udfs = []
dbanpass = []
errors = ""
for i in range (1, len(sys.argv)):
arg = pathlib.Path(sys.argv[i])
if arg == server.scripttool.getInstallPathAppServer().joinpath("Database", "View", arg.stem + ".sql"):
views.append(arg.stem)
elif arg == server.scripttool.getInstallPathAppServer().joinpath("Database", "UDF", arg.stem + ".sql"):
udfs.append(arg.stem)
elif arg == server.scripttool.getInstallPathAppServer().joinpath("DBChange", arg.stem + ".xml"):
dbanpass.append(arg.stem + ".xml")
else:
errors = errors + " - " + str(arg) + "\n";
if len(errors) > 0:
msg = "Folgende Dateien sind keine View, UDF oder DB-Anpass-Dateien des "+system+"-Systems:\n" + errors;
sg.popup_error("Fehler", msg);
if views or udfs or dbanpass:
importViewsUDFs(server, views, udfs, dbanpass)
except:
sg.popup_error("Fehler", traceback.format_exc())
if __name__ == "__main__":
server = PyAPplus64.applusFromConfigFile(applus_configs.serverConfYamlDeploy)
importIntoSystem(server, "Deploy");

View File

@ -0,0 +1,15 @@
# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
#
# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64).
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
import importViewUDF
import applus_configs
import PyAPplus64
if __name__ == "__main__":
server = PyAPplus64.applusFromConfigFile(applus_configs.serverConfYamlDeploy)
importViewUDF.importIntoSystem(server, "Deploy")

View File

@ -0,0 +1,15 @@
# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
#
# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64).
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
import importViewUDF
import applus_configs
import PyAPplus64
if __name__ == "__main__":
server = PyAPplus64.applusFromConfigFile(applus_configs.serverConfYamlTest)
importViewUDF.importIntoSystem(server, "Test")

View File

@ -11,61 +11,64 @@
import datetime
import PyAPplus64
import applus_configs
import pandas as pd # type: ignore
import pandas as pd # type: ignore
import pathlib
from typing import *
from typing import Tuple, Union, Optional
def ladeAlleWerkstattauftragMengenabweichungen(
server:PyAPplus64.APplusServer,
cond:PyAPplus64.SqlCondition|str|None=None) -> pd.DataFrame:
sql = PyAPplus64.sql_utils.SqlStatementSelect("WAUFTRAG w");
server: PyAPplus64.APplusServer,
cond: Union[PyAPplus64.SqlCondition, str, None] = None) -> pd.DataFrame:
sql = PyAPplus64.sql_utils.SqlStatementSelect("WAUFTRAG w")
sql.addLeftJoin("personal p", "w.UPDUSER = p.PERSONAL")
sql.addFieldsTable("w", "ID", "BAUFTRAG", "POSITION")
sql.addFields("(w.MENGE-w.MENGE_IST) as MENGENABWEICHUNG")
sql.addFieldsTable("w", "MENGE", "MENGE_IST",
sql.addFieldsTable("w", "MENGE", "MENGE_IST",
"APLAN as ARTIKEL", "NAME as ARTIKELNAME")
sql.addFields("w.UPDDATE", "p.NAME as UPDNAME")
sql.where.addConditionFieldGe("w.STATUS", 5)
sql.where.addCondition("abs(w.MENGE-w.MENGE_IST) > 0.001")
sql.where.addCondition("abs(w.MENGE-w.MENGE_IST) > 0.001")
sql.where.addCondition(cond)
sql.order="w.UPDDATE"
dfOrg = PyAPplus64.pandas.pandasReadSql(server, sql);
sql.order = "w.UPDDATE"
dfOrg = PyAPplus64.pandas.pandasReadSql(server, sql)
# Add Links
df = dfOrg.copy();
df = df.drop(columns=["ID"]);
df = dfOrg.copy()
df = df.drop(columns=["ID"])
# df = df[['POSITION', 'BAUFTRAG', 'MENGE']] # reorder / filter columns
df['POSITION'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(dfOrg,
lambda r: r.POSITION,
df['POSITION'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(
dfOrg,
lambda r: r.POSITION,
lambda r: server.makeWebLinkWauftrag(
bauftrag=r.BAUFTRAG, accessid=r.ID))
df['BAUFTRAG'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(dfOrg,
lambda r: r.BAUFTRAG,
df['BAUFTRAG'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(
dfOrg,
lambda r: r.BAUFTRAG,
lambda r: server.makeWebLinkBauftrag(bauftrag=r.BAUFTRAG))
colNames = {
"BAUFTRAG" : "Betriebsauftrag",
"POSITION" : "Pos",
"MENGENABWEICHUNG" : "Mengenabweichung",
"MENGE" : "Menge",
"MENGE_IST" : "Menge-Ist",
"ARTIKEL" : "Artikel",
"ARTIKELNAME" : "Artikel-Name",
"UPDDATE" : "geändert am",
"UPDNAME" : "geändert von"
"BAUFTRAG": "Betriebsauftrag",
"POSITION": "Pos",
"MENGENABWEICHUNG": "Mengenabweichung",
"MENGE": "Menge",
"MENGE_IST": "Menge-Ist",
"ARTIKEL": "Artikel",
"ARTIKELNAME": "Artikel-Name",
"UPDDATE": "geändert am",
"UPDNAME": "geändert von"
}
df.rename(columns=colNames, inplace=True);
df.rename(columns=colNames, inplace=True)
return df
def ladeAlleWerkstattauftragPosMengenabweichungen(
server : PyAPplus64.APplusServer,
cond:PyAPplus64.SqlCondition|str|None=None) -> pd.DataFrame:
sql = PyAPplus64.sql_utils.SqlStatementSelect("WAUFTRAGPOS w");
server: PyAPplus64.APplusServer,
cond: Union[PyAPplus64.SqlCondition, str, None] = None) -> pd.DataFrame:
sql = PyAPplus64.sql_utils.SqlStatementSelect("WAUFTRAGPOS w")
sql.addLeftJoin("personal p", "w.UPDUSER = p.PERSONAL")
sql.addFieldsTable("w", "ID", "BAUFTRAG", "POSITION", "AG")
@ -74,49 +77,53 @@ def ladeAlleWerkstattauftragPosMengenabweichungen(
sql.addFields("w.UPDDATE", "p.NAME as UPDNAME")
sql.where.addConditionFieldEq("w.STATUS", 4)
sql.where.addCondition("abs(w.MENGE-w.MENGE_IST) > 0.001")
sql.where.addCondition("abs(w.MENGE-w.MENGE_IST) > 0.001")
sql.where.addCondition(cond)
sql.order="w.UPDDATE"
sql.order = "w.UPDDATE"
dfOrg = PyAPplus64.pandas.pandasReadSql(server, sql);
dfOrg = PyAPplus64.pandas.pandasReadSql(server, sql)
# Add Links
df = dfOrg.copy();
df = df.drop(columns=["ID"]);
df['POSITION'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(dfOrg,
lambda r: r.POSITION,
df = dfOrg.copy()
df = df.drop(columns=["ID"])
df['POSITION'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(
dfOrg,
lambda r: r.POSITION,
lambda r: server.makeWebLinkWauftrag(
bauftrag=r.BAUFTRAG, accessid=r.ID))
df['BAUFTRAG'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(dfOrg,
lambda r: r.BAUFTRAG,
df['BAUFTRAG'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(
dfOrg,
lambda r: r.BAUFTRAG,
lambda r: server.makeWebLinkBauftrag(bauftrag=r.BAUFTRAG))
df['AG'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(dfOrg,
lambda r: r.AG,
df['AG'] = PyAPplus64.pandas.mkHyperlinkDataframeColumn(
dfOrg,
lambda r: r.AG,
lambda r: server.makeWebLinkWauftragPos(
bauftrag=r.BAUFTRAG, position=r.POSITION, accessid=r.ID))
# Demo zum Hinzufügen einer berechneten Spalte
# df['BAUFPOSAG'] = PyAPplus64.pandas.mkDataframeColumn(dfOrg,
# df['BAUFPOSAG'] = PyAPplus64.pandas.mkDataframeColumn(dfOrg,
# lambda r: "{}.{} AG {}".format(r.BAUFTRAG, r.POSITION, r.AG))
# Rename Columns
colNames = {
"BAUFTRAG" : "Betriebsauftrag",
"POSITION" : "Pos",
"AG" : "AG",
"MENGENABWEICHUNG" : "Mengenabweichung",
"MENGE" : "Menge",
"MENGE_IST" : "Menge-Ist",
"ARTIKEL" : "Artikel",
"UPDDATE" : "geändert am",
"UPDNAME" : "geändert von"
"BAUFTRAG": "Betriebsauftrag",
"POSITION": "Pos",
"AG": "AG",
"MENGENABWEICHUNG": "Mengenabweichung",
"MENGE": "Menge",
"MENGE_IST": "Menge-Ist",
"ARTIKEL": "Artikel",
"UPDDATE": "geändert am",
"UPDNAME": "geändert von"
}
df.rename(columns=colNames, inplace=True);
df.rename(columns=colNames, inplace=True)
return df
def computeInYearMonthCond(field : str, year:int|None=None,
month:int|None=None) -> PyAPplus64.SqlCondition | None:
if not (year is None):
def computeInYearMonthCond(field: str, year: Optional[int] = None,
month: Optional[int] = None) -> Optional[PyAPplus64.SqlCondition]:
if not (year is None):
if month is None:
return PyAPplus64.sql_utils.SqlConditionDateTimeFieldInYear(field, year)
else:
@ -124,59 +131,67 @@ def computeInYearMonthCond(field : str, year:int|None=None,
else:
return None
def computeFileName(year:int|None=None, month:int|None=None) -> str:
if year is None:
return 'mengenabweichungen-all.xlsx';
def computeFileName(year: Optional[int] = None, month: Optional[int] = None) -> str:
if year is None:
return 'mengenabweichungen-all.xlsx'
else:
if month is None:
return 'mengenabweichungen-{:04d}.xlsx'.format(year);
return 'mengenabweichungen-{:04d}.xlsx'.format(year)
else:
return 'mengenabweichungen-{:04d}-{:02d}.xlsx'.format(year, month);
return 'mengenabweichungen-{:04d}-{:02d}.xlsx'.format(year, month)
def _exportInternal(server: PyAPplus64.APplusServer, fn:str,
cond:Union[PyAPplus64.SqlCondition, str, None]) -> int:
def _exportInternal(server: PyAPplus64.APplusServer, fn: str,
cond: Union[PyAPplus64.SqlCondition, str, None]) -> int:
df1 = ladeAlleWerkstattauftragMengenabweichungen(server, cond)
df2 = ladeAlleWerkstattauftragPosMengenabweichungen(server, cond)
print ("erzeuge " + fn);
print("erzeuge " + fn)
PyAPplus64.pandas.exportToExcel(fn, [(df1, "WAuftrag"), (df2, "WAuftrag-Pos")], addTable=True)
return len(df1.index) + len(df2.index)
def exportVonBis(server: PyAPplus64.APplusServer, fn:str,
von:datetime.datetime|None, bis:datetime.datetime|None) -> int:
cond = PyAPplus64.sql_utils.SqlConditionDateTimeFieldInRange("w.UPDDATE", von, bis)
return _exportInternal(server, fn, cond)
def exportYearMonth(server: PyAPplus64.APplusServer,
year:int|None=None, month:int|None=None) -> int:
cond=computeInYearMonthCond("w.UPDDATE", year=year, month=month)
def exportVonBis(server: PyAPplus64.APplusServer, fn: str,
von: Optional[datetime.datetime], bis: Optional[datetime.datetime]) -> int:
cond = PyAPplus64.sql_utils.SqlConditionDateTimeFieldInRange("w.UPDDATE", von, bis)
return _exportInternal(server, fn, cond)
def exportYearMonth(server: PyAPplus64.APplusServer,
year: Optional[int] = None, month: Optional[int] = None) -> int:
cond = computeInYearMonthCond("w.UPDDATE", year=year, month=month)
fn = computeFileName(year=year, month=month)
return _exportInternal(server, fn, cond)
def computePreviousMonthYear(cyear : int, cmonth :int) -> Tuple[int, int]:
def computePreviousMonthYear(cyear: int, cmonth: int) -> Tuple[int, int]:
if cmonth == 1:
return (cyear-1, 12)
else:
return (cyear, cmonth-1);
return (cyear, cmonth-1)
def computeNextMonthYear(cyear : int, cmonth :int) -> Tuple[int, int]:
def computeNextMonthYear(cyear: int, cmonth: int) -> Tuple[int, int]:
if cmonth == 12:
return (cyear+1, 1)
else:
return (cyear, cmonth+1);
return (cyear, cmonth+1)
def main(confFile: Union[str, pathlib.Path], user: Optional[str] = None, env: Optional[str] = None) -> None:
server = PyAPplus64.applusFromConfigFile(confFile, user=user, env=env)
def main(confFile : str|pathlib.Path, user:str|None=None, env:str|None=None) -> None:
server = PyAPplus64.applusFromConfigFile(confFile, user=user, env=env)
now = datetime.date.today()
(cmonth, cyear) = (now.month, now.year)
(pyear, pmonth) = computePreviousMonthYear(cyear, cmonth);
# Ausgaben
exportYearMonth(server, cyear, cmonth) # Aktueller Monat
exportYearMonth(server, pyear, pmonth) # Vorheriger Monat
(pyear, pmonth) = computePreviousMonthYear(cyear, cmonth)
# Ausgaben
exportYearMonth(server, cyear, cmonth) # Aktueller Monat
exportYearMonth(server, pyear, pmonth) # Vorheriger Monat
# export(cyear) # aktuelles Jahr
# export(cyear-1) # letztes Jahr
# export() # alles
if __name__ == "__main__":
main(applus_configs.serverConfYamlTest)

View File

@ -6,15 +6,16 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
import PySimpleGUI as sg # type: ignore
import PySimpleGUI as sg # type: ignore
import mengenabweichung
import datetime
import PyAPplus64
import applus_configs
import pathlib
from typing import *
from typing import Tuple, Optional, Union
def parseDate (dateS:str) -> Tuple[datetime.datetime|None, bool]:
def parseDate(dateS: str) -> Tuple[Optional[datetime.datetime], bool]:
if dateS is None or dateS == '':
return (None, True)
else:
@ -24,14 +25,17 @@ def parseDate (dateS:str) -> Tuple[datetime.datetime|None, bool]:
sg.popup_error("Fehler beim Parsen des Datums '{}'".format(dateS))
return (None, False)
def createFile(server:PyAPplus64.APplusServer, fileS:str, vonS:str, bisS:str)->None:
(von, vonOK) = parseDate(vonS)
if not vonOK: return
(bis, bisOK) = parseDate(bisS)
if not bisOK: return
if (fileS is None) or fileS == '':
def createFile(server: PyAPplus64.APplusServer, fileS: str, vonS: str, bisS: str) -> None:
(von, vonOK) = parseDate(vonS)
if not vonOK:
return
(bis, bisOK) = parseDate(bisS)
if not bisOK:
return
if (fileS is None) or fileS == '':
sg.popup_error("Es wurde keine Ausgabedatei ausgewählt.")
return
else:
@ -41,23 +45,24 @@ def createFile(server:PyAPplus64.APplusServer, fileS:str, vonS:str, bisS:str)->N
sg.popup_ok("{} Datensätze erfolgreich in Datei '{}' geschrieben.".format(c, file))
def main(confFile : str|pathlib.Path, user:str|None=None, env:str|None=None) -> None:
server = PyAPplus64.applusFromConfigFile(confFile, user=user, env=env)
def main(confFile: Union[str, pathlib.Path], user: Optional[str] = None, env: Optional[str] = None) -> None:
server = PyAPplus64.applusFromConfigFile(confFile, user=user, env=env)
layout = [
[sg.Text(('Bitte geben Sie an, für welchen Zeitraum die '
'Mengenabweichungen ausgegeben werden sollen:'))],
[sg.Text('Von (einschließlich)', size=(15,1)), sg.InputText(key='Von'),
sg.CalendarButton("Kalender", close_when_date_chosen=True,
[sg.Text('Von (einschließlich)', size=(15, 1)), sg.InputText(key='Von'),
sg.CalendarButton("Kalender", close_when_date_chosen=True,
target="Von", format='%d.%m.%Y')],
[sg.Text('Bis (ausschließlich)', size=(15,1)), sg.InputText(key='Bis'),
sg.CalendarButton("Kalender", close_when_date_chosen=True,
[sg.Text('Bis (ausschließlich)', size=(15, 1)), sg.InputText(key='Bis'),
sg.CalendarButton("Kalender", close_when_date_chosen=True,
target="Bis", format='%d.%m.%Y')],
[sg.Text('Ausgabedatei', size=(15,1)), sg.InputText(key='File'),
sg.FileSaveAs(button_text="wählen", target="File",
file_types = (('Excel Files', '*.xlsx'),),
default_extension = ".xlsx")],
[sg.Button("Aktueller Monat"), sg.Button("Letzter Monat"),
[sg.Text('Ausgabedatei', size=(15, 1)), sg.InputText(key='File'),
sg.FileSaveAs(button_text="wählen",
target="File",
file_types=(('Excel Files', '*.xlsx'),),
default_extension=".xlsx")],
[sg.Button("Aktueller Monat"), sg.Button("Letzter Monat"),
sg.Button("Aktuelles Jahr"), sg.Button("Letztes Jahr")],
[sg.Button("Speichern"), sg.Button("Beenden")]
]
@ -66,33 +71,34 @@ def main(confFile : str|pathlib.Path, user:str|None=None, env:str|None=None) ->
window = sg.Window("Mengenabweichung " + systemName, layout)
now = datetime.date.today()
(cmonth, cyear) = (now.month, now.year)
(pyear, pmonth) = mengenabweichung.computePreviousMonthYear(cyear, cmonth);
(nyear, nmonth) = mengenabweichung.computeNextMonthYear(cyear, cmonth);
(pyear, pmonth) = mengenabweichung.computePreviousMonthYear(cyear, cmonth)
(nyear, nmonth) = mengenabweichung.computeNextMonthYear(cyear, cmonth)
while True:
event, values = window.read()
if event == sg.WIN_CLOSED or event == 'Beenden':
break
if event == 'Aktueller Monat':
window['Von'].update(value="01.{:02d}.{:04d}".format(cmonth, cyear));
window['Bis'].update(value="01.{:02d}.{:04d}".format(nmonth, nyear));
window['Von'].update(value="01.{:02d}.{:04d}".format(cmonth, cyear))
window['Bis'].update(value="01.{:02d}.{:04d}".format(nmonth, nyear))
if event == 'Letzter Monat':
window['Von'].update(value="01.{:02d}.{:04d}".format(pmonth, pyear));
window['Bis'].update(value="01.{:02d}.{:04d}".format(cmonth, cyear));
window['Von'].update(value="01.{:02d}.{:04d}".format(pmonth, pyear))
window['Bis'].update(value="01.{:02d}.{:04d}".format(cmonth, cyear))
if event == 'Aktuelles Jahr':
window['Von'].update(value="01.01.{:04d}".format(cyear));
window['Bis'].update(value="01.01.{:04d}".format(cyear+1));
window['Von'].update(value="01.01.{:04d}".format(cyear))
window['Bis'].update(value="01.01.{:04d}".format(cyear+1))
if event == 'Letztes Jahr':
window['Von'].update(value="01.01.{:04d}".format(cyear-1));
window['Bis'].update(value="01.01.{:04d}".format(cyear));
window['Von'].update(value="01.01.{:04d}".format(cyear-1))
window['Bis'].update(value="01.01.{:04d}".format(cyear))
if event == 'Speichern':
try:
createFile(server, values.get('File', None),
createFile(server, values.get('File', None),
values.get('Von', None), values.get('Bis', None))
except Exception as e:
sg.popup_error_with_traceback("Beim Erzeugen der Excel-Datei trat ein Fehler auf:", e);
sg.popup_error_with_traceback("Beim Erzeugen der Excel-Datei trat ein Fehler auf:", e)
window.close()
if __name__ == "__main__":
main(applus_configs.serverConfYamlProd)

55
examples/read_settings.py Normal file
View File

@ -0,0 +1,55 @@
# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
#
# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64).
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
# Einfaches Script, das verschiedene Werte des Servers ausliest.
# Dies sind SysConfig-Einstellungen, aber auch der aktuelle Mandant,
# Systemnamen, ...
import pathlib
import PyAPplus64
import applus_configs
from typing import Optional, Union
def main(confFile: Union[str, pathlib.Path], user: Optional[str] = None, env: Optional[str] = None) -> None:
server = PyAPplus64.applusFromConfigFile(confFile, user=user, env=env)
print("\n\nSysConf Lookups:")
print(" Default Auftragsart:", server.sysconf.getString("STAMM", "DEFAULTAUFTRAGSART"))
print(" Auftragsarten:")
arten = server.sysconf.getList("STAMM", "AUFTRAGSART", sep='\n')
if not arten:
arten = []
for a in arten:
print(" - " + a)
print(" Firmen-Nr. automatisch vergeben:", server.sysconf.getBoolean("STAMM", "FIRMAAUTOMATIK"))
print(" Anzahl Artikelstellen:", server.sysconf.getInt("STAMM", "ARTKLASSIFNRLAENGE"))
print("\n\nScriptTool:")
print(" CurrentDate:", server.scripttool.getCurrentDate())
print(" CurrentTime:", server.scripttool.getCurrentTime())
print(" CurrentDateTime:", server.scripttool.getCurrentDateTime())
print(" LoginName:", server.scripttool.getLoginName())
print(" UserName:", server.scripttool.getUserName())
print(" UserFullName:", server.scripttool.getUserFullName())
print(" SystemName:", server.scripttool.getSystemName())
print(" Mandant:", server.scripttool.getMandant())
print(" MandantName:", server.scripttool.getMandantName())
print(" InstallPath:", server.scripttool.getInstallPath())
print(" InstallPathAppServer:", server.scripttool.getInstallPathAppServer())
print(" InstallPathWebServer:", server.scripttool.getInstallPathWebServer())
print(" ServerInfo - Version:", server.scripttool.getServerInfo().find("version").text)
client = server.getWebClient("dbenv/dbenv.asmx")
print("WEB Environment:", client.service.getEnvironment())
if __name__ == "__main__":
main(applus_configs.serverConfYamlTest)

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "PyAPplus64"
version = "1.0.0"
version = "1.1.2"
authors = [
{ name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" },
]
@ -24,7 +24,10 @@ dependencies = [
'SQLAlchemy',
'pandas',
'XlsxWriter',
'zeep'
'zeep',
'pysimplegui',
'sqlparse',
'sqlfmt'
]
[project.urls]

View File

@ -27,4 +27,4 @@ from .sql_utils import (
try:
from . import pandas
except:
pass
pass

View File

@ -6,29 +6,28 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
from . import applus_db
from . import applus_job
from . import applus_server
from . import applus_sysconf
from . import applus_scripttool
from . import applus_usexml
from . import sql_utils
import yaml
import json
import urllib.parse
from zeep import Client
import pyodbc # type: ignore
from typing import *
import pyodbc # type: ignore
from typing import TYPE_CHECKING, Optional, Any, Callable, Dict, Sequence, Set, List
if TYPE_CHECKING:
from _typeshed import FileDescriptorOrPath
class APplusServer:
"""
Verbindung zu einem APplus DB und App Server mit Hilfsfunktionen für den komfortablen Zugriff.
:param db_settings: die Einstellungen für die Verbindung mit der Datenbank
:type db_settings: APplusDBSettings
:param server_settings: die Einstellungen für die Verbindung mit dem APplus App Server
@ -36,46 +35,91 @@ class APplusServer:
:param web_settings: die Einstellungen für die Verbindung mit dem APplus Web Server
:type web_settings: APplusWebServerSettings
"""
def __init__(self, db_settings : applus_db.APplusDBSettings, server_settings : applus_server.APplusAppServerSettings, web_settings : applus_server.APplusWebServerSettings):
def __init__(self,
db_settings: applus_db.APplusDBSettings,
server_settings: applus_server.APplusServerSettings):
self.db_settings : applus_db.APplusDBSettings = db_settings
self.db_settings: applus_db.APplusDBSettings = db_settings
"""Die Einstellungen für die Datenbankverbindung"""
self.web_settings : applus_server.APplusWebServerSettings = web_settings
"""Die Einstellungen für die Datenbankverbindung"""
self.server_settings : applus_server.APplusServerSettings = server_settings
"""Einstellung für die Verbindung zum APP- und Webserver"""
self.db_conn = db_settings.connect()
"""
Eine pyodbc-Connection zur APplus DB. Diese muss genutzt werden, wenn mehrere Operationen in einer Transaktion
genutzt werden sollen. Ansonsten sind die Hilfsmethoden wie :meth:`APplusServer.dbQuery` zu bevorzugen.
Diese Connection kann in Verbindung mit den Funktionen aus :mod:`PyAPplus64.applus_db` genutzt werden.
"""
self._db_conn_pool = list()
"""Eine Liste bestehender DB-Verbindungen"""
self.server_conn : applus_server.APplusServerConnection = applus_server.APplusServerConnection(server_settings);
self.server_conn: applus_server.APplusServerConnection = applus_server.APplusServerConnection(server_settings)
"""erlaubt den Zugriff auf den AppServer"""
self.sysconf : applus_sysconf.APplusSysConf = applus_sysconf.APplusSysConf(self);
self.sysconf: applus_sysconf.APplusSysConf = applus_sysconf.APplusSysConf(self)
"""erlaubt den Zugriff auf die Sysconfig"""
self.scripttool : applus_scripttool.APplusScriptTool = applus_scripttool.APplusScriptTool(self);
self.job: applus_job.APplusJob = applus_job.APplusJob(self)
"""erlaubt Arbeiten mit Jobs"""
self.scripttool: applus_scripttool.APplusScriptTool = applus_scripttool.APplusScriptTool(self)
"""erlaubt den einfachen Zugriff auf Funktionen des ScriptTools"""
self.client_table = self.server_conn.getClient("p2core","Table");
self.client_xml = self.server_conn.getClient("p2core","XML");
self.client_nummer = self.server_conn.getClient("p2system", "Nummer")
self._client_table = None
self._client_xml = None
self._client_nummer = None
self._client_adaptdb= None
@property
def client_table(self) -> Client:
if not self._client_table:
self._client_table = self.getAppClient("p2core", "Table")
return self._client_table
@property
def client_xml(self) -> Client:
if not self._client_xml:
self._client_xml = self.getAppClient("p2core", "XML")
return self._client_xml
@property
def client_nummer(self) -> Client:
if not self._client_nummer:
self._client_nummer = self.getAppClient("p2system", "Nummer")
return self._client_nummer
@property
def client_adaptdb(self) -> Client:
if not self._client_adaptdb:
self._client_adaptdb = self.getAppClient("p2dbtools", "AdaptDB")
return self._client_adaptdb
def getDBConnection(self) -> pyodbc.Connection:
"""
Liefert eine pyodbc-Connection zur APplus DB. Diese muss genutzt werden, wenn mehrere Operationen in einer Transaktion
genutzt werden sollen. Ansonsten sind die Hilfsmethoden wie :meth:`APplusServer.dbQuery` zu bevorzugen.
Diese Connection kann in Verbindung mit den Funktionen aus :mod:`PyAPplus64.applus_db` genutzt werden.
Die Verbindung sollte nach Benutzung wieder freigegeben oder geschlossen werden.
"""
if self._db_conn_pool:
return self._db_conn_pool.pop()
else:
conn = self.db_settings.connect()
self._db_conn_pool.append(conn)
return conn
def releaseDBConnection(self, conn : pyodbc.Connection) -> None:
"""Gibt eine DB-Connection zur Wiederverwendung frei"""
self._db_conn_pool.append(conn)
def reconnectDB(self) -> None:
try:
self.db_conn.close()
except:
pass
self.db_conn = self.db_settings.connect()
for conn in self._db_conn_pool:
try:
conn.close()
except:
pass
self._db_conn_pool = list()
def completeSQL(self, sql : sql_utils.SqlStatement, raw:bool=False) -> str:
def completeSQL(self, sql: sql_utils.SqlStatement, raw: bool = False) -> str:
"""
Vervollständigt das SQL-Statement. Es wird z.B. der Mandant hinzugefügt.
:param sql: das SQL Statement
:type sql: sql_utils.SqlStatement
:param raw: soll completeSQL ausgeführt werden? Falls True, wird die Eingabe zurückgeliefert
@ -86,55 +130,78 @@ class APplusServer:
if raw:
return str(sql)
else:
return self.client_table.service.getCompleteSQL(sql);
return self.client_table.service.getCompleteSQL(sql)
def dbQueryAll(self, sql : sql_utils.SqlStatement, *args:Any, raw:bool=False,
apply:Optional[Callable[[pyodbc.Row],Any]]=None) -> Any:
"""Führt eine SQL Query aus und liefert alle Zeilen zurück. Das SQL wird zunächst
def dbQueryAll(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False,
apply: Optional[Callable[[pyodbc.Row], Any]] = None) -> Any:
"""Führt eine SQL Query aus und liefert alle Zeilen zurück. Das SQL wird zunächst
vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden."""
sqlC = self.completeSQL(sql, raw=raw);
return applus_db.rawQueryAll(self.db_conn, sqlC, *args, apply=apply)
sqlC = self.completeSQL(sql, raw=raw)
conn = self.getDBConnection()
res = applus_db.rawQueryAll(conn, sqlC, *args, apply=apply)
self.releaseDBConnection(conn)
return res
def dbQuerySingleValues(self, sql : sql_utils.SqlStatement, *args:Any, raw:bool=False) -> Sequence[Any]:
def dbQuerySingleValues(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Sequence[Any]:
"""Führt eine SQL Query aus, die nur eine Spalte zurückliefern soll."""
return self.dbQueryAll(sql, *args, raw=raw, apply=lambda r: r[0])
def dbQuery(self, sql : sql_utils.SqlStatement, f : Callable[[pyodbc.Row], None], *args : Any, raw:bool=False) -> None:
"""Führt eine SQL Query aus und führt für jede Zeile die übergeben Funktion aus.
def dbQuery(self, sql: sql_utils.SqlStatement, f: Callable[[pyodbc.Row], None], *args: Any, raw: bool = False) -> None:
"""Führt eine SQL Query aus und führt für jede Zeile die übergeben Funktion aus.
Das SQL wird zunächst vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden."""
sqlC = self.completeSQL(sql, raw=raw);
applus_db.rawQuery(self.db_conn, sqlC, f, *args)
sqlC = self.completeSQL(sql, raw=raw)
conn = self.getDBConnection()
res = applus_db.rawQuery(conn, sqlC, f, *args)
self.releaseDBConnection(conn)
return res
def dbQuerySingleRow(self, sql:sql_utils.SqlStatement, *args:Any, raw:bool=False) -> Optional[pyodbc.Row]:
def dbQuerySingleRow(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Optional[pyodbc.Row]:
"""Führt eine SQL Query aus, die maximal eine Zeile zurückliefern soll. Diese Zeile wird geliefert."""
sqlC = self.completeSQL(sql, raw=raw);
return applus_db.rawQuerySingleRow(self.db_conn, sqlC, *args)
sqlC = self.completeSQL(sql, raw=raw)
conn = self.getDBConnection()
res = applus_db.rawQuerySingleRow(conn, sqlC, *args)
self.releaseDBConnection(conn)
return res
def dbQuerySingleRowDict(self, sql:sql_utils.SqlStatement, *args:Any, raw:bool=False) -> Optional[Dict[str, Any]]:
"""Führt eine SQL Query aus, die maximal eine Zeile zurückliefern soll.
def dbQuerySingleRowDict(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Optional[Dict[str, Any]]:
"""Führt eine SQL Query aus, die maximal eine Zeile zurückliefern soll.
Diese Zeile wird als Dictionary geliefert."""
row = self.dbQuerySingleRow(sql, *args, raw=raw);
row = self.dbQuerySingleRow(sql, *args, raw=raw)
if row:
return applus_db.row_to_dict(row);
return applus_db.row_to_dict(row)
else:
return None
def dbQuerySingleValue(self, sql:sql_utils.SqlStatement, *args:Any, raw:bool=False) -> Any:
"""Führt eine SQL Query aus, die maximal einen Wert zurückliefern soll.
def dbQuerySingleValue(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Any:
"""Führt eine SQL Query aus, die maximal einen Wert zurückliefern soll.
Dieser Wert oder None wird geliefert."""
sqlC = self.completeSQL(sql, raw=raw);
return applus_db.rawQuerySingleValue(self.db_conn, sqlC, *args)
sqlC = self.completeSQL(sql, raw=raw)
conn = self.getDBConnection()
res = applus_db.rawQuerySingleValue(conn, sqlC, *args)
self.releaseDBConnection(conn)
return res
def isDBTableKnown(self, table : str) -> bool:
def dbExecute(self, sql: sql_utils.SqlStatement, *args: Any, raw: bool = False) -> Any:
"""Führt ein SQL Statement (z.B. update oder insert) aus. Das SQL wird zunächst
vom Server angepasst, so dass z.B. Mandanteninformation hinzugefügt werden."""
sqlC = self.completeSQL(sql, raw=raw)
conn = self.getDBConnection()
res = applus_db.rawExecute(conn, sqlC, *args)
self.releaseDBConnection(conn)
return res
def isDBTableKnown(self, table: str) -> bool:
"""Prüft, ob eine Tabelle im System bekannt ist"""
sql = "select count(*) from SYS.TABLES T where T.NAME=?"
c = self.dbQuerySingleValue(sql, table);
c = self.dbQuerySingleValue(sql, table)
return (c > 0)
def getClient(self, package : str, name : str) -> Client:
"""Erzeugt einen zeep - Client.
Mittels dieses Clients kann die WSDL Schnittstelle angesprochen werden.
Wird als *package* "p2core" und als *name* "Table" verwendet und der
def getAppClient(self, package: str, name: str) -> Client:
"""Erzeugt einen zeep - Client für den APP-Server.
Mittels dieses Clients kann eines WSDL Schnittstelle des APP-Servers angesprochen werden.
Wird als *package* "p2core" und als *name* "Table" verwendet und der
resultierende client "client" genannt, dann kann
z.B. mittels "client.service.getCompleteSQL(sql)" vom AppServer ein Vervollständigen
des SQLs angefordert werden.
@ -146,9 +213,24 @@ class APplusServer:
:return: den Client
:rtype: Client
"""
return self.server_conn.getClient(package, name);
return self.server_conn.getAppClient(package, name)
def getTableFields(self, table:str, isComputed:Optional[bool]=None) -> Set[str]:
def getWebClient(self, url: str) -> Client:
"""Erzeugt einen zeep - Client für den Web-Server.
Mittels dieses Clients kann die von einer ASMX-Seite zur Verfügung gestellte Schnittstelle angesprochen werden.
Als parameter wird die relative URL der ASMX-Seite erwartet. Die Base-URL automatisch ergänzt.
Ein Beispiel für eine solche relative URL ist "masterdata/artikel.asmx".
ACHTUNG: Als Umgebung wird die Umgebung des sich anmeldenden Nutzers verwendet. Sowohl Nutzer als auch Umgebung können sich von den für App-Clients verwendeten Werten unterscheiden. Wenn möglich, sollte ein App-Client verwendet werden.
:param url: die relative URL der ASMX Seite, z.B. "masterdata/artikel.asmx"
:type package: str
:return: den Client
:rtype: Client
"""
return self.server_conn.getWebClient(url)
def getTableFields(self, table: str, isComputed: Optional[bool] = None) -> Set[str]:
"""
Liefert die Namen aller Felder einer Tabelle.
@ -158,34 +240,35 @@ class APplusServer:
:rtype: {str}
"""
sql = sql_utils.SqlStatementSelect("SYS.TABLES T")
join = sql.addInnerJoin("SYS.COLUMNS C");
join = sql.addInnerJoin("SYS.COLUMNS C")
join.on.addConditionFieldsEq("T.Object_ID", "C.Object_ID")
if not (isComputed == None):
join.on.addConditionFieldEq("c.is_computed", isComputed)
if not (isComputed is None):
join.on.addConditionFieldEq("c.is_computed", isComputed)
sql.addFields("C.NAME")
sql.where.addConditionFieldEq("t.name", sql_utils.SqlParam())
return sql_utils.normaliseDBfieldSet(self.dbQueryAll(sql, table, apply=lambda r : r.NAME));
return sql_utils.normaliseDBfieldSet(self.dbQueryAll(sql, table, apply=lambda r: r.NAME))
def getUniqueFieldsOfTable(self, table : str) -> Dict[str, List[str]]:
def getUniqueFieldsOfTable(self, table: str) -> Dict[str, List[str]]:
"""
Liefert alle Spalten einer Tabelle, die eindeutig sein müssen.
Diese werden als Dictionary gruppiert nach Index-Namen geliefert.
Jeder Eintrag enthält eine Liste von Feldern, die zusammen eindeutig sein
Liefert alle Spalten einer Tabelle, die eindeutig sein müssen.
Diese werden als Dictionary gruppiert nach Index-Namen geliefert.
Jeder Eintrag enthält eine Liste von Feldern, die zusammen eindeutig sein
müssen.
"""
return applus_db.getUniqueFieldsOfTable(self.db_conn, table)
conn = self.getDBConnection()
res = applus_db.getUniqueFieldsOfTable(conn, table)
self.releaseDBConnection(conn)
return res
def useXML(self, xml: str) -> Any:
"""Ruft ``p2core.xml.usexml`` auf. Wird meist durch ein ``UseXMLRow-Objekt`` aufgerufen."""
return self.client_xml.service.useXML(xml)
def useXML(self, xml : str) -> Any:
"""Ruft ``p2core.xml.usexml`` auf. Wird meist durch ein ``UseXMLRow-Objekt`` aufgerufen."""
return self.client_xml.service.useXML(xml);
def mkUseXMLRowInsert(self, table : str) -> applus_usexml.UseXmlRowInsert:
def mkUseXMLRowInsert(self, table: str) -> applus_usexml.UseXmlRowInsert:
"""
Erzeugt ein Objekt zum Einfügen eines neuen DB-Eintrags.
:param table: DB-Tabelle in die eingefügt werden soll
:type table: str
:return: das XmlRow-Objekt
@ -194,13 +277,13 @@ class APplusServer:
return applus_usexml.UseXmlRowInsert(self, table)
def mkUseXMLRowUpdate(self, table : str, id : int) -> applus_usexml.UseXmlRowUpdate:
def mkUseXMLRowUpdate(self, table: str, id: int) -> applus_usexml.UseXmlRowUpdate:
return applus_usexml.UseXmlRowUpdate(self, table, id)
def mkUseXMLRowInsertOrUpdate(self, table : str) -> applus_usexml.UseXmlRowInsertOrUpdate:
def mkUseXMLRowInsertOrUpdate(self, table: str) -> applus_usexml.UseXmlRowInsertOrUpdate:
"""
Erzeugt ein Objekt zum Einfügen oder Updaten eines DB-Eintrags.
:param table: DB-Tabelle in die eingefügt werden soll
:type table: string
:return: das XmlRow-Objekt
@ -209,70 +292,123 @@ class APplusServer:
return applus_usexml.UseXmlRowInsertOrUpdate(self, table)
def mkUseXMLRowDelete(self, table: str, id: int) -> applus_usexml.UseXmlRowDelete:
return applus_usexml.UseXmlRowDelete(self, table, id)
def mkUseXMLRowDelete(self, table:str, id:int) -> applus_usexml.UseXmlRowDelete :
return applus_usexml.UseXmlRowDelete(self, table, id)
def execUseXMLRowDelete(self, table:str, id:int) -> None:
def execUseXMLRowDelete(self, table: str, id: int) -> None:
delRow = self.mkUseXMLRowDelete(table, id)
delRow.delete();
delRow.delete()
def nextNumber(self, obj : str) -> str:
def nextNumber(self, obj: str) -> str:
"""
Erstellt eine neue Nummer für das Objekt und legt diese Nummer zurück.
"""
return self.client_nummer.service.nextNumber(obj)
def makeWebLink(self, base : str, **kwargs : Any) -> str :
if not self.web_settings.baseurl:
raise Exception("keine Webserver-BaseURL gesetzt");
url = str(self.web_settings.baseurl) + base;
def updateDatabase(self, file : str) -> str:
"""
Führt eine DBAnpass-xml Datei aus.
:param file: DB-Anpass Datei, die ausgeführt werden soll
:type file: string
:return: Infos zur Ausführung
:rtype: str
"""
jobId = self.job.createSOAPJob("run DBAnpass " + file);
self.client_adaptdb.service.updateDatabase(jobId, "de", file);
res = self.job.getResultURLString(jobId)
if res is None: res = "FEHLER";
if (res == "Folgende Befehle konnten nicht ausgeführt werden:\n\n"):
return ""
else:
return res
def importUdfsAndViews(self, environment : str, views : [str] = [], udfs : [str] = []) -> str:
"""
Importiert bestimmte Views und UDFs
:param environment: die Umgebung, in die Importiert werden soll
:type environment: string
:param views: Views, die importiert werden sollen
:type views: [string]
:param udfs: Views, die importiert werden sollen
:type udfs: [string]
:return: Infos zur Ausführung
:rtype: str
"""
lbl="";
files=[];
for v in views:
files.append({"type" : 1, "name" : v})
for u in udfs:
files.append({"type" : 0, "name" : u})
jobId = self.job.createSOAPJob("importing UDFs and Views");
self.client_adaptdb.service.importUdfsAndViews(jobId, environment, False, json.dumps(files), "de");
res = self.job.getResultURLString(jobId)
if res is None: res = "FEHLER";
return res
def makeWebLink(self, base: str, **kwargs: Any) -> str:
if not self.server_settings.webserver:
raise Exception("keine Webserver-BaseURL gesetzt")
url = str(self.server_settings.webserver) + base
firstArg = True
for arg, argv in kwargs.items():
if not (argv == None):
if not (argv is None):
if firstArg:
firstArg = False;
firstArg = False
url += "?"
else:
url += "&"
url += arg + "=" + urllib.parse.quote(str(argv))
return url;
return url
def makeWebLinkWauftragPos(self, **kwargs : Any) -> str:
return self.makeWebLink("wp/wauftragPosRec.aspx", **kwargs);
def makeWebLinkWauftragPos(self, **kwargs: Any) -> str:
return self.makeWebLink("wp/wauftragPosRec.aspx", **kwargs)
def makeWebLinkWauftrag(self, **kwargs : Any) -> str :
return self.makeWebLink("wp/wauftragRec.aspx", **kwargs);
def makeWebLinkWauftrag(self, **kwargs: Any) -> str:
return self.makeWebLink("wp/wauftragRec.aspx", **kwargs)
def makeWebLinkBauftrag(self, **kwargs : Any) -> str :
return self.makeWebLink("wp/bauftragRec.aspx", **kwargs);
def makeWebLinkBauftrag(self, **kwargs: Any) -> str:
return self.makeWebLink("wp/bauftragRec.aspx", **kwargs)
def makeWebLinkAuftrag(self, **kwargs: Any) -> str:
return self.makeWebLink("sales/auftragRec.aspx", **kwargs)
def makeWebLinkVKRahmen(self, **kwargs: Any) -> str:
return self.makeWebLink("sales/vkrahmenRec.aspx", **kwargs)
def applusFromConfigDict(yamlDict:Dict[str, Any], user:Optional[str]=None, env:Optional[str]=None) -> APplusServer:
def makeWebLinkWarenaugang(self, **kwargs: Any) -> str:
return self.makeWebLink("sales/warenausgangRec.aspx", **kwargs)
def applusFromConfigDict(yamlDict: Dict[str, Any], user: Optional[str] = None, env: Optional[str] = None) -> APplusServer:
"""Läd Einstellungen aus einer Config und erzeugt daraus ein APplus-Objekt"""
if user is None or user=='':
user = yamlDict["appserver"]["user"]
if env is None or env=='':
env = yamlDict["appserver"]["env"]
app_server = applus_server.APplusAppServerSettings(
appserver=yamlDict["appserver"]["server"],
appserverPort=yamlDict["appserver"]["port"],
user=user, # type: ignore
env=env)
web_server = applus_server.APplusWebServerSettings(
baseurl=yamlDict.get("webserver", {}).get("baseurl", None)
)
if user is None or user == '':
user = yamlDict["appserver"]["user"]
if env is None or env == '':
env = yamlDict["appserver"]["env"]
server_settings = applus_server.APplusServerSettings(
webserver=yamlDict.get("webserver", {}).get("baseurl", None),
appserver=yamlDict["appserver"]["server"],
appserverPort=yamlDict["appserver"]["port"],
user=user, # type: ignore
env=env,
webserverUser=yamlDict.get("webserver", {}).get("user", None),
webserverUserDomain=yamlDict.get("webserver", {}).get("userDomain", None),
webserverPassword=yamlDict.get("webserver", {}).get("password", None))
dbparams = applus_db.APplusDBSettings(
server=yamlDict["dbserver"]["server"],
server=yamlDict["dbserver"]["server"],
database=yamlDict["dbserver"]["db"],
user=yamlDict["dbserver"]["user"],
password=yamlDict["dbserver"]["password"]);
return APplusServer(dbparams, app_server, web_server);
user=yamlDict["dbserver"]["user"],
password=yamlDict["dbserver"]["password"])
return APplusServer(dbparams, server_settings)
def applusFromConfigFile(yamlfile : 'FileDescriptorOrPath',
user:Optional[str]=None, env:Optional[str]=None) -> APplusServer:
def applusFromConfigFile(yamlfile: 'FileDescriptorOrPath',
user: Optional[str] = None, env: Optional[str] = None) -> APplusServer:
"""Läd Einstellungen aus einer Config-Datei und erzeugt daraus ein APplus-Objekt"""
yamlDict = {}
with open(yamlfile, "r") as stream:
@ -280,8 +416,44 @@ def applusFromConfigFile(yamlfile : 'FileDescriptorOrPath',
return applusFromConfigDict(yamlDict, user=user, env=env)
def applusFromConfig(yamlString : str, user:Optional[str]=None, env:Optional[str]=None) -> APplusServer:
def applusFromConfig(yamlString: str, user: Optional[str] = None, env: Optional[str] = None) -> APplusServer:
"""Läd Einstellungen aus einer Config-Datei und erzeugt daraus ein APplus-Objekt"""
yamlDict = yaml.safe_load(yamlString)
return applusFromConfigDict(yamlDict, user=user, env=env)
class APplusServerConfigDescription:
"""
Beschreibung einer Configuration bestehend aus Config-Datei, Nutzer und Umgebung.
:param descr: Beschreibung als String, nur für Ausgabe gedacht
:type descr: str
:param yamlfile: die Datei
:type yamlfile: 'FileDescriptorOrPath'
:param user: der Nutzer
:type user: Optional[str]
:param env: die Umgebung
:type env: Optional[str]
"""
def __init__(self,
descr: str,
yamlfile: 'FileDescriptorOrPath',
user:Optional[str] = None,
env:Optional[str] = None):
self.descr = descr
self.yamlfile = yamlfile
self.user = user
self.env = env
def __str__(self) -> str:
return self.descr
def connect(self) -> APplusServer:
return applusFromConfigFile(self.yamlfile, user=self.user, env=self.env)

View File

@ -6,29 +6,27 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
import pyodbc # type: ignore
import pyodbc # type: ignore
import logging
from .sql_utils import SqlStatement
from . import sql_utils
from typing import *
from typing import List, Dict, Set, Any, Optional, Callable, Sequence
logger = logging.getLogger(__name__);
logger = logging.getLogger(__name__)
class APplusDBSettings:
"""
Einstellungen, mit welcher DB sich verbunden werden soll.
"""
def __init__(self, server : str, database : str, user : str, password : str):
def __init__(self, server: str, database: str, user: str, password: str):
self.server = server
self.database = database;
self.database = database
self.user = user
self.password = password
def getConnectionString(self) -> str:
"""Liefert den ODBC Connection-String für die Verbindung.
:return: den Connection-String
@ -37,7 +35,7 @@ class APplusDBSettings:
"Server="+self.server+";"
"Database="+self.database+";"
"UID="+self.user+";"
"PWD="+self.password + ";")
"PWD="+self.password + ";")
def connect(self) -> pyodbc.Connection:
"""Stellt eine neue Verbindung her und liefert diese zurück.
@ -45,22 +43,23 @@ class APplusDBSettings:
return pyodbc.connect(self.getConnectionString())
def row_to_dict(row : pyodbc.Row) -> Dict[str, Any]:
def row_to_dict(row: pyodbc.Row) -> Dict[str, Any]:
"""Konvertiert eine Zeile in ein Dictionary"""
return dict(zip([t[0] for t in row.cursor_description], row))
def _logSQLWithArgs(sql : SqlStatement, *args : Any) -> None:
def _logSQLWithArgs(sql: SqlStatement, *args: Any) -> None:
if args:
logger.debug("executing '{}' with args {}".format(str(sql), str(args)))
else:
logger.debug("executing '{}'".format(str(sql)))
def rawQueryAll(
cnxn : pyodbc.Connection,
sql : SqlStatement,
*args : Any,
apply : Optional[Callable[[pyodbc.Row], Any]]=None) -> Sequence[Any]:
cnxn: pyodbc.Connection,
sql: SqlStatement,
*args: Any,
apply: Optional[Callable[[pyodbc.Row], Any]] = None) -> Sequence[Any]:
"""
Führt eine SQL Query direkt aus und liefert alle Zeilen zurück.
Wenn apply gesetzt ist, wird die Funktion auf jeder Zeile ausgeführt und das Ergebnis ausgeben, die nicht None sind.
@ -69,48 +68,57 @@ def rawQueryAll(
with cnxn.cursor() as cursor:
cursor.execute(str(sql), *args)
rows = cursor.fetchall();
rows = cursor.fetchall()
if apply is None:
return rows
else:
res = []
for r in rows:
rr = apply(r)
if not (rr == None):
if not (rr is None):
res.append(rr)
return res
def rawQuery(cnxn : pyodbc.Connection, sql : sql_utils.SqlStatement, f : Callable[[pyodbc.Row], None], *args : Any) -> None:
def rawQuery(cnxn: pyodbc.Connection, sql: sql_utils.SqlStatement, f: Callable[[pyodbc.Row], None], *args: Any) -> None:
"""Führt eine SQL Query direkt aus und führt für jede Zeile die übergeben Funktion aus."""
_logSQLWithArgs(sql, *args)
with cnxn.cursor() as cursor:
cursor.execute(str(sql), *args)
for row in cursor:
f(row);
f(row)
def rawQuerySingleRow(cnxn : pyodbc.Connection, sql : SqlStatement, *args : Any) -> Optional[pyodbc.Row]:
def rawQuerySingleRow(cnxn: pyodbc.Connection, sql: SqlStatement, *args: Any) -> Optional[pyodbc.Row]:
"""Führt eine SQL Query direkt aus, die maximal eine Zeile zurückliefern soll. Diese Zeile wird geliefert."""
_logSQLWithArgs(sql, *args)
with cnxn.cursor() as cursor:
cursor.execute(str(sql), *args)
return cursor.fetchone();
return cursor.fetchone()
def rawQuerySingleValue(cnxn : pyodbc.Connection, sql : SqlStatement, *args : Any) -> Any:
def rawQuerySingleValue(cnxn: pyodbc.Connection, sql: SqlStatement, *args: Any) -> Any:
"""Führt eine SQL Query direkt aus, die maximal einen Wert zurückliefern soll. Dieser Wert oder None wird geliefert."""
_logSQLWithArgs(sql, *args)
with cnxn.cursor() as cursor:
cursor.execute(str(sql), *args)
row = cursor.fetchone();
row = cursor.fetchone()
if row:
return row[0];
return row[0]
else:
return None;
return None
def getUniqueFieldsOfTable(cnxn : pyodbc.Connection, table : str) -> Dict[str, List[str]] :
def rawExecute(cnxn: pyodbc.Connection, sql: SqlStatement, *args: Any) -> Any:
"""Führt ein SQL Statement direkt aus"""
_logSQLWithArgs(sql, *args)
with cnxn.cursor() as cursor:
return cursor.execute(str(sql), *args)
def getUniqueFieldsOfTable(cnxn: pyodbc.Connection, table: str) -> Dict[str, List[str]]:
"""
Liefert alle Spalten einer Tabelle, die eindeutig sein müssen.
Diese werden als Dictionary gruppiert nach Index-Namen geliefert.
Jeder Eintrag enthält eine Liste von Feldern, die zusammen eindeutig sein
Liefert alle Spalten einer Tabelle, die eindeutig sein müssen.
Diese werden als Dictionary gruppiert nach Index-Namen geliefert.
Jeder Eintrag enthält eine Liste von Feldern, die zusammen eindeutig sein
müssen.
"""
@ -123,7 +131,7 @@ def getUniqueFieldsOfTable(cnxn : pyodbc.Connection, table : str) -> Dict[str, L
sql.addFields("i.name AS INDEX_NAME", "COL_NAME(ic.OBJECT_ID,ic.column_id) AS COL")
_logSQLWithArgs(sql)
indices : Dict[str, List[str]] = {}
indices: Dict[str, List[str]] = {}
with cnxn.cursor() as cursor:
cursor.execute(str(sql))
for row in cursor:
@ -135,31 +143,31 @@ def getUniqueFieldsOfTable(cnxn : pyodbc.Connection, table : str) -> Dict[str, L
class DBTableIDs():
"""Klasse, die Mengen von IDs gruppiert nach Tabellen speichert"""
def __init__(self) -> None:
self.data : Dict[str, Set[int]]= {}
def add(self, table:str, *ids : int) -> None:
def __init__(self) -> None:
self.data: Dict[str, Set[int]] = {}
def add(self, table: str, *ids: int) -> None:
"""
fügt Eintrag hinzu
:param table: die Tabelle
:type table: str
:param id: die ID
"""
table = table.upper()
if not (table in self.data):
self.data[table] = set(ids);
self.data[table] = set(ids)
else:
self.data[table].update(ids)
def getTable(self, table : str) -> Set[int]:
def getTable(self, table: str) -> Set[int]:
"""
Liefert die Menge der IDs für eine bestimmte Tabelle.
:param table: die Tabelle
:type table: str
:return: die IDs
:return: die IDs
"""
table = table.upper()
@ -167,5 +175,3 @@ class DBTableIDs():
def __str__(self) -> str:
return str(self.data)

View File

@ -0,0 +1,203 @@
# Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
#
# This file is part of PyAPplus64 (see https://www.thomas-tuerk.de/de/pyapplus64).
#
# Use of this source code is governed by an MIT-style
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
from typing import TYPE_CHECKING, Optional
from zeep import Client
import uuid
if TYPE_CHECKING:
from .applus import APplusServer
class APplusJob:
"""
Zugriff auf Jobs
:param server: die Verbindung zum Server
:type server: APplusServer
"""
def __init__(self, server: 'APplusServer') -> None:
self.server = server
self._client = None
@property
def client(self) -> Client:
if not self._client:
self._client = self.server.getAppClient("p2core", "Job")
return self._client
def createSOAPJob(self, bez: str) -> str:
"""
Erzeugt einen neuen SOAP Job mit der gegebenen Bezeichnung und liefert die neue JobID.
:param bez: die Bezeichnung des neuen Jobs
:type bez: str
:return: die neue JobID
:rtype: str
"""
jobId = str(uuid.uuid4())
self.client.service.create(jobId, "SOAP", "0", "about:soapcall", bez)
return jobId
def restart(self, jobId: str) -> str:
"""
Startet einen Job neu
:param jobId: die ID des Jobs
:type jobId: str
:return: die URL des Jobs
:rtype: str
"""
return self.client.service.restart(jobId)
def setResultURL(self, jobId: str, resurl: str) -> None:
"""
Setzt die ResultURL eines Jobs
:param jobId: die ID des Jobs
:type jobId: str
:param resurl: die neue Result-URL
:type resurl: str
"""
self.client.service.setResultURL(jobId, resurl)
def getResultURL(self, jobId: str) -> str:
"""
Liefert die ResultURL eines Jobs
:param jobId: die ID des Jobs
:type jobId: str
:return: die Result-URL
:rtype: str
"""
return self.client.service.getResultURL(jobId)
def getResultURLString(self, jobId: str) -> Optional[str]:
"""
Liefert die ResultURL eines Jobs, wobei ein evtl. Präfix "retstring://" entfernt wird und
alle anderen Werte durch None ersetzt werden.
:param jobId: die ID des Jobs
:type jobId: str
:return: die Result-URL als String
:rtype: str
"""
res = self.getResultURL(jobId)
if res is None:
return None
if res.startswith("retstring://"):
return res[12:]
return None
def setPtURL(self, jobId: str, pturl: str) -> None:
"""
Setzt die ResultURL eines Jobs
:param jobId: die ID des Jobs
:type jobId: str
:param pturl: die neue PtURL
:type pturl: str
"""
self.client.service.setPtURL(jobId, pturl)
def getPtURL(self, jobId: str) -> str:
"""
Liefert die PtURL eines Jobs
:param jobId: die ID des Jobs
:type jobId: str
:return: die Pt-URL
:rtype: str
"""
return self.client.service.getPtURL(jobId)
def setResult(self, jobId: str, res: str) -> None:
"""
Setzt das Result eines Jobs
:param jobId: die ID des Jobs
:type jobId: str
:param res: das neue Result
:type res: str
"""
self.client.service.setResult(jobId, res)
def getResult(self, jobId: str) -> str:
"""
Liefert das Result eines Jobs
:param jobId: die ID des Jobs
:type jobId: str
:return: das Result
:rtype: str
"""
return self.client.service.getResult(jobId)
def setInfo(self, jobId: str, info: str) -> bool:
"""
Setzt die Informationen zu dem Job
:param jobId: die ID des Jobs
:type jobId: str
:param info: die neuen Infos
:type info: str
"""
return self.client.service.setInfo(jobId, info)
def getInfo(self, jobId: str) -> str:
"""
Liefert die Info eines Jobs
:param jobId: die ID des Jobs
:type jobId: str
:return: die Info
:rtype: str
"""
return self.client.service.getInfo(jobId)
def getStatus(self, jobId: str) -> str:
"""
Liefert Informationen zum Job
:param jobId: die ID des Jobs
:type jobId: str
:return: die Infos
:rtype: str
"""
return self.client.service.getStatus(jobId)
def setPosition(self, jobId: str, pos: int, max: int) -> bool:
"""
Schrittfunktion
:param jobId: die ID des Jobs
:type jobId: str
:param pos: Position
:type pos: int
:param max: Anzahl Schritte in Anzeige
:type max: int
"""
return self.client.service.setPosition(jobId, pos, max)
def start(self, jobId: str) -> bool:
"""
Startet einen Job
:param jobId: die ID des Jobs
:type jobId: str
"""
return self.client.service.start(jobId)
def kill(self, jobId: str) -> None:
"""
Startet einen Job
:param jobId: die ID des Jobs
:type jobId: str
"""
self.client.service.start(jobId)
def finish(self, jobId: str, status: int, resurl: str) -> bool:
"""
Beendet den Job
:param jobId: die ID des Jobs
:type jobId: str
:param status: der Status 2 (OK), 3 (Fehler)
:type status: int
:param resurl: die neue resurl des Jobs
:type resurl: str
"""
return self.client.service.finish(jobId, status, resurl)

View File

@ -6,22 +6,23 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
from .applus import APplusServer
from . import sql_utils
import lxml.etree as ET # type: ignore
from typing import *
import lxml.etree as ET # type: ignore
from typing import Optional, Tuple, Set
from zeep import Client
import pathlib
class XMLDefinition:
"""Repräsentation eines XML-Dokuments"""
def __init__(self, root : ET.Element) -> None:
self.root : ET.Element = root
def __init__(self, root: ET.Element) -> None:
self.root: ET.Element = root
"""das Root-Element, repräsentiert "object" aus Datei."""
def __str__(self) -> str:
return ET.tostring(self.root, encoding = "unicode")
return ET.tostring(self.root, encoding="unicode")
def getDuplicate(self) -> Tuple[Set[str], bool]:
"""
@ -30,11 +31,11 @@ class XMLDefinition:
:return: Tuple aus allen Properties und ob dies aus- (True) oder ein-(False) zuschließen sind.
:rtype: Tuple[Set[str], bool]
"""
res : Set[str] = set()
excl = True;
res: Set[str] = set()
excl = True
dupl = self.root.find("duplicate")
if (dupl is None):
return (res, excl);
return (res, excl)
exclS = dupl.get("type", default="exclude")
excl = exclS.casefold() == "exclude"
@ -43,7 +44,7 @@ class XMLDefinition:
v = e.get("ref")
if not (v is None):
res.add(sql_utils.normaliseDBfield(str(v)))
return (res, excl)
@ -55,9 +56,16 @@ class APplusScriptTool:
:type server: APplusServerConnection
"""
def __init__(self, server : APplusServer) -> None:
self.client = server.getClient("p2script", "ScriptTool")
def __init__(self, server: APplusServer) -> None:
self.server = server
self._client = None
@property
def client(self) -> Client:
if not self._client:
self._client = self.server.getAppClient("p2script", "ScriptTool")
return self._client
def getCurrentDate(self) -> str:
return self.client.service.getCurrentDate()
@ -76,41 +84,59 @@ class APplusScriptTool:
def getUserFullName(self) -> str:
return self.client.service.getUserFullName()
def getSystemName(self) -> str:
return self.client.service.getSystemName()
def getXMLDefinitionString(self, obj:str, mandant:str="") -> str:
def getInstallPath(self) -> str:
"""
Liefert den Installionspfad des Appservers
"""
return self.client.service.getInstallPath()
def getInstallPathAppServer(self) -> pathlib.Path:
"""
Liefert den Installionspfad des Appservers als PathLib-Path
"""
return pathlib.Path(self.getInstallPath())
def getInstallPathWebServer(self) -> pathlib.Path:
"""
Liefert den Installionspfad des Webservers als PathLib-Path
"""
return self.getInstallPathAppServer().parents[0].joinpath("WebServer")
def getXMLDefinitionString(self, obj: str, mandant: str = "") -> str:
"""
Läd die XML-Defintion als String vom APPServer. Auch wenn kein XML-Dokument im Dateisystem gefunden wird,
wird ein String zurückgeliefert, der einen leeren Top-"Object" Knoten enthält. Für gefundene XML-Dokumente
gibt es zusätzlich einen Top-"MD5"-Knoten.
gibt es zusätzlich einen Top-"MD5"-Knoten.
:param obj: das Objekt, dessen Definition zu laden ist, "Artikel" läd z.B. "ArtikelDefinition.xml"
:type obj: str
:param mandant: der Mandant, dessen XML-Doku geladen werden soll, wenn "" wird der Standard-Mandant verwendet
:type mandant: str optional
:return: das gefundene XML-Dokument als String
:rtype: str
:rtype: str
"""
return self.client.service.getXMLDefinition2(obj, "")
def getXMLDefinition(self, obj:str, mandant:str="", checkFileExists:bool=False) -> Optional[ET.Element]:
def getXMLDefinition(self, obj: str, mandant: str = "", checkFileExists: bool = False) -> Optional[ET.Element]:
"""
Läd die XML-Definition als String vom APPServer. und parst das XML in ein minidom-Dokument.
:param obj: das Objekt, dessen Definition zu laden ist, "Artikel" läd z.B. "ArtikelDefinition.xml"
:type obj: str
:param mandant: der Mandant, dessen XML-Doku geladen werden soll, wenn "" wird der Standard-Mandant verwendet
:type mandant: str optional
:return: das gefundene und mittels ElementTree geparste XML-Dokument
:return: das gefundene und geparste XML-Dokument
:rtype: ET.Element
"""
return ET.fromstring(self.getXMLDefinitionString(obj, mandant=mandant))
def getXMLDefinitionObj(self, obj:str, mandant:str="") -> Optional[XMLDefinition]:
def getXMLDefinitionObj(self, obj: str, mandant: str = "") -> Optional[XMLDefinition]:
"""
Benutzt getXMLDefinitionObj und liefert den Top-Level "Object" Knoten zurück, falls zusätzlich
Benutzt getXMLDefinitionObj und liefert den Top-Level "Object" Knoten zurück, falls zusätzlich
ein MD5 Knoten existiert, also falls das Dokument wirklich vom Dateisystem geladen werden konnte.
Ansonten wird None geliefert.
@ -118,22 +144,21 @@ class APplusScriptTool:
:type obj: str
:param mandant: der Mandant, dessen XML-Doku geladen werden soll, wenn "" wird der Standard-Mandant verwendet
:type mandant: str optional
:return: das gefundene und mittels ElementTree geparste XML-Dokument
:return: das gefundene und geparste XML-Dokument
:rtype: Optional[XMLDefinition]
"""
e = self.getXMLDefinition(obj, mandant=mandant);
e = self.getXMLDefinition(obj, mandant=mandant)
if e is None:
return None
if e.find("md5") is None:
return None;
return None
o = e.find("object")
if o is None:
return None
else:
return XMLDefinition(o);
return XMLDefinition(o)
def getMandant(self) -> str:
"""
@ -146,3 +171,36 @@ class APplusScriptTool:
Liefert den Namen des aktuellen Mandanten
"""
return self.client.service.getCurrentClientProperty("NAME")
def getServerInfoString(self) -> str:
"""
Liefert Informationen zum Server als String. Dieser String repräsentiert ein XML Dokument.
:return: das XML-Dokument als String
:rtype: str
"""
return self.client.service.getP2plusServerInfo()
def getServerInfo(self) -> Optional[ET.Element]:
"""
Liefert Informationen zum Server als ein XML Dokument.
:return: das gefundene und geparste XML-Dokument
:rtype: ET.Element
"""
return ET.fromstring(self.getServerInfoString())
def getAllEnvironments(self) -> [str]:
"""
Liefert alle Umgebungen
:return: die gefundenen Umgebungen
:rtype: [str]
"""
envs = []
envString = self.client.service.getAllEnvironmentsInMasterDatabase()
for e in envString.split(","):
envs.append(e.split(":")[0])
return envs

View File

@ -6,66 +6,75 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
from requests import Session # type: ignore
from requests import Session # type: ignore
from requests.auth import HTTPBasicAuth # type: ignore # or HTTPDigestAuth, or OAuth1, etc.
from zeep import Client
from zeep.transports import Transport
from zeep.cache import SqliteCache
from typing import Optional, Dict
try:
from requests_negotiate_sspi import HttpNegotiateAuth
auth_negotiate_present = True
except:
auth_negotiate_present = False
class APplusAppServerSettings:
class APplusServerSettings:
"""
Einstellungen, mit welchem APplus App-Server sich verbunden werden soll.
Einstellungen, mit welchem APplus App- and Web-Server sich verbunden werden soll.
"""
def __init__(self, appserver : str, appserverPort : int, user : str, env : Optional[str] = None):
def __init__(self, webserver: str, appserver: str, appserverPort: int, user: str, env: Optional[str] = None, webserverUser : Optional[str] = None, webserverUserDomain : Optional[str] = None, webserverPassword : Optional[str] = None):
self.appserver = appserver
self.appserverPort = appserverPort
self.user = user
self.env = env
class APplusWebServerSettings:
"""
Einstellungen, mit welchem APplus Web-Server sich verbunden werden soll.
"""
def __init__(self, baseurl:Optional[str]=None):
self.baseurl : Optional[str] = baseurl;
try:
assert (isinstance(self.baseurl, str))
if not (self.baseurl == None) and not (self.baseurl[-1] == "/"):
self.baseurl = self.baseurl + "/";
self.webserver = webserver
self.webserverUser = webserverUser
self.webserverUserDomain = webserverUserDomain
self.webserverPassword = webserverPassword
try:
if not (self.webserver[-1] == "/"):
self.webserver = self.webserver + "/"
except:
pass
class APplusServerConnection:
"""Verbindung zu einem APplus APP-Server
"""Verbindung zu einem APplus APP- und Web-Server
:param settings: die Einstellungen für die Verbindung mit dem APplus Server
:type settings: APplusAppServerSettings
"""
def __init__(self, settings : APplusAppServerSettings) -> None:
userEnv = settings.user;
def __init__(self, settings: APplusServerSettings) -> None:
userEnv = settings.user
if (settings.env):
userEnv += "|" + settings.env
session = Session()
session.auth = HTTPBasicAuth(userEnv, "")
sessionApp = Session()
sessionApp.auth = HTTPBasicAuth(userEnv, "")
self.transport = Transport(cache=SqliteCache(), session=session)
# self.transport = Transport(session=session)
self.clientCache : Dict[str, Client] = {}
self.settings=settings;
self.appserverUrl = "http://" + settings.appserver + ":" + str(settings.appserverPort) + "/";
self.transportApp = Transport(cache=SqliteCache(), session=sessionApp)
# self.transportApp = Transport(session=sessionApp)
def getClient(self, package : str, name : str) -> Client:
"""Erzeugt einen zeep - Client.
if auth_negotiate_present:
sessionWeb = Session()
sessionWeb.auth = HttpNegotiateAuth(username=settings.webserverUser, password=settings.webserverPassword, domain=settings.webserverUserDomain)
self.transportWeb = Transport(cache=SqliteCache(), session=sessionWeb)
# self.transportWeb = Transport(session=sessionWeb)
else:
self.transportWeb = self.transportApp # führt vermutlich zu Authorization-Fehlern, diese sind aber zumindest hilfreicher als NULL-Pointer Exceptions
self.clientCache: Dict[str, Client] = {}
self.settings = settings
self.appserverUrl = "http://" + settings.appserver + ":" + str(settings.appserverPort) + "/"
def getAppClient(self, package: str, name: str) -> Client:
"""Erzeugt einen zeep - Client für den APP-Server.
Mittels dieses Clients kann die WSDL Schnittstelle angesprochen werden.
Wird als *package* "p2core" und als *name* "Table" verwendet und der
Wird als *package* "p2core" und als *name* "Table" verwendet und der
resultierende client "client" genannt, dann kann
z.B. mittels "client.service.getCompleteSQL(sql)" vom AppServer ein Vervollständigen
des SQLs angefordert werden.
@ -77,11 +86,36 @@ class APplusServerConnection:
:return: den Client
:rtype: Client
"""
url = package+"/"+name;
cacheKey = "APP:"+package+"/"+name
try:
return self.clientCache[url];
return self.clientCache[cacheKey]
except:
fullClientUrl = self.appserverUrl + url + ".jws?wsdl"
client = Client(fullClientUrl, transport=self.transport)
self.clientCache[url] = client;
return client;
fullClientUrl = self.appserverUrl + package+"/"+name + ".jws?wsdl"
client = Client(fullClientUrl, transport=self.transportApp)
self.clientCache[cacheKey] = client
return client
def getWebClient(self, url: str) -> Client:
"""Erzeugt einen zeep - Client für den Web-Server.
Mittels dieses Clients kann die von einer ASMX-Seite zur Verfügung gestellte Schnittstelle angesprochen werden.
Als parameter wird die relative URL der ASMX-Seite erwartet. Die Base-URL automatisch ergänzt.
Ein Beispiel für eine solche relative URL ist "masterdata/artikel.asmx".
ACHTUNG: Als Umgebung wird die Umgebung des sich anmeldenden Nutzers verwendet. Sowohl Nutzer als auch Umgebung können sich von den für App-Clients verwendeten Werten unterscheiden. Wenn möglich, sollte ein App-Client verwendet werden.
:param url: die relative URL der ASMX Seite, z.B. "masterdata/artikel.asmx"
:type package: str
:return: den Client
:rtype: Client
"""
if not auth_negotiate_present:
raise Exception("getWebClient ist nicht verfügbar, da Python-Package requests-negotiate-sspi nicht gefunden wurde")
cacheKey = "WEB:"+url
try:
return self.clientCache[cacheKey]
except:
fullClientUrl = self.settings.webserver + url + "?wsdl"
client = Client(fullClientUrl, transport=self.transportWeb)
self.clientCache[cacheKey] = client
return client

View File

@ -6,11 +6,10 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
from typing import TYPE_CHECKING, Optional, Dict, Any, Callable, Sequence
from zeep import Client
from typing import *
if TYPE_CHECKING:
if TYPE_CHECKING:
from .applus import APplusServer
@ -22,38 +21,46 @@ class APplusSysConf:
:type server: APplusServer
"""
def __init__(self, server : 'APplusServer') -> None:
self.client = server.getClient("p2system", "SysConf")
self.cache : Dict[str, type] = {}
def __init__(self, server: 'APplusServer') -> None:
self.cache: Dict[str, type] = {}
self.server = server
self._client = None
@property
def client(self) -> Client:
if not self._client:
self._client = self.server.getAppClient("p2system", "SysConf")
return self._client
def clearCache(self) -> None:
self.cache = {};
self.cache = {}
def _getGeneral(self, ty:str, f : Callable[[str, str], Any], module:str, name:str, useCache:bool) -> Any:
cacheKey = module + "/" + name + "/" + ty;
def _getGeneral(self, ty: str, f: Callable[[str, str], Any], module: str, name: str, useCache: bool) -> Any:
cacheKey = module + "/" + name + "/" + ty
if useCache and cacheKey in self.cache:
return self.cache[cacheKey]
else:
v = f(module, name);
self.cache[cacheKey] = v;
return v;
v = f(module, name)
self.cache[cacheKey] = v
return v
def getString(self, module:str, name:str, useCache:bool=True) -> str:
return self._getGeneral("string", self.client.service.getString, module, name, useCache);
def getString(self, module: str, name: str, useCache: bool = True) -> str:
return self._getGeneral("string", self.client.service.getString, module, name, useCache)
def getInt(self, module:str, name:str, useCache:bool=True) -> int:
return self._getGeneral("int", self.client.service.getInt, module, name, useCache);
def getInt(self, module: str, name: str, useCache: bool = True) -> int:
return self._getGeneral("int", self.client.service.getInt, module, name, useCache)
def getDouble(self, module:str, name:str, useCache:bool=True) -> float:
return self._getGeneral("double", self.client.service.getDouble, module, name, useCache);
def getDouble(self, module: str, name: str, useCache: bool = True) -> float:
return self._getGeneral("double", self.client.service.getDouble, module, name, useCache)
def getBoolean(self, module:str, name:str, useCache:bool=True) -> bool:
return self._getGeneral("boolean", self.client.service.getBoolean, module, name, useCache);
def getBoolean(self, module: str, name: str, useCache: bool = True) -> bool:
return self._getGeneral("boolean", self.client.service.getBoolean, module, name, useCache)
def getList(self, module : str, name:str, useCache:bool=True, sep:str=",") -> Optional[Sequence[str]]:
s = self.getString(module, name, useCache=useCache);
if (s == None or s == ""):
def getList(self, module: str, name: str, useCache: bool = True, sep: str = ",") -> Optional[Sequence[str]]:
s = self.getString(module, name, useCache=useCache)
if (s is None or s == ""):
return None
return s.split(sep);
return s.split(sep)

View File

@ -6,26 +6,23 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
import lxml.etree as ET # type: ignore
import lxml.etree as ET # type: ignore
from . import sql_utils
import datetime
from typing import *
from typing import TYPE_CHECKING, Any, Dict, Optional
if TYPE_CHECKING:
if TYPE_CHECKING:
from .applus import APplusServer
def _formatValueForXMLRow(v : Any) -> str:
def _formatValueForXMLRow(v: Any) -> str:
"""Hilfsfunktion zum Formatieren eines Wertes für XML"""
if (v is None):
return "";
return ""
if isinstance(v, (int, float)):
return str(v);
return str(v)
elif isinstance(v, str):
return v;
return v
elif isinstance(v, datetime.datetime):
return v.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
elif isinstance(v, datetime.date):
@ -38,7 +35,7 @@ def _formatValueForXMLRow(v : Any) -> str:
class UseXmlRow:
"""
Klasse, die eine XML-Datei erzeugen kann, die mittels p2core.useXML
Klasse, die eine XML-Datei erzeugen kann, die mittels p2core.useXML
genutzt werden kann. Damit ist es möglich APplus BusinessObjekte zu
erzeugen, ändern und zu löschen. Im Gegensatz zu direkten DB-Zugriffen,
werden diese Anfragen über den APP-Server ausgeführt. Dabei werden
@ -46,13 +43,13 @@ class UseXmlRow:
Als sehr einfaches Beispiel wird z.B. INSDATE oder UPDDATE automatisch gesetzt.
Interessanter sind automatische Änderungen und Checks.
Bei der Benutzung wird zunächst ein Objekt erzeugt, dann evtl.
mittels :meth:`addField` Felder hinzugefügt und schließlich mittels
:meth:`exec` an den AppServer übergeben.
Bei der Benutzung wird zunächst ein Objekt erzeugt, dann evtl.
mittels :meth:`addField` Felder hinzugefügt und schließlich mittels
:meth:`exec` an den AppServer übergeben.
Normalerweise sollte die Klasse nicht direkt, sondern über Unterklassen
für das Einfügen, Ändern oder Löschen benutzt werden.
:param applus: Verbindung zu APplus
:param applus: Verbindung zu APplus
:type applus: APplusServer
:param table: die Tabelle
:type table: str
@ -60,18 +57,18 @@ class UseXmlRow:
:type cmd: str
"""
def __init__(self, applus : 'APplusServer', table : str, cmd : str) -> None:
def __init__(self, applus: 'APplusServer', table: str, cmd: str) -> None:
self.applus = applus
self.table = table
self.cmd = cmd
self.fields : Dict[str, Any] = {}
self.fields: Dict[str, Any] = {}
def __str__(self) -> str:
return self.toprettyxml()
return self.toprettyxml()
def _buildXML(self) -> ET.Element :
def _buildXML(self) -> ET.Element:
"""Hilfsfunktion, die das eigentliche XML baut"""
row = ET.Element("row", cmd=self.cmd, table=self.table, nsmap={ "dt" : "urn:schemas-microsoft-com:datatypes"});
row = ET.Element("row", cmd=self.cmd, table=self.table, nsmap={"dt": "urn:schemas-microsoft-com:datatypes"})
for name, value in self.fields.items():
child = ET.Element(name)
@ -80,20 +77,19 @@ class UseXmlRow:
return row
def toprettyxml(self)->str:
def toprettyxml(self) -> str:
"""
Gibt das formatierte XML aus. Dieses kann per useXML an den AppServer übergeben werden.
Gibt das formatierte XML aus. Dieses kann per useXML an den AppServer übergeben werden.
Dies wird mittels :meth:`exec` automatisiert.
"""
return ET.tostring(self._buildXML(), encoding = "unicode", pretty_print=True)
return ET.tostring(self._buildXML(), encoding="unicode", pretty_print=True)
def getField(self, name:str) -> Any:
def getField(self, name: str) -> Any:
"""Liefert den Wert eines gesetzten Feldes"""
if name is None:
if name is None:
return None
name = sql_utils.normaliseDBfield(name);
name = sql_utils.normaliseDBfield(name)
if name in self.fields:
return self.fields[name]
@ -102,21 +98,21 @@ class UseXmlRow:
else:
return None
def checkFieldSet(self, name:Optional[str]) -> bool:
def checkFieldSet(self, name: Optional[str]) -> bool:
"""Prüft, ob ein Feld gesetzt wurde"""
if name is None:
if name is None:
return False
name = sql_utils.normaliseDBfield(name)
return (name in self.fields) or (name == "MANDANT")
def checkFieldsSet(self, *names : str) -> bool:
def checkFieldsSet(self, *names: str) -> bool:
"""Prüft, ob alle übergebenen Felder gesetzt sind"""
for n in names:
if not (self.checkFieldSet(n)):
return False
return True
def addField(self, name:str|None, value:Any) -> None:
def addField(self, name: Optional[str], value: Any) -> None:
"""
Fügt ein Feld zum Row-Node hinzu.
@ -124,15 +120,14 @@ class UseXmlRow:
:type name: string
:param value: Wert des Feldes
"""
if name is None:
if name is None:
return
self.fields[sql_utils.normaliseDBfield(name)] = value
def addTimestampField(self, id:int, ts:Optional[bytes]=None) -> None:
def addTimestampField(self, id: int, ts: Optional[bytes] = None) -> None:
"""
Fügt ein Timestamp-Feld hinzu. Wird kein Timestamp übergeben, wird mittels der ID der aktuelle
Fügt ein Timestamp-Feld hinzu. Wird kein Timestamp übergeben, wird mittels der ID der aktuelle
Timestamp aus der DB geladen. Dabei kann ein Fehler auftreten.
Ein Timestamp-Feld ist für Updates und das Löschen nötig um sicherzustellen, dass die richtige
Version des Objekts geändert oder gelöscht wird. Wird z.B. ein Objekt aus der DB geladen, inspiziert
@ -140,59 +135,58 @@ class UseXmlRow:
So wird sichergestellt, dass nicht ein anderer User zwischenzeitlich Änderungen vornahm. Ist dies
der Fall, wird dann bei "exec" eine Exception geworfen.
:param id: DB-id des Objektes dessen Timestamp hinzugefügt werden soll
:param id: DB-id des Objektes dessen Timestamp hinzugefügt werden soll
:type id: string
:param ts: Fester Timestamp der verwendet werden soll, wenn None wird der Timestamp aus der DB geladen.
:type ts: bytes
"""
if ts is None:
ts = self.applus.dbQuerySingleValue("select timestamp from " + self.table + " where id = ?", id);
ts = self.applus.dbQuerySingleValue("select timestamp from " + self.table + " where id = ?", id)
if ts:
self.addField("timestamp", ts.hex());
self.addField("timestamp", ts.hex())
else:
raise Exception("kein Eintrag in Tabelle '" + self.table + " mit ID " + str(id) + " gefunden")
def addTimestampIDFields(self, id:int, ts:Optional[bytes]=None) -> None:
def addTimestampIDFields(self, id: int, ts: Optional[bytes] = None) -> None:
"""
Fügt ein Timestamp-Feld sowie ein Feld id hinzu. Wird kein Timestamp übergeben, wird mittels der ID der aktuelle
Fügt ein Timestamp-Feld sowie ein Feld id hinzu. Wird kein Timestamp übergeben, wird mittels der ID der aktuelle
Timestamp aus der DB geladen. Dabei kann ein Fehler auftreten. Intern wird :meth:`addTimestampField` benutzt.
:param id: DB-id des Objektes dessen Timestamp hinzugefügt werden soll
:param id: DB-id des Objektes dessen Timestamp hinzugefügt werden soll
:type id: string
:param ts: Fester Timestamp der verwendet werden soll, wenn None wird der Timestamp aus der DB geladen.
:type ts: bytes
"""
self.addField("id", id)
self.addTimestampField(id, ts=ts);
self.addTimestampField(id, ts=ts)
def exec(self) -> Any:
"""
Führt die UseXmlRow mittels useXML aus. Je nach Art der Zeile wird etwas zurückgeliefert oder nicht.
In jedem Fall kann eine Exception geworfen werden.
"""
return self.applus.useXML(self.toprettyxml());
return self.applus.useXML(self.toprettyxml())
class UseXmlRowInsert(UseXmlRow):
"""
Klasse, die eine XML-Datei für das Einfügen eines neuen Datensatzes erzeugen kann.
:param applus: Verbindung zu APplus
:param applus: Verbindung zu APplus
:type applus: APplusServer
:param table: die Tabelle
:type table: string
"""
def __init__(self, applus:'APplusServer', table:str) -> None:
super().__init__(applus, table, "insert");
def __init__(self, applus: 'APplusServer', table: str) -> None:
super().__init__(applus, table, "insert")
def insert(self) -> int:
"""
Führt das insert aus. Entweder wird dabei eine Exception geworfen oder die ID des neuen Eintrags zurückgegeben.
Dies ist eine Umbenennung von :meth:`exec`.
"""
return super().exec();
return super().exec()
class UseXmlRowDelete(UseXmlRow):
@ -201,7 +195,7 @@ class UseXmlRowDelete(UseXmlRow):
Die Felder `id` und `timestamp` werden automatisch gesetzt.
Dies sind die einzigen Felder, die gesetzt werden sollten.
:param applus: Verbindung zu APplus
:param applus: Verbindung zu APplus
:type applus: APplusServer
:param table: die Tabelle
:type table: string
@ -211,17 +205,16 @@ class UseXmlRowDelete(UseXmlRow):
:type ts: bytes optional
"""
def __init__(self, applus:'APplusServer', table:str, id:int, ts:Optional[bytes]=None) -> None:
super().__init__(applus, table, "delete");
self.addTimestampIDFields(id, ts=ts);
def __init__(self, applus: 'APplusServer', table: str, id: int, ts: Optional[bytes] = None) -> None:
super().__init__(applus, table, "delete")
self.addTimestampIDFields(id, ts=ts)
def delete(self) -> None:
"""
Führt das delete aus. Evtl. wird dabei eine Exception geworfen.
Dies ist eine Umbenennung von :meth:`exec`.
"""
super().exec();
super().exec()
class UseXmlRowUpdate(UseXmlRow):
@ -229,7 +222,7 @@ class UseXmlRowUpdate(UseXmlRow):
Klasse, die eine XML-Datei für das Ändern eines neuen Datensatzes, erzeugen kann.
Die Felder `id` und `timestamp` werden automatisch gesetzt.
:param applus: Verbindung zu APplus
:param applus: Verbindung zu APplus
:type applus: APplusServer
:param table: die Tabelle
:type table: string
@ -239,18 +232,16 @@ class UseXmlRowUpdate(UseXmlRow):
:type ts: bytes optional
"""
def __init__(self, applus : 'APplusServer', table : str, id : int, ts:Optional[bytes]=None) -> None:
super().__init__(applus, table, "update");
self.addTimestampIDFields(id, ts=ts);
def __init__(self, applus: 'APplusServer', table: str, id: int, ts: Optional[bytes] = None) -> None:
super().__init__(applus, table, "update")
self.addTimestampIDFields(id, ts=ts)
def update(self) -> None:
"""
Führt das update aus. Evtl. wird dabei eine Exception geworfen.
Dies ist eine Umbenennung von :meth:`exec`.
"""
super().exec();
super().exec()
class UseXmlRowInsertOrUpdate(UseXmlRow):
@ -258,34 +249,33 @@ class UseXmlRowInsertOrUpdate(UseXmlRow):
Klasse, die eine XML-Datei für das Einfügen oder Ändern eines neuen Datensatzes, erzeugen kann.
Die Methode `checkExists` erlaubt es zu prüfen, ob ein Objekt bereits existiert. Dafür werden die
gesetzten Felder mit den Feldern aus eindeutigen Indices verglichen. Existiert ein Objekt bereits, wird
ein Update ausgeführt, ansonsten ein Insert. Bei Updates werden die Felder `id` und `timestamp`
ein Update ausgeführt, ansonsten ein Insert. Bei Updates werden die Felder `id` und `timestamp`
automatisch gesetzt.
:param applus: Verbindung zu APplus
:param applus: Verbindung zu APplus
:type applus: APplusServer
:param table: die Tabelle
:type table: string
"""
def __init__(self, applus : 'APplusServer', table : str) -> None:
super().__init__(applus, table, "");
def __init__(self, applus: 'APplusServer', table: str) -> None:
super().__init__(applus, table, "")
def checkExists(self) -> int|None:
def checkExists(self) -> Optional[int]:
"""
Prüft, ob der Datensatz bereits in der DB existiert.
Prüft, ob der Datensatz bereits in der DB existiert.
Ist dies der Fall, wird die ID geliefert, sonst None
"""
# Baue Bedingung
cond = sql_utils.SqlConditionOr();
cond = sql_utils.SqlConditionOr()
for idx, fs in self.applus.getUniqueFieldsOfTable(self.table).items():
if (self.checkFieldsSet(*fs)):
condIdx = sql_utils.SqlConditionAnd();
condIdx = sql_utils.SqlConditionAnd()
for f in fs:
condIdx.addConditionFieldEq(f, self.getField(f))
cond.addCondition(condIdx)
sql = sql_utils.SqlStatementSelect(self.table, "id")
sql.where = cond
return self.applus.dbQuerySingleValue(sql)
@ -293,12 +283,12 @@ class UseXmlRowInsertOrUpdate(UseXmlRow):
def insert(self) -> int:
"""Führt ein Insert aus. Existiert das Objekt bereits, wird eine Exception geworfen."""
r = UseXmlRowInsert(self.applus, self.table)
r = UseXmlRowInsert(self.applus, self.table)
for k, v in self.fields.items():
r.addField(k, v)
return r.insert();
return r.insert()
def update(self, id:Optional[int]=None, ts:Optional[bytes]=None) -> int:
def update(self, id: Optional[int] = None, ts: Optional[bytes] = None) -> int:
"""Führt ein Update aus. Falls ID oder Timestamp nicht übergeben werden, wird
nach einem passenden Objekt gesucht. Existiert das Objekt nicht, wird eine Exception geworfen."""
@ -307,23 +297,23 @@ class UseXmlRowInsertOrUpdate(UseXmlRow):
if id is None:
raise Exception("Update nicht möglich, da kein Objekt für Update gefunden.")
r = UseXmlRowUpdate(self.applus, self.table, id, ts=ts)
r = UseXmlRowUpdate(self.applus, self.table, id, ts=ts)
for k, v in self.fields.items():
r.addField(k, v)
r.update();
r.update()
return id
def exec(self) -> int:
def exec(self) -> int:
"""
Führt entweder ein Update oder ein Insert durch. Dies hängt davon ab, ob das Objekt bereits in
der DB existiert. In jedem Fall wird die ID des erzeugten oder geänderten Objekts geliefert.
"""
id = self.checkExists();
if id == None:
id = self.checkExists()
if id is None:
return self.insert()
else:
else:
return self.update(id=id)
def updateOrInsert(self) -> int:
@ -332,4 +322,4 @@ class UseXmlRowInsertOrUpdate(UseXmlRow):
Dies ist eine Umbenennung von :meth:`exec`.
Es wird die ID des Eintrages geliefert
"""
return self.exec();
return self.exec()

View File

@ -6,8 +6,6 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
"""
Dupliziert ein oder mehrere APplus Business-Objekte
"""
@ -16,58 +14,57 @@ from . import sql_utils
from . import applus_db
from . import applus_usexml
from .applus import APplusServer
import pyodbc # type: ignore
import pyodbc # type: ignore
import traceback
import logging
from typing import *
from typing import List, Set, Optional, Dict, Tuple, Sequence, Any, Union
logger = logging.getLogger(__name__);
logger = logging.getLogger(__name__)
noCopyFields = sql_utils.normaliseDBfieldSet({"INSUSER", "UPDDATE", "TIMESTAMP", "MANDANT", "GUID", "ID", "TIMESTAMP_A", "INSDATE", "ID_A", "UPDUSER"})
"""Menge von Feld-Namen, die nie kopiert werden sollen."""
def getFieldsToCopyForTable(server : APplusServer, table : str, force:bool=True) -> Set[str]:
def getFieldsToCopyForTable(server: APplusServer, table: str, force: bool = True) -> Set[str]:
"""
Bestimmt die für eine Tabelle zu kopierenden Spalten. Dazu wird in den XML-Definitionen geschaut.
Ist dort 'include' hinterlegt, werden diese Spalten verwendet. Ansonsten alle nicht generierten Spalten,
Ist dort 'include' hinterlegt, werden diese Spalten verwendet. Ansonsten alle nicht generierten Spalten,
ohne die 'exclude' Spalten. In jedem Fall werden Spalten wie "ID", die nie kopiert werden sollten, entfernt.
"""
xmlDefs = server.scripttool.getXMLDefinitionObj(table)
fields : Set[str]
fields: Set[str]
if (xmlDefs is None):
if not force:
raise Exception ("Keine XML-Definitionen für '{}' gefunden".format(table));
raise Exception("Keine XML-Definitionen für '{}' gefunden".format(table))
(fields, excl) = (set(), True)
else:
else:
(fields, excl) = xmlDefs.getDuplicate()
if not excl:
if not excl:
return fields.difference(noCopyFields)
allFields = server.getTableFields(table, isComputed=False)
return allFields.difference(fields).difference(noCopyFields);
return allFields.difference(fields).difference(noCopyFields)
class FieldsToCopyForTableCache():
"""
Cache für welche Felder für welche Tabelle kopiert werden sollen
"""
def __init__(self, server : APplusServer) -> None:
self.server = server
self.cache : Dict[str, Set[str]]= {}
def getFieldsToCopyForTable(self, table : str) -> Set[str]:
def __init__(self, server: APplusServer) -> None:
self.server = server
self.cache: Dict[str, Set[str]] = {}
def getFieldsToCopyForTable(self, table: str) -> Set[str]:
"""
Bestimmt die für eine Tabelle zu kopierenden Spalten. Dazu wird in den XML-Definitionen geschaut.
Ist dort 'include' hinterlegt, werden diese Spalten verwendet. Ansonsten alle nicht generierten Spalten,
Ist dort 'include' hinterlegt, werden diese Spalten verwendet. Ansonsten alle nicht generierten Spalten,
ohne die 'exclude' Spalten. In jedem Fall werden Spalten wie "ID", die nie kopiert werden sollten, entfernt.
"""
if (table is None):
return None
t = table.upper()
fs = self.cache.get(t, None)
if not (fs is None):
@ -78,14 +75,14 @@ class FieldsToCopyForTableCache():
return fs
def initFieldsToCopyForTableCacheIfNeeded(server : APplusServer, cache : Optional[FieldsToCopyForTableCache]) -> FieldsToCopyForTableCache:
def initFieldsToCopyForTableCacheIfNeeded(server: APplusServer, cache: Optional[FieldsToCopyForTableCache]) -> FieldsToCopyForTableCache:
"""
Hilfsfunktion, die einen Cache erzeugt, falls dies noch nicht geschehen ist.
"""
if cache is None:
return FieldsToCopyForTableCache(server)
else:
return cache;
return cache
class DuplicateBusinessObject():
@ -94,34 +91,34 @@ class DuplicateBusinessObject():
Dies beinhaltet Daten zu abhängigen Objekten sowie die Beziehung zu diesen Objekten. Zu einem Artikel
wird z.B. der Arbeitsplan gespeichert, der wiederum Arbeitsplanpositionen enthält. Als Beziehung ist u.a.
hinterlegt, dass das Feld "APLAN" der Arbeitsplans dem Feld "ARTIKEL" des Artikels entsprechen muss und dass
"APLAN" aus den Positionen, "APLAN" aus dem APlan entsprichen muss. So kann beim Duplizieren ein
"APLAN" aus den Positionen, "APLAN" aus dem APlan entsprichen muss. So kann beim Duplizieren ein
anderer Name des Artikels gesetzt werden und automatisch die Felder der abhängigen Objekte angepasst werden.
Einige Felder der Beziehung sind dabei statisch, d.h. können direkt aus den zu speichernden Daten abgelesen werden.
Andere Felder sind dynamisch, d.h. das Parent-Objekt muss in der DB angelegt werden, damit ein solcher dynamischer Wert erstellt
Andere Felder sind dynamisch, d.h. das Parent-Objekt muss in der DB angelegt werden, damit ein solcher dynamischer Wert erstellt
und geladen werden kann. Ein typisches Beispiel für ein dynamisches Feld ist "GUID".
"""
def __init__(self, table : str, fields : Dict[str, Any], fieldsNotCopied:Dict[str, Any]={}, allowUpdate:bool=False) -> None:
def __init__(self, table: str, fields: Dict[str, Any], fieldsNotCopied: Dict[str, Any] = {}, allowUpdate: bool = False) -> None:
self.table = table
"""für welche Tabelle ist das BusinessObject"""
self.fields = fields
self.fields = fields
"""die Daten"""
self.fieldsNotCopied = fieldsNotCopied
"""Datenfelder, die im Original vorhanden sind, aber nicht kopiert werden sollen"""
self.dependentObjs : List[Dict[str, Any]] = []
self.dependentObjs: List[Dict[str, Any]] = []
"""Abhängige Objekte"""
self.allowUpdate = allowUpdate
"""Erlaube Updates statt Fehlern, wenn Objekt schon in DB existiert"""
def addDependentBusinessObject(self, dObj : Optional['DuplicateBusinessObject'], *args : Tuple[str, str]) -> None:
def addDependentBusinessObject(self, dObj: Optional['DuplicateBusinessObject'], *args: Tuple[str, str]) -> None:
"""
Fügt ein neues Unterobjekt zum DuplicateBusinessObject hinzu.
Dabei handelt es sich selbst um ein DuplicateBusinessObject, das zusammen mit dem
Parent-Objekt dupliziert werden sollen. Zum Beispiel sollen zu einem
Parent-Objekt dupliziert werden sollen. Zum Beispiel sollen zu einem
Auftrag auch die Positionen dupliziert werden.
Zusätzlich zum Objekt selbst können mehrere (keine, eine oder viele)
Paare von Feldern übergeben werden. Ein Paar ("pf", "sf") verbindet das
@ -136,20 +133,20 @@ class DuplicateBusinessObject():
:param args: Liste von Tupeln, die Parent- und Sub-Objekt-Felder miteinander verbinden
"""
if (dObj is None):
return
return
args2= {}
args2 = {}
for f1, f2 in args:
args2[sql_utils.normaliseDBfield(f1)] = sql_utils.normaliseDBfield(f2)
self.dependentObjs.append({
"dependentObj" : dObj,
"connection" : args2
"dependentObj": dObj,
"connection": args2
})
def getField(self, field:str, onlyCopied:bool=False) -> Any:
def getField(self, field: str, onlyCopied: bool = False) -> Any:
"""
Schlägt den Wert eines Feldes nach. Wenn onlyCopied gesetzt ist, werden nur Felder zurückgeliefert, die auch kopiert
Schlägt den Wert eines Feldes nach. Wenn onlyCopied gesetzt ist, werden nur Felder zurückgeliefert, die auch kopiert
werden sollen.
"""
@ -159,40 +156,40 @@ class DuplicateBusinessObject():
if (not onlyCopied) and (f in self.fieldsNotCopied):
return self.fieldsNotCopied[f]
return None
def insert(self, server : APplusServer) -> applus_db.DBTableIDs:
def insert(self, server: APplusServer) -> applus_db.DBTableIDs:
"""
Fügt alle Objekte zur DB hinzu. Es wird die Menge der IDs der erzeugten
Objekte gruppiert nach Tabellen erzeugt. Falls ein Datensatz schon
existiert, wird dieser entweder aktualisiert oder eine Fehlermeldung
geworfen. Geliefert wird die Menge aller Eingefügten Objekte mit ihrer ID.
"""
res = applus_db.DBTableIDs()
def insertDO(do : 'DuplicateBusinessObject') -> Optional[int]:
def insertDO(do: 'DuplicateBusinessObject') -> Optional[int]:
nonlocal res
insertRow : applus_usexml.UseXmlRow
if do.allowUpdate:
insertRow = server.mkUseXMLRowInsertOrUpdate(do.table);
insertRow: applus_usexml.UseXmlRow
if do.allowUpdate:
insertRow = server.mkUseXMLRowInsertOrUpdate(do.table)
else:
insertRow = server.mkUseXMLRowInsert(do.table);
insertRow = server.mkUseXMLRowInsert(do.table)
for f, v in do.fields.items():
insertRow.addField(f, v)
try:
id = insertRow.exec()
res.add(do.table, id)
return id
except:
msg = traceback.format_exc();
msg = traceback.format_exc()
logger.error("Exception inserting BusinessObjekt: %s\n%s", str(insertRow), msg)
return None
def insertDep(do : 'DuplicateBusinessObject', doID : int, so : 'DuplicateBusinessObject', connect : Dict[str,str]) -> None:
def insertDep(do: 'DuplicateBusinessObject', doID: int, so: 'DuplicateBusinessObject', connect: Dict[str, str]) -> None:
nonlocal res
# Abbruch, wenn do nicht eingefügt wurde
@ -206,10 +203,10 @@ class DuplicateBusinessObject():
so.fields[fs] = do.fields[fd]
else:
connectMissing[fd] = fs
# load missing fields from DB
if len(connectMissing) > 0:
sql = sql_utils.SqlStatementSelect(do.table);
sql = sql_utils.SqlStatementSelect(do.table)
sql.where.addConditionFieldEq("id", doID)
for fd in connectMissing:
sql.addFields(fd)
@ -224,19 +221,17 @@ class DuplicateBusinessObject():
if not (id is None):
insertDeps(so, id)
def insertDeps(do : 'DuplicateBusinessObject', doID : int) -> None:
def insertDeps(do: 'DuplicateBusinessObject', doID: int) -> None:
for so in do.dependentObjs:
insertDep(do, doID, so["dependentObj"], so["connection"])
topID = insertDO(self)
if not (topID is None):
insertDeps(self, topID)
return res
def setFields(self, upds : Dict[str, Any]) -> None:
def setFields(self, upds: Dict[str, Any]) -> None:
"""
Setzt Felder des DuplicateBusinessObjektes und falls nötig seiner Unterobjekte.
So kann zum Beispiel die Nummer vor dem Speichern geändert werden.
@ -244,11 +239,11 @@ class DuplicateBusinessObject():
:param upds: Dictionary mit zu setzenden Werten
"""
def setFieldsInternal(dobj : 'DuplicateBusinessObject', upds : Dict[str, Any]) -> None:
def setFieldsInternal(dobj: 'DuplicateBusinessObject', upds: Dict[str, Any]) -> None:
# setze alle Felder des Hauptobjekts
for f, v in upds.items():
dobj.fields[f] = v
# verarbeite alle Subobjekte
for su in dobj.dependentObjs:
subupds = {}
@ -256,70 +251,66 @@ class DuplicateBusinessObject():
if fp in upds:
subupds[fs] = upds[fp]
setFieldsInternal(su["dependentObj"], subupds)
updsNorm : Dict[str, Any] = {}
updsNorm: Dict[str, Any] = {}
for f, v in upds.items():
updsNorm[sql_utils.normaliseDBfield(f)] = v
setFieldsInternal(self, updsNorm)
def _loadDBDuplicateBusinessObjectDict(
server : APplusServer,
table : str,
row : pyodbc.Row,
cache:Optional[FieldsToCopyForTableCache]=None,
allowUpdate:bool=False) -> Optional[DuplicateBusinessObject]:
server: APplusServer,
table: str,
row: pyodbc.Row,
cache: Optional[FieldsToCopyForTableCache] = None,
allowUpdate: bool = False) -> Optional[DuplicateBusinessObject]:
"""
Hilfsfunktion, die ein DuplicateBusinessObjekt erstellt. Die Daten stammen aus
einer PyOdbc Zeile. So ist es möglich, mit nur einem SQL-Statement,
mehrere DuplicateBusinessObjekte zu erstellen.
mehrere DuplicateBusinessObjekte zu erstellen.
:param server: Verbindung zum APP-Server, benutzt zum Nachschlagen der zu kopierenden Felder
:param table: Tabelle für das neue DuplicateBusinessObjekt
:param row: die Daten als PyODBC Zeile
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:return: das neue DuplicateBusinessObject
"""
table = table.upper();
table = table.upper()
def getFieldsToCopy() -> Set[str]:
if cache is None:
return getFieldsToCopyForTable(server, table)
else:
else:
return cache.getFieldsToCopyForTable(table)
def getFields() -> Tuple[Dict[str, Any], Dict[str, Any]]:
ftc = getFieldsToCopy()
fields = {}
fields = {}
fieldsNotCopied = {}
for f, v in applus_db.row_to_dict(row).items():
f = sql_utils.normaliseDBfield(f);
f = sql_utils.normaliseDBfield(f)
if f in ftc:
fields[f] = v
else:
fieldsNotCopied[f] = v
return (fields, fieldsNotCopied)
if (row is None):
return None
(fields, fieldsNotCopied) = getFields()
(fields, fieldsNotCopied) = getFields()
return DuplicateBusinessObject(table, fields, fieldsNotCopied=fieldsNotCopied, allowUpdate=allowUpdate)
def loadDBDuplicateBusinessObject(
server : APplusServer,
table : str,
cond : sql_utils.SqlCondition,
cache : Optional[FieldsToCopyForTableCache]=None,
allowUpdate : bool = False) -> Optional[DuplicateBusinessObject]:
server: APplusServer,
table: str,
cond: sql_utils.SqlCondition,
cache: Optional[FieldsToCopyForTableCache] = None,
allowUpdate: bool = False) -> Optional[DuplicateBusinessObject]:
"""
Läd ein einzelnes DuplicateBusinessObjekt aus der DB. Die Bedingung sollte dabei
einen eindeutigen Datensatz auswählen. Werden mehrere zurückgeliefert, wird ein
einen eindeutigen Datensatz auswählen. Werden mehrere zurückgeliefert, wird ein
zufälliger ausgewählt. Wird kein Datensatz gefunden, wird None geliefert.
:param server: Verbindung zum APP-Server, benutzt zum Nachschlagen der zu kopierenden Felder
@ -328,30 +319,31 @@ def loadDBDuplicateBusinessObject(
:type table: str
:param cond: SQL-Bedingung zur Auswahl eines Objektes
:type cond: sql_utils.SqlCondition
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
:param allowUpdate: ist Update statt Insert erlaubt?
:type allowUpdate: bool
:return: das neue DuplicateBusinessObject
:rtype: Optional[DuplicateBusinessObject]
"""
table = table.upper();
table = table.upper()
def getRow() -> pyodbc.Row:
def getRow() -> pyodbc.Row:
sql = sql_utils.SqlStatementSelect(table)
sql.setTop(1)
sql.where.addCondition(cond);
sql.where.addCondition(cond)
return server.dbQuerySingleRow(sql)
return _loadDBDuplicateBusinessObjectDict(server, table, getRow(), cache=cache, allowUpdate=allowUpdate);
return _loadDBDuplicateBusinessObjectDict(server, table, getRow(), cache=cache, allowUpdate=allowUpdate)
def loadDBDuplicateBusinessObjectSimpleCond(
server : APplusServer,
table : str,
field : str,
value : Optional[Union[sql_utils.SqlValue, bool]],
cache : Optional[FieldsToCopyForTableCache]=None,
allowUpdate : bool = False) -> Optional[DuplicateBusinessObject]:
server: APplusServer,
table: str,
field: str,
value: Union[sql_utils.SqlValue, bool, None],
cache: Optional[FieldsToCopyForTableCache] = None,
allowUpdate: bool = False) -> Optional[DuplicateBusinessObject]:
"""
Wrapper für loadDBDuplicateBusinessObject, das eine einfache Bedingung benutzt,
bei der ein Feld einen bestimmten Wert haben muss.
@ -363,21 +355,21 @@ def loadDBDuplicateBusinessObjectSimpleCond(
:param field: Feld für Bedingung
:type field: str
:param value: Wert des Feldes für Bedingung
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
:return: das neue DuplicateBusinessObject
:rtype: Optional[DuplicateBusinessObject]
"""
cond = sql_utils.SqlConditionFieldEq(field, value)
return loadDBDuplicateBusinessObject(server, table, cond, cache=cache, allowUpdate=allowUpdate)
def loadDBDuplicateBusinessObjects(
server : APplusServer,
table : str,
cond : sql_utils.SqlCondition,
cache : Optional[FieldsToCopyForTableCache]=None,
allowUpdate : bool = False) -> Sequence[DuplicateBusinessObject]:
server: APplusServer,
table: str,
cond: sql_utils.SqlCondition,
cache: Optional[FieldsToCopyForTableCache] = None,
allowUpdate: bool = False) -> Sequence[DuplicateBusinessObject]:
"""
Läd eine Liste von DuplicateBusinessObjekten aus der DB. Die Bedingung kann mehrere Datensätze auswählen.
@ -387,7 +379,7 @@ def loadDBDuplicateBusinessObjects(
:type table: str
:param cond: SQL-Bedingung zur Auswahl eines Objektes
:type cond: sql_utils.SqlCondition
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
:return: Liste der neuen DuplicateBusinessObjects
:rtype: Sequence[DuplicateBusinessObject]
@ -395,20 +387,21 @@ def loadDBDuplicateBusinessObjects(
table = table.upper()
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache)
def processRow(r : pyodbc.Row) -> Optional[DuplicateBusinessObject]:
return _loadDBDuplicateBusinessObjectDict(server, table, r, cache=cache, allowUpdate=allowUpdate)
def processRow(r: pyodbc.Row) -> Optional[DuplicateBusinessObject]:
return _loadDBDuplicateBusinessObjectDict(server, table, r, cache=cache, allowUpdate=allowUpdate)
sql = sql_utils.SqlStatementSelect(table)
sql.where.addCondition(cond)
return server.dbQueryAll(sql, apply=processRow)
def loadDBDuplicateBusinessObjectsSimpleCond(
server : APplusServer,
table : str,
field : str,
value : Optional[Union[sql_utils.SqlValue, bool]],
cache : Optional[FieldsToCopyForTableCache]=None,
allowUpdate : bool = False) -> Sequence[DuplicateBusinessObject]:
server: APplusServer,
table: str,
field: str,
value: Union[sql_utils.SqlValue, bool, None],
cache: Optional[FieldsToCopyForTableCache] = None,
allowUpdate: bool = False) -> Sequence[DuplicateBusinessObject]:
"""
Wrapper für loadDBDuplicateBusinessObjects, das eine einfache Bedingung benutzt,
bei der ein Feld einen bestimmten Wert haben muss.
@ -419,90 +412,91 @@ def loadDBDuplicateBusinessObjectsSimpleCond(
:type table: str
:param field: Feld für Bedingung
:param value: Wert des Feldes für Bedingung
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
:return: Liste der neuen DuplicateBusinessObjects
:rtype: Sequence[DuplicateBusinessObject]
"""
cond = sql_utils.SqlConditionFieldEq(field, value)
return loadDBDuplicateBusinessObjects(server, table, cond, cache=cache, allowUpdate=allowUpdate)
# Im Laufe der Zeit sollten load-Funktionen für verschiedene BusinessObjekte
# erstellt werden. Dies erfolgt immer, wenn eine solche Funktion wirklich
# benutzt werden soll
def loadDBDuplicateAPlan(
server : APplusServer,
aplan : str,
cache:Optional[FieldsToCopyForTableCache]=None) -> Optional[DuplicateBusinessObject]:
server: APplusServer,
aplan: str,
cache: Optional[FieldsToCopyForTableCache] = None) -> Optional[DuplicateBusinessObject]:
"""
Erstelle DuplicateBusinessObject für einzelnen Arbeitsplan.
Erstelle DuplicateBusinessObject für einzelnen Arbeitsplan.
:param server: Verbindung zum APP-Server, benutzt zum Nachschlagen der zu kopierenden Felder
:type server: APplusServer
:param aplan: Aplan, der kopiert werden soll.
:type aplan: str
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
:return: das neue DuplicateBusinessObject
:rtype: DuplicateBusinessObject
"""
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache);
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache)
boMain = loadDBDuplicateBusinessObjectSimpleCond(server, "aplan", "APLAN", aplan, cache=cache)
if boMain is None:
return None
for so in loadDBDuplicateBusinessObjectsSimpleCond(server, "aplanpos", "APLAN", aplan, cache=cache):
boMain.addDependentBusinessObject(so, ("aplan", "aplan"))
return boMain
def loadDBDuplicateStueli(server : APplusServer, stueli : str, cache:Optional[FieldsToCopyForTableCache]=None) -> Optional[DuplicateBusinessObject]:
def loadDBDuplicateStueli(server: APplusServer, stueli: str, cache: Optional[FieldsToCopyForTableCache] = None) -> Optional[DuplicateBusinessObject]:
"""
Erstelle DuplicateBusinessObject für einzelne Stückliste.
Erstelle DuplicateBusinessObject für einzelne Stückliste.
:param server: Verbindung zum APP-Server, benutzt zum Nachschlagen der zu kopierenden Felder
:type server: APplusServer
:param stueli: Stückliste, die kopiert werden soll.
:type stueli: str
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
:return: das neue DuplicateBusinessObject
:rtype: Optional[DuplicateBusinessObject]
"""
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache);
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache)
boMain = loadDBDuplicateBusinessObjectSimpleCond(server, "stueli", "stueli", stueli, cache=cache)
if boMain is None:
return None
for so in loadDBDuplicateBusinessObjectsSimpleCond(server, "stuelipos", "stueli", stueli, cache=cache):
boMain.addDependentBusinessObject(so, ("stueli", "stueli"))
return boMain
def addSachgruppeDependentObjects(
do : DuplicateBusinessObject,
server : APplusServer,
cache:Optional[FieldsToCopyForTableCache]=None) -> None:
do: DuplicateBusinessObject,
server: APplusServer,
cache: Optional[FieldsToCopyForTableCache] = None) -> None:
"""
Fügt Unterobjekte hinzu, die die Sachgruppenwerte kopieren.
:param do: zu erweiterndes DuplicateBusinessObject
:param server: Verbindung zum APP-Server, benutzt zum Nachschlagen der zu kopierenden Felder
:type server: APplusServer
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
"""
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache);
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache)
klasse = do.fields.get(sql_utils.normaliseDBfield("SACHGRUPPENKLASSE"), None)
if (klasse == None):
if (klasse is None):
# keine Klasse gesetzt, nichts zu kopieren
return
return
# bestimme alle Gruppen
def loadGruppen() -> Sequence[str]:
@ -511,7 +505,7 @@ def addSachgruppeDependentObjects(
sql.where.addConditionFieldEq("tabelle", do.table)
return server.dbQueryAll(sql, apply=lambda r: r.sachgruppe)
gruppen = loadGruppen();
gruppen = loadGruppen()
# Gruppe bearbeiten
def processGruppen() -> None:
@ -525,26 +519,24 @@ def addSachgruppeDependentObjects(
for so in loadDBDuplicateBusinessObjects(server, "sachwert", cond, cache=cache, allowUpdate=True):
do.addDependentBusinessObject(so, ("guid", "instanzguid"))
processGruppen()
def loadDBDuplicateArtikel(
server : APplusServer,
artikel : str,
cache:Optional[FieldsToCopyForTableCache]=None,
dupAplan:bool=True,
dupStueli:bool=True) -> Optional[DuplicateBusinessObject]:
server: APplusServer,
artikel: str,
cache: Optional[FieldsToCopyForTableCache] = None,
dupAplan: bool = True,
dupStueli: bool = True) -> Optional[DuplicateBusinessObject]:
"""
Erstelle DuplicateBusinessObject für einzelnen Artikel.
Erstelle DuplicateBusinessObject für einzelnen Artikel.
:param server: Verbindung zum APP-Server, benutzt zum Nachschlagen der zu kopierenden Felder
:type server: APplusServer
:param artikel: Artikel, der kopiert werden soll
:type artikel: str
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:param cache: Cache, so dass benötigte Felder nicht immer wieder neu berechnet werden müssen
:type cache: Optional[FieldsToCopyForTableCache]
:param dupAplan: Arbeitsplan duplizieren?
:type dupAplan: bool optional
@ -554,7 +546,7 @@ def loadDBDuplicateArtikel(
:rtype: DuplicateBusinessObject
"""
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache);
cache = initFieldsToCopyForTableCacheIfNeeded(server, cache)
boArt = loadDBDuplicateBusinessObjectSimpleCond(server, "artikel", "ARTIKEL", artikel, cache=cache)
if boArt is None:
return None
@ -563,7 +555,7 @@ def loadDBDuplicateArtikel(
if dupAplan:
boAplan = loadDBDuplicateAPlan(server, artikel, cache=cache)
boArt.addDependentBusinessObject(boAplan, ("artikel", "aplan"))
if dupStueli:
boStueli = loadDBDuplicateStueli(server, artikel, cache=cache)
boArt.addDependentBusinessObject(boStueli, ("artikel", "stueli"))

View File

@ -8,112 +8,111 @@
"""Pandas Interface für PyAPplus64."""
from typing import Annotated as Ann
import pandas as pd # type: ignore
from pandas._typing import AggFuncType, FilePath, WriteExcelBuffer # type: ignore
import pandas as pd # type: ignore
from pandas._typing import AggFuncType, FilePath, WriteExcelBuffer # type: ignore
import sqlalchemy
import traceback
from .applus import APplusServer
from .applus import sql_utils
from typing import *
from typing import Optional, Callable, Sequence, Tuple, Any, Union
def createSqlAlchemyEngine(server : APplusServer) -> sqlalchemy.Engine:
def createSqlAlchemyEngine(server: APplusServer) -> sqlalchemy.Engine:
"""Erzeugt eine SqlAlchemy-Engine für die Verbindung zur DB."""
return sqlalchemy.create_engine(sqlalchemy.engine.URL.create("mssql+pyodbc", query={"odbc_connect": server.db_settings.getConnectionString()}))
def pandasReadSql(
server : APplusServer,
sql : sql_utils.SqlStatement,
raw:bool=False,
engine:Optional[sqlalchemy.Engine]=None) -> pd.DataFrame:
server: APplusServer,
sql: sql_utils.SqlStatement,
raw: bool = False,
engine: Optional[sqlalchemy.Engine] = None) -> pd.DataFrame:
"""Wrapper für pd.read_sql für sqlalchemy-engine.
:param server: APplusServer für Datenbankverbindung und complete-SQL
:type server: APplusServer
:param sql: das SQL-statement
"""
if engine is None:
engine = createSqlAlchemyEngine(server);
engine = createSqlAlchemyEngine(server)
with engine.connect() as conn:
return pd.read_sql(sqlalchemy.text(server.completeSQL(sql, raw=raw)), conn)
def _createHyperLinkGeneral(genOrg : Callable[[], str|int|float], genLink: Callable[[], str]) -> str|int|float:
def _createHyperLinkGeneral(genOrg: Callable[[], Union[str, int, float]], genLink: Callable[[], str]) -> Union[str, int, float]:
"""
Hilfsfunktion zum Generieren eines Excel-Links.
:param genLink: Funktion, die Parameter aufgerufen wird und einen Link generiert
:param genLink: Funktion, die Parameter aufgerufen wird und einen Link generiert
"""
org:str|int|float=""
org2:str|int|float
try:
org = genOrg();
org: Union[str, int, float] = ""
org2: Union[str, int, float]
try:
org = genOrg()
if not org:
return org
else :
else:
if isinstance(org, (int, float)):
org2 = org;
org2 = org
else:
org2 = "\"" + str(org).replace("\"", "\"\"") + "\""
org2 = "\"" + str(org).replace("\"", "\"\"") + "\""
return "=HYPERLINK(\"{}\", {})".format(genLink(), org2)
except:
msg = traceback.format_exc();
print ("Exception: {}".format(msg))
msg = traceback.format_exc()
print("Exception: {}".format(msg))
return org
def mkDataframeColumn(df : pd.DataFrame, makeValue : AggFuncType) -> pd.Series:
def mkDataframeColumn(df: pd.DataFrame, makeValue: AggFuncType) -> pd.Series:
"""
Erzeugt für alle Zeilen eines Dataframes eine neuen Wert. Dies wird benutzt, um eine Spalte zu berechnen.
Diese kann eine Originalspalte ersetzen, oder neu hinzugefügt werden.
:param df: der Dataframe
:param makeValue: Funktion, die eine Zeile als Parameter bekommt und den neuen Wert berechnet
"""
def mkValueWrapper(r): # type: ignore
def mkValueWrapper(r): # type: ignore
try:
return makeValue(r)
except:
msg = traceback.format_exc();
print ("Exception: {}".format(msg))
msg = traceback.format_exc()
print("Exception: {}".format(msg))
return ""
if (len(df.index) > 0):
return df.apply(mkValueWrapper, axis=1)
else:
return df.apply(lambda r: "", axis=1);
return df.apply(lambda r: "", axis=1)
def mkHyperlinkDataframeColumn(df : pd.DataFrame, makeOrig : AggFuncType, makeLink : Callable[[Any], str]) -> pd.Series :
def mkHyperlinkDataframeColumn(df: pd.DataFrame, makeOrig: AggFuncType, makeLink: Callable[[Any], str]) -> pd.Series:
"""
Erzeugt für alle Zeilen eines Dataframes einen Hyperlink. Dies wird benutzt, um eine Spalte mit einem Hyperlink zu berechnen.
Diese kann eine Originalspalte ersetzen, oder neu hinzugefügt werden.
:param df: der Dataframe
:param makeOrig: Funktion, die eine Zeile als Parameter bekommt und den Wert berechnet, der angezeigt werden soll
:param makeLink: Funktion, die eine Zeile als Parameter bekommt und den Link berechnet
"""
if (len(df.index) > 0):
return df.apply(lambda r: _createHyperLinkGeneral(lambda : makeOrig(r), lambda : makeLink(r)), axis=1)
return df.apply(lambda r: _createHyperLinkGeneral(lambda: makeOrig(r), lambda: makeLink(r)), axis=1)
else:
return df.apply(lambda r: "", axis=1);
return df.apply(lambda r: "", axis=1)
def exportToExcel(
filename:FilePath | WriteExcelBuffer | pd.ExcelWriter,
dfs : Sequence[Tuple[pd.DataFrame, str]],
addTable:bool=True) -> None:
filename: Union[FilePath, WriteExcelBuffer, pd.ExcelWriter],
dfs: Sequence[Tuple[pd.DataFrame, str]],
addTable: bool = True) -> None:
"""
Schreibt eine Menge von Dataframes in eine Excel-Tabelle
:param filename: Name der Excel-Datei
:param filename: Name der Excel-Datei
:param dfs: Liste von Tupeln aus DataFrames und Namen von Sheets.
"""
with pd.ExcelWriter(filename, engine='xlsxwriter') as writer:
with pd.ExcelWriter(filename, engine='xlsxwriter') as writer:
for (df, name) in dfs:
df.to_excel(writer, sheet_name=name, index=False, header=True)
ws = writer.sheets[name]
@ -126,6 +125,4 @@ def exportToExcel(
ws.add_table(0, 0, max_row, max_col - 1, {'columns': column_settings})
# Spaltenbreiten anpassen
ws.autofit();
ws.autofit()

File diff suppressed because it is too large Load Diff

View File

@ -6,13 +6,12 @@
# license that can be found in the LICENSE file or at
# https://opensource.org/licenses/MIT.
#-*- coding: utf-8 -*-
import pathlib
import datetime
from typing import *
from typing import Set, Union
def checkDirExists(dir : Union[str, pathlib.Path]) -> pathlib.Path:
def checkDirExists(dir: Union[str, pathlib.Path]) -> pathlib.Path:
"""Prüft, ob ein Verzeichnis existiert. Ist dies nicht möglich, wird eine Exception geworfen.
:param dir: das Verzeichnis
@ -26,19 +25,19 @@ def checkDirExists(dir : Union[str, pathlib.Path]) -> pathlib.Path:
dir = dir.resolve()
if not (dir.exists()):
raise Exception("Verzeichnis '" + str(dir) + "' nicht gefunden");
raise Exception("Verzeichnis '" + str(dir) + "' nicht gefunden")
if not (dir.is_dir()):
raise Exception("'" + str(dir) + "' ist kein Verzeichnis");
return dir;
raise Exception("'" + str(dir) + "' ist kein Verzeichnis")
return dir
def formatDateTimeForAPplus(v : Union[datetime.datetime, datetime.date, datetime.time]) -> str:
def formatDateTimeForAPplus(v: Union[datetime.datetime, datetime.date, datetime.time]) -> str:
"""Formatiert ein Datum oder eine Uhrzeit für APplus"""
if (v == None):
return "";
if v is None:
return ""
elif isinstance(v, str):
return v;
return v
elif isinstance(v, datetime.datetime):
return v.strftime("%Y-%m-%d %H:%M:%S.%f")[:-3]
elif isinstance(v, datetime.date):
@ -47,10 +46,11 @@ def formatDateTimeForAPplus(v : Union[datetime.datetime, datetime.date, datetime
return v.strftime("%H:%M:%S.%f")[:-3]
else:
return str(v)
def containsOnlyAllowedChars(charset : Set[str], s : str) -> bool:
def containsOnlyAllowedChars(charset: Set[str], s: str) -> bool:
"""Enthält ein String nur erlaubte Zeichen?"""
for c in s:
if not (c in charset):
if not (c in charset):
return False
return True

View File

@ -7,17 +7,17 @@
# https://opensource.org/licenses/MIT.
from PyAPplus64 import applus_db
import datetime
def test_DBTableIDs1() -> None:
ids = applus_db.DBTableIDs();
ids = applus_db.DBTableIDs()
assert (str(ids) == "{}")
ids.add("t1", 1)
assert (str(ids) == "{'T1': {1}}")
ids.add("t1", 2,3,4)
ids.add("t1", 2, 3, 4)
assert (str(ids) == "{'T1': {1, 2, 3, 4}}")
assert (ids.getTable("T1") == {1, 2, 3, 4})
assert (ids.getTable("T2") == set())
ids.add("t2", 2,3,4)
assert (ids.getTable("T2") == {2,3,4})
ids.add("t2", 2, 3, 4)
assert (ids.getTable("T2") == {2, 3, 4})
assert (str(ids) == "{'T1': {1, 2, 3, 4}, 'T2': {2, 3, 4}}")

View File

@ -9,284 +9,350 @@
from PyAPplus64 import sql_utils
import datetime
def test_normaliseDBField1() -> None:
def test_normaliseDBField1() -> None:
assert (sql_utils.normaliseDBfield("aAa") == "AAA")
assert (sql_utils.normaliseDBfield("a#Aa") == "A#AA")
assert (sql_utils.normaliseDBfield("2") == "2")
def test_normaliseDBFieldSet() -> None:
def test_normaliseDBFieldSet() -> None:
assert (sql_utils.normaliseDBfieldSet(set()) == set())
assert (sql_utils.normaliseDBfieldSet({"aAa", "b", "c", "2"}) == {"2", "AAA", "B", "C"})
def test_normaliseDBFieldList() -> None:
def test_normaliseDBFieldList() -> None:
assert (sql_utils.normaliseDBfieldList([]) == [])
assert (sql_utils.normaliseDBfieldList(["aAa", "b", "c", "2"]) == ["AAA", "B", "C", "2"])
def test_SqlField1() -> None:
assert (str(sql_utils.SqlField("abc")) == "ABC")
def test_SqlField2() -> None:
assert (str(sql_utils.SqlField("t.abc")) == "T.ABC")
def test_SqlParam() -> None:
assert (str(sql_utils.sqlParam) == "?")
def test_SqlDateTime() -> None:
dt = datetime.datetime(year=2023, month=1, day=12, hour=9, minute=59, second=12, microsecond=2344)
assert (str(sql_utils.SqlDateTime(dt)) == "2023-01-12T09:59:12.002")
def test_SqlDate() -> None:
dt = datetime.datetime(year=2023, month=1, day=12, hour=9, minute=59, second=12, microsecond=2344)
assert (str(sql_utils.SqlDate(dt)) == "20230112")
def test_formatSqlValueString1() -> None:
assert(sql_utils.formatSqlValueString("") == "''");
assert (sql_utils.formatSqlValueString("") == "''")
def test_formatSqlValueString2() -> None:
assert(sql_utils.formatSqlValueString("abc") == "'abc'");
assert (sql_utils.formatSqlValueString("abc") == "'abc'")
def test_formatSqlValueString3() -> None:
assert(sql_utils.formatSqlValueString("a b c") == "'a b c'");
assert (sql_utils.formatSqlValueString("a b c") == "'a b c'")
def test_formatSqlValueString4() -> None:
assert(sql_utils.formatSqlValueString("a \"b\" c") == "'a \"b\" c'");
assert (sql_utils.formatSqlValueString("a \"b\" c") == "'a \"b\" c'")
def test_formatSqlValueString5() -> None:
assert(sql_utils.formatSqlValueString("a 'b'\nc") == "'a ''b''\nc'");
assert (sql_utils.formatSqlValueString("a 'b'\nc") == "'a ''b''\nc'")
def test_formatSqlValue1() -> None:
assert(sql_utils.formatSqlValue(2) == "2");
assert (sql_utils.formatSqlValue(2) == "2")
def test_formatSqlValue2() -> None:
assert(sql_utils.formatSqlValue(2.4) == "2.4");
assert (sql_utils.formatSqlValue(2.4) == "2.4")
def test_formatSqlValue3() -> None:
assert(sql_utils.formatSqlValue("AA") == "'AA'");
assert (sql_utils.formatSqlValue("AA") == "'AA'")
def test_formatSqlValue4() -> None:
assert(sql_utils.formatSqlValue(sql_utils.SqlField("aa")) == "AA");
assert (sql_utils.formatSqlValue(sql_utils.SqlField("aa")) == "AA")
def test_formatSqlValue5() -> None:
assert(sql_utils.formatSqlValue(0) == "0");
assert (sql_utils.formatSqlValue(0) == "0")
def test_formatSqlValue6() -> None:
dt = datetime.datetime(year=2023, month=1, day=12, hour=9, minute=59, second=12, microsecond=2344)
assert(sql_utils.formatSqlValue(sql_utils.SqlDateTime(dt)) == "'2023-01-12T09:59:12.002'");
assert (sql_utils.formatSqlValue(sql_utils.SqlDateTime(dt)) == "'2023-01-12T09:59:12.002'")
def test_SqlConditionTrue() -> None:
assert(str(sql_utils.SqlConditionTrue()) == "(1=1)");
assert (str(sql_utils.SqlConditionTrue()) == "(1=1)")
def test_SqlConditionFalse() -> None:
assert(str(sql_utils.SqlConditionFalse()) == "(1=0)");
assert (str(sql_utils.SqlConditionFalse()) == "(1=0)")
def test_SqlConditionBool1() -> None:
assert(str(sql_utils.SqlConditionBool(True)) == "(1=1)");
assert (str(sql_utils.SqlConditionBool(True)) == "(1=1)")
def test_SqlConditionBool2() -> None:
assert(str(sql_utils.SqlConditionBool(False)) == "(1=0)");
assert (str(sql_utils.SqlConditionBool(False)) == "(1=0)")
def test_SqlConditionIsNull() -> None:
cond = sql_utils.SqlConditionIsNull("AA");
assert(str(cond) == "('AA' is null)");
cond = sql_utils.SqlConditionIsNull("AA")
assert (str(cond) == "('AA' is null)")
def test_SqlConditionIsNotNull() -> None:
cond = sql_utils.SqlConditionIsNotNull("AA");
assert(str(cond) == "('AA' is not null)");
cond = sql_utils.SqlConditionIsNotNull("AA")
assert (str(cond) == "('AA' is not null)")
def test_SqlConditionNot() -> None:
cond1 = sql_utils.SqlConditionIsNull("AA");
cond = sql_utils.SqlConditionNot(cond1);
assert(str(cond) == "(not ('AA' is null))");
cond1 = sql_utils.SqlConditionIsNull("AA")
cond = sql_utils.SqlConditionNot(cond1)
assert (str(cond) == "(not ('AA' is null))")
def test_SqlConditionStringStartsWith() -> None:
cond = sql_utils.SqlConditionStringStartsWith("f", "a'an")
assert(str(cond) == "(left(F, 4) = 'a''an')");
assert (str(cond) == "(left(F, 4) = 'a''an')")
def test_SqlConditionIn1() -> None:
cond = sql_utils.SqlConditionIn(sql_utils.SqlField("f"), [])
assert(str(cond) == "(1=0)");
assert (str(cond) == "(1=0)")
def test_SqlConditionIn2() -> None:
cond = sql_utils.SqlConditionIn(sql_utils.SqlField("f"), ["a"])
assert(str(cond) == "(F = 'a')");
assert (str(cond) == "(F = 'a')")
def test_SqlConditionIn3() -> None:
cond = sql_utils.SqlConditionIn(sql_utils.SqlField("f"), ["a", "a'A", "b", "c"])
assert(str(cond) == "(F in ('a', 'a''A', 'b', 'c'))");
assert (str(cond) == "(F in ('a', 'a''A', 'b', 'c'))")
def test_SqlConditionStringNotEmpty1() -> None:
cond = sql_utils.SqlConditionFieldStringNotEmpty("f")
assert(str(cond) == "(F is not null and F != '')");
assert (str(cond) == "(F is not null and F != '')")
def test_SqlConditionEq1() -> None:
cond = sql_utils.SqlConditionEq("f1", None)
assert(str(cond) == "('f1' is null)");
assert (str(cond) == "('f1' is null)")
def test_SqlConditionEq2() -> None:
cond = sql_utils.SqlConditionEq(None, "f1")
assert(str(cond) == "('f1' is null)");
assert (str(cond) == "('f1' is null)")
def test_SqlConditionEq3() -> None:
cond = sql_utils.SqlConditionEq(sql_utils.SqlField("f1"), sql_utils.SqlField("f2"))
assert(str(cond) == "(F1 = F2)");
assert (str(cond) == "(F1 = F2)")
def test_SqlConditionEq4() -> None:
cond = sql_utils.SqlConditionEq(sql_utils.SqlField("f1"), "aa'a")
assert(str(cond) == "(F1 = 'aa''a')");
assert (str(cond) == "(F1 = 'aa''a')")
def test_SqlConditionEq5() -> None:
cond = sql_utils.SqlConditionEq(sql_utils.SqlField("f1"), 2)
assert(str(cond) == "(F1 = 2)");
assert (str(cond) == "(F1 = 2)")
def test_SqlConditionEq6() -> None:
cond = sql_utils.SqlConditionEq(sql_utils.SqlField("f1"), True)
assert(str(cond) == "(F1 = 1)");
assert (str(cond) == "(F1 = 1)")
def test_SqlConditionEq7() -> None:
cond = sql_utils.SqlConditionEq(sql_utils.SqlField("f1"), False)
assert(str(cond) == "(F1 = 0 OR F1 is null)");
assert (str(cond) == "(F1 = 0 OR F1 is null)")
def test_SqlConditionEq8() -> None:
cond = sql_utils.SqlConditionEq(True, sql_utils.SqlField("f1"))
assert(str(cond) == "(F1 = 1)");
assert (str(cond) == "(F1 = 1)")
def test_SqlConditionEq9() -> None:
cond = sql_utils.SqlConditionEq(False, sql_utils.SqlField("f1"))
assert(str(cond) == "(F1 = 0 OR F1 is null)");
assert (str(cond) == "(F1 = 0 OR F1 is null)")
def test_SqlConditionEq10() -> None:
cond = sql_utils.SqlConditionEq(False, True)
assert(str(cond) == "(1=0)");
assert (str(cond) == "(1=0)")
def test_SqlConditionEq11() -> None:
cond = sql_utils.SqlConditionEq(True, True)
assert(str(cond) == "(1=1)");
assert (str(cond) == "(1=1)")
def test_SqlConditionFieldEq1() -> None:
cond = sql_utils.SqlConditionFieldEq("f1", None)
assert(str(cond) == "(F1 is null)");
assert (str(cond) == "(F1 is null)")
def test_SqlConditionFieldEq2() -> None:
cond = sql_utils.SqlConditionFieldEq("f1", sql_utils.SqlField("f2"))
assert(str(cond) == "(F1 = F2)");
assert (str(cond) == "(F1 = F2)")
def test_SqlConditionFieldEq3() -> None:
cond = sql_utils.SqlConditionFieldEq("f1", "aa'a")
assert(str(cond) == "(F1 = 'aa''a')");
assert (str(cond) == "(F1 = 'aa''a')")
def test_SqlConditionFieldEq4() -> None:
cond = sql_utils.SqlConditionFieldEq("f1", 2)
assert(str(cond) == "(F1 = 2)");
assert (str(cond) == "(F1 = 2)")
def test_SqlConditionFieldEq5() -> None:
cond = sql_utils.SqlConditionFieldEq("f1", sql_utils.sqlParam)
assert(str(cond) == "(F1 = ?)");
assert (str(cond) == "(F1 = ?)")
def test_SqlConditionLt1() -> None:
cond = sql_utils.SqlConditionLt(sql_utils.SqlField("f"), sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F < '20221212')");
assert (str(cond) == "(F < '20221212')")
def test_SqlConditionLt2() -> None:
cond = sql_utils.SqlConditionLt(2, sql_utils.SqlField("f"))
assert(str(cond) == "(2 < F)");
assert (str(cond) == "(2 < F)")
def test_SqlConditionGt1() -> None:
cond = sql_utils.SqlConditionGt(sql_utils.SqlField("f"), sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F > '20221212')");
assert (str(cond) == "(F > '20221212')")
def test_SqlConditionGt2() -> None:
cond = sql_utils.SqlConditionGt(2, sql_utils.SqlField("f"))
assert(str(cond) == "(2 > F)");
assert (str(cond) == "(2 > F)")
def test_SqlConditionLe1() -> None:
cond = sql_utils.SqlConditionLe(sql_utils.SqlField("f"), sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F <= '20221212')");
assert (str(cond) == "(F <= '20221212')")
def test_SqlConditionLe2() -> None:
cond = sql_utils.SqlConditionLe(2, sql_utils.SqlField("f"))
assert(str(cond) == "(2 <= F)");
assert (str(cond) == "(2 <= F)")
def test_SqlConditionGe1() -> None:
cond = sql_utils.SqlConditionGe(sql_utils.SqlField("f"), sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F >= '20221212')");
assert (str(cond) == "(F >= '20221212')")
def test_SqlConditionGe2() -> None:
cond = sql_utils.SqlConditionGe(2, sql_utils.SqlField("f"))
assert(str(cond) == "(2 >= F)");
assert (str(cond) == "(2 >= F)")
def test_SqlConditionFieldLt1() -> None:
cond = sql_utils.SqlConditionFieldLt("f", sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F < '20221212')");
assert (str(cond) == "(F < '20221212')")
def test_SqlConditionFieldLe1() -> None:
cond = sql_utils.SqlConditionFieldLe("f", sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F <= '20221212')");
assert (str(cond) == "(F <= '20221212')")
def test_SqlConditionFieldGt1() -> None:
cond = sql_utils.SqlConditionFieldGt("f", sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F > '20221212')");
assert (str(cond) == "(F > '20221212')")
def test_SqlConditionFieldGe1() -> None:
cond = sql_utils.SqlConditionFieldGe("f", sql_utils.SqlDate(datetime.date(year=2022, month=12, day=12)))
assert(str(cond) == "(F >= '20221212')");
assert (str(cond) == "(F >= '20221212')")
def test_SqlConditionAnd1() -> None:
conj = sql_utils.SqlConditionAnd();
assert(str(conj) == "(1=1)");
conj = sql_utils.SqlConditionAnd()
assert (str(conj) == "(1=1)")
def test_SqlConditionAnd2() -> None:
cond1 = sql_utils.SqlConditionPrepared("cond1");
conj = sql_utils.SqlConditionAnd();
cond1 = sql_utils.SqlConditionPrepared("cond1")
conj = sql_utils.SqlConditionAnd()
conj.addCondition(cond1)
assert(str(conj) == "cond1");
assert (str(conj) == "cond1")
def test_SqlConditionAnd3() -> None:
cond1 = sql_utils.SqlConditionPrepared("cond1");
cond2 = sql_utils.SqlConditionPrepared("cond2");
conj = sql_utils.SqlConditionAnd();
cond1 = sql_utils.SqlConditionPrepared("cond1")
cond2 = sql_utils.SqlConditionPrepared("cond2")
conj = sql_utils.SqlConditionAnd()
conj.addCondition(cond1)
conj.addCondition(cond2)
assert(str(conj) == "(cond1 AND cond2)");
assert (str(conj) == "(cond1 AND cond2)")
def test_SqlConditionAnd4() -> None:
cond1 = sql_utils.SqlConditionPrepared("cond1");
cond2 = sql_utils.SqlConditionPrepared("cond2");
cond3 = sql_utils.SqlConditionPrepared("cond3");
conj = sql_utils.SqlConditionAnd();
cond1 = sql_utils.SqlConditionPrepared("cond1")
cond2 = sql_utils.SqlConditionPrepared("cond2")
cond3 = sql_utils.SqlConditionPrepared("cond3")
conj = sql_utils.SqlConditionAnd()
conj.addCondition(cond1)
conj.addCondition(cond2)
conj.addCondition(cond3)
assert(str(conj) == "(cond1 AND cond2 AND cond3)");
assert (str(conj) == "(cond1 AND cond2 AND cond3)")
def test_SqlConditionOr1() -> None:
conj = sql_utils.SqlConditionOr();
assert(str(conj) == "(1=0)");
conj = sql_utils.SqlConditionOr()
assert (str(conj) == "(1=0)")
def test_SqlConditionOr2() -> None:
cond1 = sql_utils.SqlConditionPrepared("cond1");
conj = sql_utils.SqlConditionOr();
cond1 = sql_utils.SqlConditionPrepared("cond1")
conj = sql_utils.SqlConditionOr()
conj.addCondition(cond1)
assert(str(conj) == "cond1");
assert (str(conj) == "cond1")
def test_SqlConditionOr3() -> None:
cond1 = sql_utils.SqlConditionPrepared("cond1");
cond2 = sql_utils.SqlConditionPrepared("cond2");
conj = sql_utils.SqlConditionOr();
cond1 = sql_utils.SqlConditionPrepared("cond1")
cond2 = sql_utils.SqlConditionPrepared("cond2")
conj = sql_utils.SqlConditionOr()
conj.addCondition(cond1)
conj.addCondition(cond2)
assert(str(conj) == "(cond1 OR cond2)");
assert (str(conj) == "(cond1 OR cond2)")
def test_SqlConditionOr4() -> None:
cond1 = sql_utils.SqlConditionPrepared("cond1");
cond2 = sql_utils.SqlConditionPrepared("cond2");
cond3 = sql_utils.SqlConditionPrepared("cond3");
conj = sql_utils.SqlConditionOr();
cond1 = sql_utils.SqlConditionPrepared("cond1")
cond2 = sql_utils.SqlConditionPrepared("cond2")
cond3 = sql_utils.SqlConditionPrepared("cond3")
conj = sql_utils.SqlConditionOr()
conj.addCondition(cond1)
conj.addCondition(cond2)
conj.addCondition(cond3)
assert(str(conj) == "(cond1 OR cond2 OR cond3)");
assert (str(conj) == "(cond1 OR cond2 OR cond3)")
def test_SqlStatementSelect1() -> None:
sql = sql_utils.SqlStatementSelect("tabelle t")
@ -323,6 +389,7 @@ def test_SqlStatementSelect2() -> None:
sql.addJoin("left join t3 on cond3")
assert (str(sql) == "SELECT * FROM t1 left join t2 on cond2 left join t3 on cond3")
def test_SqlStatementSelect4() -> None:
sql = sql_utils.SqlStatementSelect("t")
sql.where.addCondition("cond1")
@ -331,9 +398,10 @@ def test_SqlStatementSelect4() -> None:
sql.where.addCondition("cond2")
assert (str(sql) == "SELECT * FROM t WHERE ((cond1) AND (cond2))")
def test_SqlStatementSelect5() -> None:
sql = sql_utils.SqlStatementSelect("t")
cond = sql_utils.SqlConditionOr();
cond = sql_utils.SqlConditionOr()
sql.where.addCondition(cond)
cond.addCondition("cond1")
assert (str(sql) == "SELECT * FROM t WHERE (cond1)")
@ -341,9 +409,10 @@ def test_SqlStatementSelect5() -> None:
cond.addCondition("cond2")
assert (str(sql) == "SELECT * FROM t WHERE ((cond1) OR (cond2))")
def test_SqlStatementSelect6() -> None:
sql = sql_utils.SqlStatementSelect("t")
sql.where = sql_utils.SqlConditionOr();
sql.where = sql_utils.SqlConditionOr()
sql.where.addCondition("cond1")
assert (str(sql) == "SELECT * FROM t WHERE (cond1)")