Module Ouvrages¶
Objectif¶
Le module Ouvrages permet de composer des elements reutilisables a partir de lignes de catalogue, de main d'oeuvre, de produits standards, de menuiserie ou d'autres ouvrages. Un ouvrage devient ensuite une ligne exploitable dans les devis, factures et avoirs.
Le point le plus important pour la reprise n'est pas seulement le CRUD : le module transporte un payload complet dans les documents commerciaux avec le prefixe __ouvrage_payload__:. Ce contrat evite qu'un ouvrage insere dans un devis perde sa composition quand il est transforme, facture ou rendu.
Carte technique¶
flowchart TD
Router["app/core/Router.php"] --> Controller["OuvrageController"]
Controller --> Model["OuvrageModel"]
Controller --> Taxonomy["CatalogueTaxonomyModel"]
Model --> Sql["app/bd/ouvrages.sql"]
Model --> Tables["ouvrages / ouvrage_lines"]
Selector["choisir-produit-pour-devis.php"] --> Document["Devis / Facture / Avoir"]
Document --> BaseDocument["BaseDocumentModel"]
BaseDocument --> Payload["__ouvrage_payload__"]
Payload --> Pdf["InternalPennylanePdfController"]
Payload --> Facture["FactureModel"]
Fichiers a connaitre¶
| Role | Fichiers |
|---|---|
| Routage | app/core/Router.php |
| Controleur | app/controllers/OuvrageController.php |
| Modele | app/models/OuvrageModel.php |
| Taxonomies | app/models/CatalogueTaxonomyModel.php, app/bd/catalogue-taxonomies.sql |
| Vue CRUD | app/views/ouvrages.php |
| Selecteur document | app/views/partials/modals/choisir-produit-pour-devis.php |
| Lignes devis | app/views/partials/tables/devis-templates.php, app/models/DevisModel.php |
| Lignes facture/avoir | app/models/FactureModel.php, app/views/edit_avoir.php |
| Persistance payload | app/models/BaseDocumentModel.php |
| PDF interne Pennylane | app/controllers/InternalPennylanePdfController.php |
| SQL | app/bd/ouvrages.sql, app/bd/ouvrages-tpe-feature.sql |
Routes¶
Toutes les routes passent par :
Auth::check();requireFeatureAccess(FEATURE_EDITER_CATALOGUE, APP_DIR . '/dashboard');requireFeatureAccess(FEATURE_GERER_OUVRAGE, APP_DIR . '/dashboard').
| Route | Methode | Controleur | Usage |
|---|---|---|---|
ouvrages |
GET | index() |
Page de gestion. |
ouvrages/search |
GET | search() |
Recherche paginee AJAX. |
ouvrages/get |
GET | get() |
Charge un ouvrage avec ses lignes. |
ouvrages/save |
POST | save() |
Cree ou modifie un ouvrage. |
ouvrages/delete |
POST | delete() |
Archive logiquement un ouvrage. |
Droits et plans¶
OuvrageModel::checkFeatureAccess() refait les controles de features :
FEATURE_EDITER_CATALOGUE;FEATURE_GERER_OUVRAGE.
Si FEATURE_GERER_OUVRAGE manque, le message indique que les ouvrages sont reserves aux societes ayant au moins le plan TPE.
Permissions catalogue :
- liste :
PermissionMiddleware::require('catalogue', 'view_all'); - creation :
catalogue/create; - edition :
catalogue/edit; - suppression :
catalogue/delete_all.
Schema de donnees¶
ouvrages¶
Table principale des ouvrages.
Champs structurants :
societe_id;created_by;titre;description_devis;unite;taxonomy_family_id,taxonomy_category_id;cout_achat_ht;prix_vente_ht;marge_ht;marge_percent;active;- timestamps.
Indexes utiles :
(societe_id, active);created_by;(societe_id, taxonomy_family_id, taxonomy_category_id, active).
ouvrage_lines¶
Composition detaillee d'un ouvrage.
Champs structurants :
ouvrage_id;line_type:materialoulabor;source_type:catalogue,main_oeuvre,standard,carpentry,ouvrage;source_id;label_snapshot;description_snapshot;quantity;unit;purchase_unit_ht;sale_unit_ht;tva;config_json;sort_order.
config_json permet de garder une configuration locale de composant sans dependre uniquement de la source catalogue.
Extensions documents¶
ouvrages.sql modifie les enums :
devis_lignes.sourceaccepteouvrage;factures_lignes.sourceaccepteouvrage.
Cette extension est indispensable : une ligne ouvrage peut ne pas avoir de produit_id catalogue direct.
Creation et edition¶
OuvrageController::save() appelle OuvrageModel::save($_POST).
Regles verifiees :
- titre obligatoire ;
- au moins une ligne obligatoire ;
unitepar defaut :Pce;- taxonomie normalisee via
CatalogueTaxonomyModel::normalizePair('ouvrage', ...); - sauvegarde transactionnelle ;
- en edition, suppression puis reinsertion de toutes les lignes ;
- suppression logique :
active = 0.
Normalisation des lignes :
- si
linesarrive comme chaine JSON, elle est decodee ; - les lignes sans libelle sont ignorees ;
line_typeinconnu devientmaterial;line_type = laborforcesource_type = main_oeuvre;- sources autorisees :
catalogue,main_oeuvre,standard,carpentry,ouvrage; - quantite minimale :
0.01; - prix achat/vente minimum :
0; - TVA minimum :
0; config_jsoninvalide devientnull.
Totaux calcules :
cout_achat_ht = somme(quantity * purchase_unit_ht);prix_vente_ht = somme(quantity * sale_unit_ht);marge_ht = vente - achat;marge_percent = marge / achat * 100si achat > 0, sinon0.
Lecture et recherche¶
getPaginatedOuvrages() :
- filtre toujours sur
societe_id = active_societe_id; - filtre toujours
active = 1; - limite
itemsPerPageentre 1 et 100 ; - recherche sur
titreetdescription_devis; - filtre optionnel par famille/categorie ;
- joint les taxonomies
scope = ouvrage; - joint le createur et compte les lignes
material/labor.
getById() :
- refuse les ouvrages d'une autre societe ;
- charge les lignes triees par
sort_order, puisid; - decode
config_jsonenconfig; - hydrate certaines images depuis les sources catalogue ;
- calcule une TVA representative avec
resolveOuvrageTva(); - construit
document_description.
resolveOuvrageTva() prend le taux TVA qui represente le plus gros total de vente HT parmi les lignes. Si aucune ligne taxable n'a de montant, le taux renvoye est 0.
Description documentaire¶
buildDocumentDescription() concatene :
description_devis;Matériaux : ...avec les lignesmaterial;Main d’œuvre : ...avec les ligneslabor.
Chaque ligne est rendue sous la forme :
Cette description est le resume humain visible dans les documents. Le payload complet reste stocke separement via __ouvrage_payload__.
Contrat payload document¶
Le contrat est dans BaseDocumentModel::saveLinePayload().
Quand la source est ouvrage et que le payload contient catalogue_kind = ouvrage ou lines, le champ package_description du payload document contient :
La lecture inverse est dans BaseDocumentModel::getLinePayloadFromTable() :
- si
source = ouvrage; - et si
package_descriptioncommence par__ouvrage_payload__:; - alors le JSON est decode ;
idetsource = ouvragesont rajoutes ;- le payload complet est retourne au lieu du payload menuiserie standard.
Ce choix evite de perdre :
- les lignes internes de l'ouvrage ;
- les quantites/prix snapshots ;
- les taux TVA ;
- les configurations locales ;
- les informations necessaires pour rouvrir le configurateur.
Devis, factures et avoirs¶
Devis vers facture¶
DevisModel::buildProgressInvoiceLines() detecte une ligne ouvrage si :
source === 'ouvrage';- ou
sim_payload.catalogue_kind === 'ouvrage'; - ou
sim_payload.source_tab === 'ouvrage'.
Une ligne sans produit_id n'est traitee comme separateur que si ce n'est pas une ligne ouvrage. Cette regle est critique : les ouvrages peuvent exister sans produit catalogue direct.
Facture¶
FactureModel accepte explicitement le cas :
BaseDocumentModel::hydrateFactureLinesFromDevis() recharge le payload devis si la ligne facture ouvrage n'a pas encore sim_payload.lines.
Avoir¶
app/views/edit_avoir.php considere une ligne document valide si elle a un produit_id ou si source === 'ouvrage'.
PDF interne Pennylane¶
InternalPennylanePdfController::normalizePayload() reconnait aussi __ouvrage_payload__: pour reconstruire le payload ouvrage avant rendu.
Selecteur et configurateur¶
app/views/partials/modals/choisir-produit-pour-devis.php integre l'onglet ouvrage dans le selecteur de produits. Le JS partage les tabs :
catalogue;main_oeuvre;ouvrage;standard;carpentry.
app/views/partials/tables/devis-templates.php contient la regle de reouverture :
Donc modifier une ligne ouvrage dans un devis doit rouvrir le configurateur avec le payload sauvegarde, pas avec une reconstruction approximative depuis le catalogue.
Taxonomies¶
Les ouvrages utilisent les taxonomies catalogue avec scope = ouvrage.
Tables impliquees :
catalogue_taxonomy_families;catalogue_taxonomy_categories.
app/bd/catalogue-taxonomies.sql ajoute aussi l'index idx_ouvrages_taxonomy sur ouvrages.
Particularite schema runtime¶
OuvrageModel::__construct() appelle ensureSchema(), qui lit app/bd/ouvrages.sql et execute les instructions separees par ;.
Impact :
- la premiere instanciation du modele peut tenter de creer/alterer des tables ;
- l'utilisateur SQL applicatif doit avoir les droits necessaires ;
- ce comportement peut masquer une migration non appliquee en local, mais echouer en production si les droits sont plus stricts ;
- toute modification du SQL doit rester idempotente.
Tests existants¶
Tests presents dans le depot inspecte :
tests/ouvrage_configurator_inline_edit_test.php: verifie que le configurateur permet l'edition locale des composants ;tests/ouvrage_document_payload_persistence_test.php: verifie le contrat__ouvrage_payload__, la restauration du payload et la conservation dans devis/factures ;tests/ouvrages_selector_tva_dependency_test.php: verifie que la vue chargeTVA_RATESavant le selecteur partage.
Commandes utiles :
php tests/ouvrage_configurator_inline_edit_test.php
php tests/ouvrage_document_payload_persistence_test.php
php tests/ouvrages_selector_tva_dependency_test.php
Points de vigilance¶
- Ne pas remplacer
__ouvrage_payload__:par une colonne arbitraire sans migrer la lecture existante des devis, factures, avoirs et PDF. - Ne pas supposer qu'une ligne ouvrage a un
produit_id. Plusieurs chemins verifient explicitement ce cas. - Le modele supprime puis reinsere toutes les lignes a l'edition. Si une future fonctionnalite reference
ouvrage_lines.id, elle sera fragile. - Le taux TVA affiche pour l'ouvrage est representatif, pas une preuve que toutes les lignes ont le meme taux.
- Les prix sont des snapshots. Modifier un produit source apres insertion ne doit pas modifier automatiquement les documents existants.
ensureSchema()execute du SQL depuis le modele. En environnement verrouille, preferer une migration controlee avant de charger la page.- Les ouvrages peuvent contenir d'autres ouvrages (
source_type = ouvrage). Il faut eviter les boucles fonctionnelles si une future UI autorise la selection recursive.
Verification conseillee¶
- Creer un ouvrage avec au moins une ligne materiau et une ligne main d'oeuvre.
- Verifier
ouvrages: totaux achat, vente, marge. - Verifier
ouvrage_lines:line_type,source_type,tva,config_json. - Inserer l'ouvrage dans un devis depuis le selecteur.
- Verifier que
devis_ligne_payload.package_descriptioncontient__ouvrage_payload__:. - Modifier la ligne dans le devis : le configurateur doit se rouvrir avec les composants.
- Transformer le devis en facture et verifier que
sim_payload.linesest encore present. - Generer le PDF interne et verifier que la ligne ouvrage est rendue avec ses composants attendus.
- Lancer les trois tests PHP listes ci-dessus.
Questions ouvertes¶
- Aucun test PDF dedie aux composants ouvrage n'a ete trouve dans les fichiers inspectes.
- Le modele accepte
source_type = ouvrage, mais la strategie produit pour empecher les compositions recursives n'est pas explicite dans le code inspecte. - Le SQL est execute au runtime par le modele ; a moyen terme, le comportement serait plus previsible si toutes les migrations etaient appliquees hors requete utilisateur.