Einmal schnell gucken, wie viele und welche Art von Produkten über welchen SalesChannel und URL zu kaufen sind.
SELECT x.url, 'single' type, count(x.id)
FROM (
select d.url, p.id, p.parent_id, count(p2.id) children from sales_channel_domain d
join sales_channel sc on d.sales_channel_id = sc.id
join product_visibility pv on pv.sales_channel_id = sc.id
join product p on p.id = pv.product_id
left outer join product p2 on p.id = p2.parent_id
where sc.active = 1
group by d.url, p.id, p.parent_id
)x
WHERE x.parent_id is null and x.children < 1
group by x.url
union all
SELECT x.url, 'main' type, count(x.id)
FROM (
select d.url, p.id, p.parent_id, count(p2.id) children from sales_channel_domain d
join sales_channel sc on d.sales_channel_id = sc.id
join product_visibility pv on pv.sales_channel_id = sc.id
join product p on p.id = pv.product_id
left outer join product p2 on p.id = p2.parent_id
where sc.active = 1
group by d.url, p.id, p.parent_id
)x
WHERE x.parent_id is null and x.children > 0
group by x.url
union all
SELECT x.url, 'variant' type, count(x.id)
FROM (
select d.url, p2.id, p2.parent_id, 0 children from sales_channel_domain d
join sales_channel sc on d.sales_channel_id = sc.id
join product_visibility pv on pv.sales_channel_id = sc.id
join product p on p.id = pv.product_id
left outer join product p2 on p.id = p2.parent_id
where sc.active = 1
)x
group by x.url
Manchmal möchte man Artikel für 0.00 Euro anbieten. Zum Beispiel irgendwelche Muster-Artikel.
Wenn man nun einfach einen Preis von 0.00 Euro eingibt, gibt es einige seltsame Nebeneffekte. Der Warenkorb funktioniert auch nicht mehr und man findet im Log den Fehler (sBasket::getPriceForAddProduct):
BASKET-INSERT #01 No price acquired
Die Lösung ist hier https://forum.shopware.com/discussion/7376/erledigt-null-euro-artikel-verwenden beschrieben. Es muss also immer ein Preis > 0 gegeben sein, denn man über die Gruppe wieder auf 0 zurück bekommt.
B2B ist anders. B2C ist einfach. Bei B2C macht man Werbung, zeigt Preise und versucht einen möglichst offenen (Paypal-Express) und einfachen Checkout den Kunden zu präsentieren. Bei B2B kommt der Kunde teilweise nicht einfach in den Shop. Da ist der Shop eine Dienstleistung, die den Kunden bereit gestellt wird. Also wird man teilweise erst Kunde und kommt dann in den Shop. Preise sind oft auch so eine Verhandlungssache und nicht jedem Kunden werden sofort Preise präsentiert, weil diese ihn verschrecken könnten, da man mit Abnahmen in kleinen Mengen kalkuliert und Staffelpreise zu ungenau wären, weil hier keine Abnahmen pro Jahr oder so dargestellt werden können. Bloß weil ich 1000 Stück bestelle heißt es nicht, dass ich als B2B-Kunde nicht schon genau weiß, dass ich noch weitere 11000 im restlichen Jahr bestellen werde (aber ich will natürlich Lagerplatz sparen oder nicht Dinge aufwendig gekühlt lagern).
Mein erster Versuch etwas für den B2B-Bereich in Shopware zu entwickeln war mein Plugin zum Verhindern von Kundenregistrierungen. Das klingt nach weniger als es kann, weil es doch sehr fein granulär regelt was bei der Registrierung möglich sein soll:
- Keine Registrierung und nur ein Text mit Infos
- Keine Registrierung aber ein Formular für Anfragen
- Blockieren von bestimmten Email-Adressen
- nur bestimmte Email-Adressen erlauben (Mitarbeiter-Shops und so)
- Nur Firmen als Kunden
- Keine Firmen als Kunden
Damit lässt sich schon mal sehr klar definieren, wer und wie in meinen B2B-Shop darf. Muss ich den Account selbst anlegen für meinen Kunden (dann kann ich direkt festlegen was er sehen darf und was nicht) oder darf es selbst muss aber meine Einstellungen abwarten und darf erst dann kaufen? Das alles kann ich damit steuern.
Ich würde gerne stärker in die Richtung gehen und habe anfangen mir eine recht eigene aber doch nicht so einzigartige, wie Firmen oft glauben, Fantasy-Firma aus zu denken und damit einmal exemplarisch einen Weg zu einem fertigen B2B-Shop zu skizzieren. Die Firma vertreibt noch nicht über einen Shop, hat aber eine entsprechende IT mit ERP und WWS. Die Firma produziert selbst, oft auch nach Bedarf, bietet aber auch einige kleine OEM-Produkte zusätzlich zu ihren Produkten an. Diese OEM-Produkte sind meist kleines Zubehör und Verbrauchsmaterialien.
Als Shopware Edition kommt die CE zum Einsatz, weil die an sich ja alles kann und wenn mehr Support gewünscht ist, kann man ja immer noch nachrüsten. Die B2B-Suite lasse ich aus und gucke, ob man nicht die nötigsten Dinge auch einfacher und günstiger selber schreiben kann und wo hier die Grenzen sind (Workflows mit Freigaben und Hierarchien, wäre hier ein Punkt, wo man echt überlegen sollte, ob man da selbst was schreibt).
Die wichtigsten Punkte sind:
1) Produkt-Daten in den Shop bekommen
2) Bestellungen exportieren
3) Lagerbestände abgleichen
4) Kunden Registrierung
5) Preise und Rechnungen
6) Abrufbestellungen
7) Sets aus Produkten (in Hinsicht auf OEM-Zubehör, das ausgehen könnten)
Also fangen wir mal an die Punkte zu analysieren und einfache + schnelle Lösungen zu finden. Denn eine Time-To-Market sollte auch hier nicht länger als 4-6 Monate benötigen. Was das kostet... darüber kann man vielleicht ganz am Ende noch mal nachdenken. Aber so viel würde ich schon mal sagen: Das Team sollte aus einem Shopware-Entwickler, einen Entwickler aus dem ERP-/WWS-Bereich, jemanden für das Theme + allgemeines Design (Umsetzung kann ja über den Shopware-Entwickler laufen) und jemanden der Kunden-/Preis-/Produktdaten betreut.
1) Produkt-Daten in den Shop bekommen
Das ERP sollte an sich schon eine Schnittstelle mitbringen um Produkte zu exportieren, wenn nicht kann man die in 2-3 Tagen realisieren. Ob SAP oder was eigenes, es funktioniert alles ganz einfach und linear. Wird ein Produkt angelegt oder geändert und ist von den Daten her vollständig wird es als Datei oder per JMS ausgegeben (per Änderung-Flag oder direkt Live ist dabei sogar egal).
Wenn noch keine Schnittstelle existiert, wäre es gut, wenn diese direkt auf das Event reagiert und den Content per Push an den nächsten Service weiterreicht oder auch auch direkt im richtigen JON-Format an Shopware sendet.
Am einfachsten lässt sich so etwas über eine Template-Engine realisieren, wie sie für fast jede Umgebung existiert.
Ansonsten kann man sich eine eigene kleine Middlware bauen, die Daten per File-Watcher oder MDB empfang, die Daten auf Objekte mappt, die vom Interface her den Models von Shopware gleichen und dann per JSON-Serialisierung ausgeben und direkt an die Shopware-API sendet.
Nur eine Sache würde ich wirklich an der Shopware-API über ein Plugin ändern: Es sollte egal sein, ob man POST oder PUT verwendet, wenn eine Produktnummer dabei steht und useNumberAsId=true gesetzt ist, sollte die API selbst heraus bekommen.
Also das Plugin nimmt die Nummer und lädt die Id dazu nach. Existiert eine wird diese im Model, das gerade rein kommt, ergänzt und die Anfrage an die PUT-Action weitergeleitet. Existiert keine Id wird zur POST-Action weitergeleitet. Das ist dann genau so wie ein Merge bei einem ORM (Doctrine, JPA). Ich hab so ein Plugin schon mal geschrieben und es ist echt praktisch und beschleunigt die Datenübermittlung sehr, weil nicht erst durch ein GET bestimmt werden muss, ob die Software die Daten
als POST oder PUT senden muss.
Sollte als Schnittstelle OCI oder BMEcat zur Verfügung stehen, sollte man diese nutzen. Einen eigenen API-Controller für diese Formate zu schreiben, geht relativ schnell und unkompliziert. Teilweise kann soet was sogar besser sein, als die vorhandene Shopware-API. Wenn man Standard-Formate nutzen kann, sollte man es tun, dann wäre selbst ein Wechsel es ERP (z.B. von was eigenen auf SAP) mit übersichtlich viel Arbeit möglich und was am Shop ändern zu müssen.
2) Bestellungen exportieren
Genau so wichtig wie der Import und Abgleich der Artikeldaten ist der Rückweg, nämlich der Export von Bestellungen. Eine einfache API-Schnittstelle mit Filter auf den Bestell-Status zu schreiben geht schnell und einfach. Somit kann ein anderes System sich alle Bestellungen mit einem oder mehreren Status/Status aus dem Shop einfach abholen. Die simpelste Variante ist es per Scheduler-EJB oder Cron-Job laufen zu lassen.
Wenn man mehr in Richtung Echtzeit gehen möchte, kann man eine neue Bestellung natürlich auch vom Shop aus mit einem Push-Client an eine REST-API einer Middleware oder eines ERP senden.
Die primitivste Variante ist natürlich, die Bestellung als Datei abzulegen und per FileWatcher dann dort abholen zu lassen.
Wichtig bei dem Vorgang ist der Faktor Zeit. Denn ja schneller alles geht, desto weniger Gefahr besteht, das Bestellungen aus dem Shop und dem ERP einen Konflikt um Lagerbestände auslösen könnten.
Es fing alles mit einem kleinen Plugin zum dumpen von Bestellungen an, dass ich zum Debugen auf dem produktiven Server entworfen habe. Es entwickelte sich weiter zu einem vollwertigen order-Export Plugin und kann nun:
- Export als Shopware-XML, openTRANS 1.0, openTRANS 2.1
- per XSLT kann man auch jedes weiter XML-format erzeugen (OCI sollte so auch gehen.. sollte.. habe leider kein System zum Testen)
- Export als Datei direkt nach der Bestellung
- Export über CLI-Command und Cron-Job (in das Dateisystem)
- Automatisches Status-Update nach dem Export
- Export oder einen REST-API Controller (mit manuellen Status-Update)
- Automatischer Push an eine REST-API (RESTEasy im Wildfly wurde getestet)
Mit den ganzen verschiedenen Wegen und Formaten, ist eine Integration in vielen Fällen möglich. Man benötigt weiterhin einen Entwickler, da mit die Gegenseite korrekt eingebunden werden kann, aber wenn man die Kommunikation erst einmal laufen hat, läuft alles sehr stabil und zuverlässig (eine Version läuft seit Nov. 2017 und hat bis jetzt nie Probleme verursacht).
Bestellungen sind zum Glück immer sehr einfach strukturiert und sind seit Jahrzehnten auch fast unverändert geblieben.
Was das Plugin nicht zur Verfügung stellt sind allgemeine Updates beider Status und die Übermittlung von Trackingcodes. Hier ist aber die REST-API die Lösung des Problems. Da die Nummer der Bestellung beim Export mit übermittelt wurde, kann man anhand dieser die Bestellung über die REST-API laden und modifizieren.
Wenn man das fertige Plugin verwendet, ist der Aufwand eher gering und man kann sich eher um die Update-Aktionen, die vom ERP aus getriggert werden kümmern.
Momentan entwickelt es sich auch zu einem Dropshipping-fähigen Plugin, mit dem man Bestellungen, für sich oder für seinen Kunden, direkt an externe Lieferanten und Großhändler weiterreichen kann.
3) Lagerbestände abgleichen
Lagerbestände abzugleichen ist mit das komplexeste Thema bei der ganzen Anglegenheit. Während bei den Artikeln sich nur ein System in eine Richtung synchronisieren muss, geht es hier in beide Richtungen. Denn Bestellungen können sowohl über den Shop als auch das ERP ausgelöst werden und manchmal fällt im Lager auch einfach was runter.
Das schlimmste was passieren kann ist, dass man mehr verkauft als man hat. Im einfachsten Fall muss der Kunde länger warten bis man selbst nachbestellt oder nach produziert hat. Im schlimmsten Fall muss man dem Kunden sagen, dass er die bestellte Ware nicht bekommen kann.
Das eigentliche Problem bei den Lagerbeständen ist, dass alles asynchron und nebenläufig ist. Während im ERP eine Order gespeichert wird, wird auch der aktuelle Lagerbestand bestimmt und 2 Bestellungen aus dem Shop liegen in der Import-Queue (alles mit dem selben Artikel). Wenn der Shop jetzt einfach den da bestimmten Lagerbestand anzeigen würde, wäre bis zum nächsten Abgleich der Lagerbestand wieder um 2 höher, weil der schon durch die Bestellungen abgezogene Bestand bei der Neuberechnung nicht verbucht war, der Shop diese 2 Bestellungen aber ab sich schon kennt.
Hier muss mit Differenz-Buchungen und Timestamp gearbeitet werden. Eine sehr gute Lösung ist das führen einer Lagerbewegung-Chain, die durch Absolute-Lagerbestände in Blöcke auf geteilt wird. Man rechnet immer ab dem neusten Absolute-Eintrag aufwerts.
Beispiel:
-1 order 136 2018-01-01 12:15:00 (exportiert)
-1 order 135 2018-01-01 12:14:59 (exportiert)
-1 order 134 2018-01-01 12:02:00 (exportiert und importiert im ERP)
+8 absolute 2018-01-01 12:00:00
Bestand: 5 im Shop
Es ist gerade 12:15:10 und der Lagerbestand im ERP bestimmt und in den Shop importiert, die beiden Orders brauchen noch 15s bis deren Import beginnt (weil der Austausch über Dateien und Cronjobs läuft). Die letzte Bestandsänderung für den Artikel verzeichnet das ERP für 12:03:10, weil dort die Order von 12:02:00 im ERP verarbeitet wurde.
-1 order 136 2018-01-01 12:15:00 (exportiert)
-1 order 135 2018-01-01 12:14:59 (exportiert)
7 absolute 2018-01-01 12:03:10
-1 order 134 2018-01-01 12:02:00 (exportiert und importiert im ERP)
8 absolute 2018-01-01 12:00:00
Bestand: 5 im Shop
Das ist super! Der Bestand aus Shop-Sicht ist gleich geblieben, weil er in beiden Fällen alle Daten kannte. Es wird nie zuviel verkauft werden, obwohl das ERP noch gerade glaubt 7 auf Lager zu haben, weil es die beiden neusten Bestellungen noch nicht kennt.
Am Ende kann man so schnell wie möglich sein und es wird immer diesen kleinen Bereich geben, wo eie BEstellung gerade unterwegs ist, während der Bestand im ERP berechnet wird.
Wo man diese Differenzverrechnung macht ist nicht ganz so wichtig. Ob in einer Middleware oder im Shop direkt, macht kaum einen Unerschied. Es muss immer der verkaufte Bestand aus den Orders als Differenz verbucht werden und Bestandsmeldungen vom ERP entgegen genommen und gespeichert werden.
Das schöne an dem Prinzip ist auch, dass es sich super erweitern lässt, um zukünftige Bestande über Liefer-Avis und ähnliches. Darüber lassen sich dann wieder Lieferzeiten sehr viel genauer bestimmen, wenn man schon weiß, wann wieder etwas auf Lager sein wird und dann auch wie viel. Wenn ich 10 Stück bestelle und im Avis nur 5 Stück angekündigt sind,muss ich ein Lieferdatum nach der kommenden Lieferung verwenden. Normal bei Shopware hätte man nur die typische Lieferzeit des Suppliers, weiß aber nie ob dann wirklich was da ist und wenn doch wie viel Stück.
Die Implementierung so einer Tabelle und den passenden Queries ist relativ einfach. Events für Orders, um die Verkäufe zu vermerken, sind leicht gefunden. Ein API-Controller, um die Bestandmeldungen vom ERP/der Middleware zu empfangen ist auch nicht viel Arbeit.
Ich habe mal so ein Plugin angefangen und einen schon wirklich sehr vollständigen Prototypen hatt ich nach 3 Abenden.. also wohl so 4-5 Stunden. 1-2 Tage voll daran arbeiten und man bekommt so etwas ohne Probleme hin. Die Zeit zum Testen natürlich nicht mit gerechnet.