Initial commit

This commit is contained in:
Thomas Türk 2023-05-04 15:06:55 +02:00
commit f8dd0a008d
38 changed files with 4101 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@ -0,0 +1,4 @@
/dist
/docs/build/
/src/PyAPplus64.egg-info
__pycache__

9
LICENSE Normal file
View File

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

6
MANIFEST.in Normal file
View File

@ -0,0 +1,6 @@
include examples
include docs/*
exclude docs/builddocspdf.sh
include docs/source/*
include src/PyAPplus64/py.typed

54
README.md Normal file
View File

@ -0,0 +1,54 @@
# 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.
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.
- das Duplizieren von Datensätzen
+ zu kopierende Felder aus XML-Definitionen werden ausgewertet
+ Abhängige Objekte können einfach ebenfalls mit-kopiert werden
+ Änderungen wie beispielsweise Nummer des Objektes möglich
+ Unterstützung für Kopieren dyn. Attribute
+ Anlage neuer Objekte mittels APP-Server
+ Datensätze können zwischen Systemen kopiert und auch in Dateien gespeichert werden
+ Beispiel: Kopieren von Artikeln mit Arbeitsplan und Stückliste zwischen Deploy- und Prod-System
- einfaches Erstellen von Excel-Reports aus SQL-Abfragen
+ mittels Pandas und XlsxWriter
+ einfache Wrapper um andere Libraries, spart aber Zeit
- ...
In `PyAPplus64` wurden die Features (vielleicht leicht verallgemeinert)
implementiert, die ich für konkrete Aufgabenstellungen benötigte. Ich gehe davon
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.
## Lizenz
`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/

20
docs/Makefile Normal file
View File

@ -0,0 +1,20 @@
# Minimal makefile for Sphinx documentation
#
# You can set these variables from the command line, and also
# from the environment for the first two.
SPHINXOPTS ?=
SPHINXBUILD ?= sphinx-build
SOURCEDIR = source
BUILDDIR = build
# Put it first so that "make" without argument is like "make help".
help:
@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
.PHONY: help Makefile
# Catch-all target: route all unknown targets to Sphinx using the new
# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
%: Makefile
@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)

4
docs/builddocs.sh Executable file
View File

@ -0,0 +1,4 @@
#!/bin/sh
sphinx-apidoc -T -f ../src/PyAPplus64 -o source/generated
sphinx-build -a -E -b html source build/html

14
docs/builddocspdf.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/sh
##
## Copyright (c) 2023 Thomas Tuerk (kontakt@thomas-tuerk.de)
##
## This file is part of PyAPplus64.
##
sphinx-apidoc -T -f ../src/PyAPplus64 -o source/generated
sphinx-build -a -E -b latex source build/pdf
cd build/pdf
pdflatex pyapplus64.tex
pdflatex pyapplus64.tex

35
docs/make.bat Normal file
View File

@ -0,0 +1,35 @@
@ECHO OFF
pushd %~dp0
REM Command file for Sphinx documentation
if "%SPHINXBUILD%" == "" (
set SPHINXBUILD=sphinx-build
)
set SOURCEDIR=source
set BUILDDIR=build
%SPHINXBUILD% >NUL 2>NUL
if errorlevel 9009 (
echo.
echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
echo.installed, then set the SPHINXBUILD environment variable to point
echo.to the full path of the 'sphinx-build' executable. Alternatively you
echo.may add the Sphinx directory to PATH.
echo.
echo.If you don't have Sphinx installed, grab it from
echo.https://www.sphinx-doc.org/
exit /b 1
)
if "%1" == "" goto help
%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
goto end
:help
%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%
:end
popd

View File

@ -0,0 +1,40 @@
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.
Dieser kann von Microsoft bezogen werden.
zeep
----
Die Soap-Library ``zeep`` wird benutzt (``python -m pip install zeep``).
PyYaml
------
Die Library ``pyyaml`` wird für Config-Dateien benutzt (``python -m pip install pyyaml``).
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
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
sein, falls sich die Struktur der Dokumentation ändert.
Diese Aufrufe werden von ``builddocs.sh`` automatisiert.
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`).

View File

@ -0,0 +1,92 @@
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.
Als triviales Beispiel sucht folgender Code alle `DOCUMENTS` Einträge in
Artikeln (angezeigt als `Bild` in `ArtikelRec.aspx`), für die Datei, auf die
verwiesen wird, nicht im Dateisystem existiert. Diese fehlenden Dateien werden
ausgegeben und das Feld `DOCUMENTS` gelöscht. Das Löschen erfolgt dabei über
`useXML`, so dass die Felder `UPDDATE` und `UPDUSER` korrekt gesetzt werden.
.. literalinclude:: ../../examples/check_dokumente.py
:language: python
:linenos:
Man kann alle Python Bibliotheken nutzen. Als Erweiterung wäre es in obigem Script
zum Beispiel einfach möglich, alle BMP-Bilder zu suchen, nach PNG zu konvertieren
und den DB-Eintrag anzupassen.
Ad-hoc Reports
--------------
APplus erlaubt es mittels Jasper-Reports, flexible und schöne Reports zu erzeugen.
Dies funktioniert gut und ist für regelmäßig benutzte Reports sehr sinnvoll.
Für ad-hoc Reports, die nur sehr selten oder sogar nur einmal benutzt werden, ist die
Erstellung eines Jasper-Reports und die Einbindung in APplus jedoch zu zeitaufwändig.
Teilweise genügen die Ergebnisse einer SQL-Abfrage, die direkt im MS SQL Server abgesetzt
werden kann. Wird es etwas komplizierter oder sollen die Ergebnisse noch etwas verarbeitet
werden, bietet sich evtl. Python an.
Folgendes Script erzeugt zum Beispiel eine Excel-Tabelle, mit einer Übersicht,
welche Materialen wie oft für Artikel benutzt werden:
.. literalinclude:: ../../examples/adhoc_report.py
:language: python
:linenos:
Dieses kurze Script nutzt Standard-Pandas Methoden zur Erzeugung der Excel-Datei. Allerdings
sind diese Methoden in den Aufrufen von `pandasReadSql` und `exportToExcel` gekapselt,
so dass der Aufruf sehr einfach ist. Zudem ist es sehr einfach, die Verbindung zur Datenbank
und zum APP-Server mittels der YAML-Konfigdatei herzustellen. Bei diesem
Aufruf kann optional ein Nutzer und eine Umgebung übergeben werden, die die Standard-Werte aus
der YAML-Datei überschreiben. `pandasReadSql` nutzt intern `completeSQL`, so dass
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
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.
Ebenso wird auf die SysConf zugegriffen.
Viele solcher Anpassungen lassen sich gut und sinnvoll im App-Server einrichten und als Job regelmäßig aufrufen.
Im konkreten Fall wird jedoch für die Verarbeitung der Dateien viel Rechenzeit benötigt. Dies würde dadurch
verschlimmert, dass die Dateien nicht auf der gleichen Maschine wie der App-Server liegen und somit
viele, relativ langsame Dateizugriffe über das Netzwerk nötig wären. Hinzu kommt, dass die
Verarbeitung der Dateien dank existierender Bibliotheken in Python einfacher ist.
`PyAPplus64` kann für solche Anpassungen eine interessante Alternative zur Implementierung im App-Server
oder zur Entwicklung eines Tools ohne spezielle Bibliotheken sein. Nach Initialisierung des Servers::
import PyAPplus64
server = PyAPplus64.applus.applusFromConfigFile("my-applus-server.yaml")
bietet `P2APplus64` wie oben demonstriert einfachen lesenden und schreibenden Zugriff auf die DB. Hierbei
werden automatisch die für die Umgebung nötigen Mandanten zu den SQL Statements hinzugefügt. Zudem ist einfach ein
Zugriff auf die Sysconf möglich::
print (server.sysconf.getString("STAMM", "MYLAND"))
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::
client = server.server_conn.getClient("p2system", "SysConf");
print (client.service.getString("STAMM", "MYLAND"))

View File

@ -0,0 +1,42 @@
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. `PyAPplus64` erlaubt u.a.
- einfacher Zugriff auf SOAP-Schnittstelle des APP-Servers
+ damit Zugriff auf SysConfig
+ Zugriff auf Tools wie z.B. `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 AppServer 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.
- das Duplizieren von Datensätzen
+ zu kopierende Felder aus XML-Definitionen werden ausgewertet
+ Abhängige Objekte können einfach ebenfalls mit-kopiert werden
+ Änderungen wie beispielsweise Nummer des Objektes möglich
+ Unterstützung für Kopieren dyn. Attribute
+ Anlage neuer Objekte mittels APP-Server
+ Datensätze können zwischen Systemen kopiert und auch in Dateien gespeichert werden
+ Beispiel: Kopieren von Artikeln mit Arbeitsplan und Stückliste zwischen Deploy- und Prod-System
- einfaches Erstellen von Excel-Reports aus SQL-Abfragen
+ mittels Pandas und XlsxWriter
+ einfache Wrapper um andere Libraries, spart aber Zeit
- ...
In `PyAPplus64` wurden die Features (vielleicht leicht verallgemeinert)
implementiert, die ich für konkrete Aufgabenstellungen benötigte. Ich gehe davon
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. Zudem kann ich leider nicht garantieren, dass `PyAPplus64` fehlerfrei ist.

56
docs/source/conf.py Normal file
View File

@ -0,0 +1,56 @@
import pathlib
import sys
sys.path.append('../src/')
# Configuration file for the Sphinx documentation builder.
#
# For the full list of built-in configuration values, see the documentation:
# https://www.sphinx-doc.org/en/master/usage/configuration.html
# -- Project information -----------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
project = 'PyAPplus64'
copyright = '2023, Thomas Tuerk'
author = 'Thomas Tuerk'
# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
extensions = [
'sphinx.ext.duration',
'sphinx.ext.doctest',
'sphinx.ext.autodoc',
'sphinx.ext.autosummary',
]
templates_path = ['_templates']
exclude_patterns = [] # type: ignore
language = 'de'
# -- Options for HTML output -------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output
html_theme = 'nature'
# html_static_path = ['_static']
source_suffix = {
'.rst': 'restructuredtext',
}
latex_elements = {
# The paper size ('letterpaper' or 'a4paper').
'papersize': 'a4paper',
# The font size ('10pt', '11pt' or '12pt').
# 'pointsize': '11pt',
# Additional stuff for the LaTeX preamble.
# 'preamble': PREAMBLE
}
autodoc_type_aliases = {
'SqlValue': 'SqlValue'
}

73
docs/source/examples.rst Normal file
View File

@ -0,0 +1,73 @@
Beispiele
=========
Im Verzeichnis ``examples`` finden sich Python Dateien, die die Verwendung von `PyAPplus64` demonstrieren.
Config-Dateien
--------------
Viele Scripte teilen sich Einstellungen. Beispielsweise greifen fast alle Scripte irgendwie auf APplus zu und benötigen Informationen,
mit welchem APP-Server, welchem Web-Server und welcher Datenbank sie sich verbinden sollen. Solche Informationen, insbesondere die Passwörter, werden nicht in
jedem Script gespeichert, sondern nur in den Config-Dateien. Es bietet sich wohl meist an, 3 Konfigdateien zu erstellen, je eine für
das Deploy-, das Test- und das Prod-System. Ein Beispiel ist im Unterverzeichnis ``examples/applus-server.yaml`` zu finden.
.. literalinclude:: ../../examples/applus-server.yaml
:language: yaml
: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.
.. literalinclude:: ../../examples/applus_configs.py
:language: python
:linenos:
``check_dokumente.py``
-----------------------
Einfaches Beispiel für lesenden und schreibenden Zugriff auf APplus Datenbank.
.. literalinclude:: ../../examples/check_dokumente.py
:language: python
:lines: 6-
:linenos:
``adhoc_report.py``
-------------------
Sehr einfaches Beispiel zur Erstellung einer Excel-Tabelle aus einer SQL-Abfrage.
.. literalinclude:: ../../examples/adhoc_report.py
:language: python
:lines: 7-
:linenos:
``mengenabweichung.py``
-----------------------
Etwas komplizierteres Beispiel zur Erstellung einer Excel-Datei aus SQL-Abfragen.
.. literalinclude:: ../../examples/mengenabweichung.py
:language: python
:lines: 9-
:linenos:
``mengenabweichung_gui.py``
---------------------------
Beispiel für eine sehr einfache GUI, die die Eingabe einfacher Parameter erlaubt.
Die GUI wird um die Erzeugung von Excel-Dateien mit Mengenabweichungen gebaut.
.. literalinclude:: ../../examples/mengenabweichung_gui.pyw
:language: python
:lines: 7-
:linenos:
``copy_artikel.py``
-----------------------
Beispiel, wie Artikel inklusive Arbeitsplan und Stückliste dupliziert werden kann.
.. literalinclude:: ../../examples/copy_artikel.py
:language: python
:lines: 21-
:linenos:

View File

@ -0,0 +1,93 @@
PyAPplus64 package
==================
Submodules
----------
PyAPplus64.applus module
------------------------
.. automodule:: PyAPplus64.applus
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.applus\_db module
----------------------------
.. automodule:: PyAPplus64.applus_db
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.applus\_scripttool module
------------------------------------
.. automodule:: PyAPplus64.applus_scripttool
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.applus\_server module
--------------------------------
.. automodule:: PyAPplus64.applus_server
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.applus\_sysconf module
---------------------------------
.. automodule:: PyAPplus64.applus_sysconf
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.applus\_usexml module
--------------------------------
.. automodule:: PyAPplus64.applus_usexml
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.duplicate module
---------------------------
.. automodule:: PyAPplus64.duplicate
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.pandas module
------------------------
.. automodule:: PyAPplus64.pandas
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.sql\_utils module
----------------------------
.. automodule:: PyAPplus64.sql_utils
:members:
:undoc-members:
:show-inheritance:
PyAPplus64.utils module
-----------------------
.. automodule:: PyAPplus64.utils
:members:
:undoc-members:
:show-inheritance:
Module contents
---------------
.. automodule:: PyAPplus64
:members:
:undoc-members:
:show-inheritance:

11
docs/source/index.rst Normal file
View File

@ -0,0 +1,11 @@
PyAPplus64 Dokumentation
########################
.. toctree::
:maxdepth: 1
beschreibung
anwendungen
abhaengigkeiten.rst
examples.rst
generated/PyAPplus64

36
examples/adhoc_report.py Normal file
View File

@ -0,0 +1,36 @@
# 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 PyAPplus64
import applus_configs
import pathlib
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")
df1 = PyAPplus64.pandas.pandasReadSql(server, sql1)
# Sql Select-Statements können auch über SqlStatementSelect zusammengebaut
# werden. Die ist bei vielen, komplizierten Bedingungen teilweise hilfreich.
sql2 = PyAPplus64.SqlStatementSelect("ARTIKEL")
sql2.addFields("Material", "count(*) as Anzahl")
sql2.addGroupBy("MATERIAL")
sql2.having.addConditionFieldIsNotNull("MATERIAL")
sql2.order = "Anzahl desc"
df2 = PyAPplus64.pandas.pandasReadSql(server, sql2)
# Ausgabe als Excel mit 2 Blättern
PyAPplus64.pandas.exportToExcel(outfile, [(df1, "Materialien"), (df2, "Materialien 2")], addTable=True)
if __name__ == "__main__":
main(applus_configs.serverConfYamlTest, "myout.xlsx")

View File

@ -0,0 +1,26 @@
# 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.
# Einstellung für die Verbindung mit dem APP-, Web- und DB-Server.
# Viele der Einstellungen sind im APplus Manager zu finden
appserver : {
server : "some-server",
port : 2037,
user : "asol.projects",
env : "default-umgebung" # hier wirklich Umgebung, nicht Mandant verwenden
}
webserver : {
baseurl : "http://some-server/APplusProd6/"
}
dbserver : {
server : "some-server",
db : "APplusProd6",
user : "SA",
password : "your-db-password"
}

View File

@ -0,0 +1,17 @@
# 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 pathlib
basedir = pathlib.Path(__file__)
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")

View File

@ -0,0 +1,31 @@
# 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 pathlib
import PyAPplus64
import applus_configs
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");
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)

59
examples/copy_artikel.py Normal file
View File

@ -0,0 +1,59 @@
# 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.
# Dieses Script demonstriert, wie mit Hilfe PyAPplus64.duplicate
# BusinessObjekte dupliziert werden können.
# Dies ist sowohl in der gleichen DB als auch in anderen DBs möglich.
# So kann z.B. ein einzelner Artikel aus Test in Prod kopiert werden.
# Ebenso ist es möglich, die Daten in einer Datei zwischenzuspeichern und
# später irgendwo anders einzuspielen.
#
# 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
# getestet werden. Diese vielen Scripte per Hand zu kopieren ist aufwändig
# und Fehleranfällig und kann mit solchen Admin-Scripten automatisiert werden.
import pathlib
import PyAPplus64
import applus_configs
import logging
import yaml
def main(confFile:pathlib.Path, artikel:str, artikelNeu:str|None=None) -> None:
# Server verbinden
server = PyAPplus64.applus.applusFromConfigFile(confFile)
# DuplicateBusinessObject für Artikel erstellen
dArt = PyAPplus64.duplicate.loadDBDuplicateArtikel(server, artikel);
# DuplicateBusinessObject zur Demonstration in YAML konvertieren und zurück
dArtYaml = yaml.dump(dArt);
print(dArtYaml);
dArt2 = yaml.load(dArtYaml, Loader=yaml.UnsafeLoader)
# Neue Artikel-Nummer bestimmen und DuplicateBusinessObject in DB schreiben
# Man könnte hier genauso gut einen anderen Server verwenden
if (artikelNeu is None):
artikelNeu = server.nextNumber("Artikel")
if not (dArt is None):
dArt.setFields({"artikel" : artikelNeu})
res = dArt.insert(server);
print(res);
if __name__ == "__main__":
# Logger Einrichten
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")

View File

@ -0,0 +1,182 @@
# 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.
# Erzeugt Excel-Tabellen mit Werkstattaufträgen und Werkstattauftragspositionen mit Mengenabweichungen
import datetime
import PyAPplus64
import applus_configs
import pandas as pd # type: ignore
import pathlib
from typing import *
def ladeAlleWerkstattauftragMengenabweichungen(
server:PyAPplus64.APplusServer,
cond: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",
"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(cond)
sql.order="w.UPDDATE"
dfOrg = PyAPplus64.pandas.pandasReadSql(server, sql);
# Add Links
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,
lambda r: server.makeWebLinkWauftrag(
bauftrag=r.BAUFTRAG, accessid=r.ID))
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"
}
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");
sql.addLeftJoin("personal p", "w.UPDUSER = p.PERSONAL")
sql.addFieldsTable("w", "ID", "BAUFTRAG", "POSITION", "AG")
sql.addFields("(w.MENGE-w.MENGE_IST) as MENGENABWEICHUNG")
sql.addFieldsTable("w", "MENGE", "MENGE_IST", "APLAN as ARTIKEL")
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(cond)
sql.order="w.UPDDATE"
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,
lambda r: server.makeWebLinkWauftrag(
bauftrag=r.BAUFTRAG, accessid=r.ID))
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,
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,
# 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"
}
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):
if month is None:
return PyAPplus64.sql_utils.SqlConditionDateTimeFieldInYear(field, year)
else:
return PyAPplus64.sql_utils.SqlConditionDateTimeFieldInMonth(field, year, month)
else:
return None
def computeFileName(year:int|None=None, month:int|None=None) -> str:
if year is None:
return 'mengenabweichungen-all.xlsx';
else:
if month is None:
return 'mengenabweichungen-{:04d}.xlsx'.format(year);
else:
return 'mengenabweichungen-{:04d}-{:02d}.xlsx'.format(year, month);
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);
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)
fn = computeFileName(year=year, month=month)
return _exportInternal(server, fn, cond)
def computePreviousMonthYear(cyear : int, cmonth :int) -> Tuple[int, int]:
if cmonth == 1:
return (cyear-1, 12)
else:
return (cyear, cmonth-1);
def computeNextMonthYear(cyear : int, cmonth :int) -> Tuple[int, int]:
if cmonth == 12:
return (cyear+1, 1)
else:
return (cyear, cmonth+1);
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
# export(cyear) # aktuelles Jahr
# export(cyear-1) # letztes Jahr
# export() # alles
if __name__ == "__main__":
main(applus_configs.serverConfYamlTest)

View File

@ -0,0 +1,98 @@
# 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 mengenabweichung
import datetime
import PyAPplus64
import applus_configs
import pathlib
from typing import *
def parseDate (dateS:str) -> Tuple[datetime.datetime|None, bool]:
if dateS is None or dateS == '':
return (None, True)
else:
try:
return (datetime.datetime.strptime(dateS, '%d.%m.%Y'), True)
except:
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 == '':
sg.popup_error("Es wurde keine Ausgabedatei ausgewählt.")
return
else:
file = pathlib.Path(fileS)
c = mengenabweichung.exportVonBis(server, file.as_posix(), von, bis)
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)
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,
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,
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.Button("Aktuelles Jahr"), sg.Button("Letztes Jahr")],
[sg.Button("Speichern"), sg.Button("Beenden")]
]
systemName = server.scripttool.getSystemName() + "/" + server.scripttool.getMandant()
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);
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));
if event == 'Letzter Monat':
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));
if event == 'Letztes Jahr':
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),
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);
window.close()
if __name__ == "__main__":
main(applus_configs.serverConfYamlProd)

4
mypy.ini Normal file
View File

@ -0,0 +1,4 @@
[mypy]
disallow_incomplete_defs = True
disallow_untyped_defs = True

33
pyproject.toml Normal file
View File

@ -0,0 +1,33 @@
[build-system]
requires = ["setuptools"]
build-backend = "setuptools.build_meta"
[project]
name = "PyAPplus64"
version = "1.0.0"
authors = [
{ name="Thomas Tuerk", email="kontakt@thomas-tuerk.de" },
]
description = "Verschiedene Hilfsmittel, um mit dem ERP System APplus zu interagieren. Dieses Packet wurde für APplus 6.4 entwickelt, funktioniert vermutlich aber auch mit anderen Versionen."
readme = "README.md"
requires-python = ">=3.6"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Development Status :: 3 - Alpha",
"Intended Audience :: Other Audience",
"Operating System :: Microsoft :: Windows"
]
dependencies = [
'pyodbc',
'PyYAML',
'SQLAlchemy',
'pandas',
'XlsxWriter',
'zeep'
]
[project.urls]
homepage = "https://www.thomas-tuerk.de/de/pyapplus64"
repository = "https://git.thomas-tuerk.de/thtuerk/PyAPplus64"
documentation = "https://www.thomas-tuerk.de/assets/PyAPplus64/html/index.html"

View File

@ -0,0 +1,30 @@
# 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 . import utils
from . import applus_db
from . import applus_scripttool
from . import applus_server
from . import applus_sysconf
from . import applus_usexml
from . import applus
from . import sql_utils
from . import duplicate
from . import utils
from .applus import APplusServer, applusFromConfigFile
from .sql_utils import (
SqlCondition,
SqlStatement,
SqlStatementSelect
)
try:
from . import pandas
except:
pass

287
src/PyAPplus64/applus.py Normal file
View File

@ -0,0 +1,287 @@
# 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.
#-*- coding: utf-8 -*-
from . import applus_db
from . import applus_server
from . import applus_sysconf
from . import applus_scripttool
from . import applus_usexml
from . import sql_utils
import yaml
import urllib.parse
from zeep import Client
import pyodbc # type: ignore
from typing import *
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
:type server_settings: APplusAppServerSettings
: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):
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.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.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);
"""erlaubt den Zugriff auf die Sysconfig"""
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")
def reconnectDB(self) -> None:
try:
self.db_conn.close()
except:
pass
self.db_conn = self.db_settings.connect()
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
:type raw: boolean
:return: das vervollständigte SQL-Statement
:rtype: str
"""
if raw:
return str(sql)
else:
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
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)
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.
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)
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)
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);
if 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.
Dieser Wert oder None wird geliefert."""
sqlC = self.completeSQL(sql, raw=raw);
return applus_db.rawQuerySingleValue(self.db_conn, sqlC, *args)
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);
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
resultierende client "client" genannt, dann kann
z.B. mittels "client.service.getCompleteSQL(sql)" vom AppServer ein Vervollständigen
des SQLs angefordert werden.
:param package: das Packet, z.B. "p2core"
:type package: str
:param name: der Name im Packet, z.B. "Table"
:type package: string
:return: den Client
:rtype: Client
"""
return self.server_conn.getClient(package, name);
def getTableFields(self, table:str, isComputed:Optional[bool]=None) -> Set[str]:
"""
Liefert die Namen aller Felder einer Tabelle.
:param table: Name der Tabelle
:param isComputed: wenn gesetzt, werden nur die Felder geliefert, die berechnet werden oder nicht berechnet werden
:return: Liste aller Feld-Namen
:rtype: {str}
"""
sql = sql_utils.SqlStatementSelect("SYS.TABLES T")
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)
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));
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
müssen.
"""
return applus_db.getUniqueFieldsOfTable(self.db_conn, table)
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:
"""
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
:rtype: applus_usexml.UseXmlRowInsert
"""
return applus_usexml.UseXmlRowInsert(self, table)
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:
"""
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
:rtype: applus_usexml.UseXmlRowInsertOrUpdate
"""
return applus_usexml.UseXmlRowInsertOrUpdate(self, table)
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:
delRow = self.mkUseXMLRowDelete(table, id)
delRow.delete();
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;
firstArg = True
for arg, argv in kwargs.items():
if not (argv == None):
if firstArg:
firstArg = False;
url += "?"
else:
url += "&"
url += arg + "=" + urllib.parse.quote(str(argv))
return url;
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 makeWebLinkBauftrag(self, **kwargs : Any) -> str :
return self.makeWebLink("wp/bauftragRec.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)
)
dbparams = applus_db.APplusDBSettings(
server=yamlDict["dbserver"]["server"],
database=yamlDict["dbserver"]["db"],
user=yamlDict["dbserver"]["user"],
password=yamlDict["dbserver"]["password"]);
return APplusServer(dbparams, app_server, web_server);
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:
yamlDict = yaml.safe_load(stream)
return applusFromConfigDict(yamlDict, user=user, env=env)
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)

171
src/PyAPplus64/applus_db.py Normal file
View File

@ -0,0 +1,171 @@
# 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.
#-*- coding: utf-8 -*-
import pyodbc # type: ignore
import logging
from .sql_utils import SqlStatement
from . import sql_utils
from typing import *
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):
self.server = server
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
"""
return ("Driver={SQL Server Native Client 11.0};"
"Server="+self.server+";"
"Database="+self.database+";"
"UID="+self.user+";"
"PWD="+self.password + ";")
def connect(self) -> pyodbc.Connection:
"""Stellt eine neue Verbindung her und liefert diese zurück.
"""
return pyodbc.connect(self.getConnectionString())
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:
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]: