Il est temps d'évoquer en public un projet qui me tient à cœur et qui occupe presque tout mon temps de travail depuis le début de cette année 2018 : Anyblok / WMS Base, une bibliothèque pour écrire des applications de logistique en Python avec PostgreSQL, dans le cadre d'Anyblok.
C'est un projet déjà bien avancé, bien que pour l'instant encore expérimental. Il reprend des idées que je rumine depuis des années.
Nous avons un primo utilisateur avec un certain nombre de besoins précis, mais la volonté a été dès le départ de chercher à se baser sur une couche générique, libre et publique. La vocation d'Anyblok / WMS Base est bien de s'adresser à tous les cas de logistique et gestion de stocks.
Au total, c'est un sacré pari, et j'espère que ce composant libre trouvera son public d'utilisateurs – des développeurs, donc – bien au-delà d'Anybox.
Présentation
Comme son nom le reflète, Anyblok / WMS Base est une couche intermédiaire – au-dessus d'Anyblok, donc – sur laquelle se baser pour développer des applications de logistique concrètes (WMS pour Warehouse Management System).
Il est fort possible – en tout cas je le souhaite – que nous développions par la suite des surcouches supplémentaires pour traiter des cas d'application courants, mais c'est prématuré à ce stade.
Il n'y a pas encore d'exposition HTTP ou autre, et encore moins d'interface utilisateur, mais ce sera de toute façon à part. Comme c'est une bibliothèque, les applications utilisant Anyblok / WMS Base peuvent aussi bien être des micro-services que des vastes ERP (basés sur Anyblok), ou même être embarquées.
Le code est en Python 3, pur jusqu'à présent, et j'ai choisi sans états d'âme de ne cibler comme SGBD que PostgreSQL, pour pouvoir profiter de ses fonctionnalités spécifiques (JSON, index GiST…)
Pour reprendre juste les titres de la page de documentation des objectifs, les maîtres mots d'Anyblok / WMS Base sont :
- généricité et minimalisme (tightness)
- traçabilité
- flexibilité
- prise en compte du fait que la réalité matérielle prime sur sa représentation
- performance et passage à l'échelle
- qualité
Les objectifs de traçabilité, flexibilité et de réalisme obligent à une représentation assez détaillée et à conserver un historique complet (voir ci-dessous pour un survol des concepts principaux). Le défi est d'arriver à les concilier avec les autres, tout en étant agréable pour le développement en aval.
J'ai conscience que c'est très ambitieux, voire gonflé, mais dans le même temps bon espoir que le fait de se limiter à un cœur de fonctionnalité générique permette justement de ne pas rougir en relisant cette page dans quelques années.
Je travaille en ce moment même sur un exemple simplifié (non interactif) d'application qui servira de banc d'essai pour évaluer les performances.
Aspects communautaires
Le source est libre (licence MPL 2.0, comme Anyblok) et il le restera, ne serait-ce que parce que, personnellement, jamais je ne signerai de CLA qui permette de relicencier sous licences propriétaires.
Il est disponible publiquement sur GitHub.
La documentation en anglais est déjà bien fournie. Il y a une feuille de route indicative.
Le but est bien de chercher à créer à terme une petite communauté autour de cette brique fondamentale. Il n'y a pas encore de guide du contributeur, ni d'exemples publics, mais il y en aura vite si des gens manifestent leur intérêt. Pour le moment, je suis preneur de remarques et commentaires ; vous pouvez me contacter de préférence via Mastodon (voir le pied de page).
Si vous soumettez une Pull Request et que je ne vous réponds pas, ne le prenez pas mal, c'est sans doute que je ne travaille pas cette semaine-là (n'hésitez pas à relancer après deux semaines). De toute manière, c'est une bonne idée d'en discuter d'abord, par exemple en soumettant votre problématique dans la page des améliorations.
Concepts centraux
Note
ceci est un résumé de la page Core Concepts de la documentation anglophone.
Marchandises, alias Biens (Goods)
Le modèle Wms.Goods sert à représenter de manière unique les objets physiques, avec un type et des propriétés flexibles; les avatars représentent quant à eux le passage dans le temps et l'espace de ces objets physiques.
Le type encode des informations de comportement (behaviours), et n'est pas à confondre avec une notion de produit que l'on trouverait dans un catalogue de vente – ce qui n'empêche pas de le lier à une telle notion s'il y en a une. Par exemple, une caisse de six bouteilles ne serait pas du même type qu'une bouteille individuelle. Ce sont dans les comportements du type « caisse » que l'on indique qu'en ouvrir une (opération Unpack) donne six bouteilles individuelles.
À partir de la version 0.7, le champ quantity du modèle Wms.Goods sera déporté dans un module (Blok) optionnel. Je suis arrivé en effet à la conclusion que c'était en pratique une complication inutile, sauf pour les cas de marchandises en vrac (au poids, à la longueur). Pour les niveaux de stocks, on comptera donc simplement les lignes dans la version par défaut.
Opérations
Note
Pour plus de détails, voir la page de documentation dédiée.
La manipulation des marchandises dans le système s'effectue normalement exclusivement en créant et exécutant des opérations (Wms.Operation) : Move, Arrival, Unpack, etc. La liste des opérations possibles n'est pas figée, et d'ailleurs le code applicatif peut très bien en définir de nouvelles, en implémentant une sous-API interne.
Certaines opérations vont travailler uniquement sur les avatars – c'est par exemple le cas de Move – alors que d'autres, comme par exemple Unpack, vont également manipuler la table des Goods.
Les opérations forment historique complet, qui est un graphe orienté acyclique (DAG), et sont systématiquement annulables (*cancel*), conditionnellement réversibles (*revert*) et totalement oubliables (*obliviate*).
Sans aller sans doute jusqu'à me baser sur Git ou Mercurial (obsolescence de changesets), je prévois des fonctionnalités de réécriture d'historique ou de prévisionnel intéressantes.
Emplacements
C'est à la fois le plus simple et le moins abouti actuellement des concepts de base : les emplacements (Location) représentent où l'on met les marchandises ; dans le cadre d'Anyblok / WMS Base, ce sont les avatars qui vont pointer dessus.
Je ne suis pas satisfait de cette terminologie de « Location », mais n'ai pour l'instant pas de bien meilleure idée.
Les emplacements forment une arborescence, comme des poupées russes, mais je me suis rendu compte il y a peu que c'était une erreur de baser les niveaux de stock dessus.
Composants additionnels
En plus des concepts centraux expliqués ci-dessus, qui sont définis par le Blok wms_core, il est prévu qu'Anyblok / WMS Base comprenne des Bloks optionnels, dont la liste est ici.
Il y en a pour le moment un seul: wms_reservation, qui permet de réserver des marchandises (Goods), non pas pour une opération, mais pour un but plus général – par exemple livrer un client – qui se traduira en une ou plusieurs opérations.
J'attends de ce mécanisme qu'il permette en particulier de réduire la contention en base de données à son strict minimum. L'articulation de tout cela est expliquée dans la section Architecture.
Motivation et historique
Dans le cadre de mon travail pour Anybox, j'ai été confronté plus d'une fois, comme mes collègues, à des applications dont la logistique était un aspect important, voire même le seul. Ne nous mentons pas : celles dans lesquelles j'ai mis les mains, que ce soit pour en corriger le comportement fonctionnel ou en améliorer les performances, étaient toutes des Odoo. Pour tout dire, j'ai commencé en 2011 par une instance d'OpenERP 5 qui ne faisait que de la logistique avancée, mais mes collègues et moi avons également été exposés à d'autres manières de voir.
Au fil du temps cela dit, je me suis fait une petite idée de ce que j'aurais voulu voir différemment fait, mais je n'avais pas eu jusqu'ici l'opportunité de me lancer.
Au final, Anyblok / WMS Base est très différent d'Odoo : si au départ je voulais ne fournir que l'équivalent des concepts de move, quants et location, les premiers sont rapidement devenus un cas particulier d'opérations, les seconds se sont retrouvés spécialisés en marchandises, propriétés et avatars et ne porteront bientôt plus de quantités par défaut…
À ce stade, la comparaison avec Odoo n'a plus grand sens, et en tirant les conclusions logiques de l'historique des opérations, j'arrive à des problématiques qui lui seraient sans doute bien étrangères.
Anyblok / WMS Base a été présenté pour la première fois à toute l'équipe Anybox lors du colloque de mars 2018, avec une réception très favorable ; les discussions qui ont suivi m'ont apporté encore d'autres cas d'utilisation auxquels réfléchir.
Rappel sur Anyblok
Note
voir aussi la documentation.
Anyblok est une surcouche de SQLAlchemy qui apporte quelques facilités, mais surtout la possibilité de surcharger les objets métier (appelés modèles) de manière dynamique, d'une manière similaire à celle d'Odoo pour ceux qui connaissent, mais, contrairement à ce dernier, sans embarquer pour cela sa propre couche ORM : la réputation de SQLAlchemy n'est plus à faire dans ce domaine, et il a subi plus d'une fois l'épreuve du feu.
J'avais eu le privilège de présenter Anyblok à PyConFr 2015, en présence de l'auteur, Jean-Sébastien Suzanne.
Pourquoi de la surcharge dynamique ?
Pour comprendre l'intérêt de ce genre de système par rapport à la simple utilisation de l'orientation objet de Python, il faut voir que lorsque l'on développe sur la base d'un ORM ou une couche de base similaire, ce n'est majoritairement pas l'applicatif qui instancie les objets, mais bien la sous-couche. Partant de là, se posent deux problèmes pour les applications qui souhaitent surcharger le comportement d'une couche intermédiaire comme Anyblok / WMS Base en sous-classant.
- comment signaler à la couche ORM d'utiliser la classe fille ?
- comment la classe fille doit-elle référencer la classe mère sans bloquer la possibilité de surcharges ultérieures ou parallèles ?
À cela on peut ajouter également qu'il peut être souhaitable que les modules utilisés (avec donc leurs surcharges), puissent dépendre de configuration en base de données, afin par exemple d'héberger une seule instance pour plusieurs clients différents.
Anyblok fournit tout cela en construisant dynamiquement les classes de modèle et en les stockant dans un registre qui dépend de la connexion au système de gestion de base de données (SGBD) et donc en particulier de la base en service. Ce principe de fonctionnement est similaire à celui d'Odoo (anciennement appelé OpenERP) que j'avais expliqué en anglais sur le site d'Anybox, avec un graphe d'héritage plus à plat.
Les dangers de la surcharge
Ce genre de surcharge dynamique est très utile, car il permet de ne jamais être bloqué ; c'est crucial dans un métier où les développements et correctifs en urgence sont inévitables, mais il faut prendre garde à ne pas en abuser, car le chaos n'est pas loin. Les règles de développement habituelles sont tout aussi valables que d'habitude : découper les APIs internes pour permettre de surcharger sans trop dupliquer, refondre le code pour simplifier les dépendances, incorporer en amont les correctifs qui doivent l'être, etc.
Tout cela n'est finalement pas différent de l'hygiène de base à laquelle tout développeur Python doit s'astreindre : la souplesse et la confiance envers le développeur sont de grands atouts au cœur de la philosophie du langage, ce n'est pas une raison pour faire du monkey patching dans tous les sens et des applications qui ne tiennent qu'à coups de bouts de ficelle…
Anyblok / WMS Base et la surcharge
Pour revenir au cas particulier d'Anyblok / WMS Base, dans l'ensemble, l'idée est de fournir une couche intermédiaire qui ne nécessite pas d'être trop surchargée, et donc qui incorpore le moins possible d'aspects métier.
Mais il y a des endroits, comme par exemple la définition des propriétés des marchandises (Goods), où la surcharge est ponctuellement encouragée, ce qui permet justement de ne pas trop faire de suppositions sur le cas d'utilisation concret.