Fee check & rules

This commit is contained in:
2026-05-14 19:32:17 +02:00
parent 160ec1544b
commit 4395884754
10 changed files with 1496 additions and 68 deletions

View File

@@ -9,7 +9,7 @@ Statut: `migration partielle`
- Matching, Create Contracts, back-to-back: [matching.md](matching.md)
- Shipments, controllers, SLA, weight reports: [shipments-execution.md](shipments-execution.md)
- Pricing manuel, basis, premium, linked currency: [pricing.md](pricing.md)
- Fees, freight, lots effectifs, `% rate`: [fees.md](fees.md)
- Fees, freight, lots effectifs, quantités, `% rate`: [FR](fees.md) / [EN](fees.en.md)
- Valuation, PnL, MTM, derivatives: [FR](valuation-pnl-mtm.md) / [EN](valuation-pnl-mtm.en.md)
- Factures provisoires/finales, padding: [invoicing.md](invoicing.md)
- Impacts `account.move`, validate/post: [accounting-bridge.md](accounting-bridge.md)
@@ -37,7 +37,8 @@ Statut: `migration partielle`
- `BR-PT-PRI-003`: pricing manuel.
- `BR-PT-FEE-001`: maritime freight depuis fee shipment.
- `BR-PT-FEE-002`: lots effectifs des fees.
- `BR-PT-FEE-003`: `% rate` via delta de financement.
- `BR-PT-FEE-003`: quantité du fee alignée sur les lots effectifs.
- `BR-PT-FEE-004`: `% rate` via delta de financement.
- `BR-PT-VAL-001`: valuation achat/vente et sale-first.
- `BR-PT-VAL-002`: references de valuation.
- `BR-PT-VAL-003`: MTM hors fees.

View File

@@ -0,0 +1,185 @@
# Fees
Language: `en`
Mirror page: [fees.md](fees.md)
Status: `partial migration`
See also the historical technical page: `../fees.md`.
> **Operational summary**
> A fee carries an amount, but its quantity must remain aligned with the lots
> it applies to. `Per packing` is the only exception: it follows packing units,
> not lot weight.
| Marker | Topic | Short rule |
| --- | --- | --- |
| Quantity | `fee.quantity` | Sum of linked lots in `fee.lots`. |
| State | empty `fee.qt_state` | Uses the contract `weight basis`. |
| State | filled `fee.qt_state` | Uses that state, capped by the `weight basis`. |
| Fallback | state missing on lot | Uses the closest previous state by `sequence`. |
| Net / gross | `fee.weight_type` | `net` reads `quantity`, `brut` reads `gross_quantity`. |
| Packing | `ppack` | Packing quantity, decoupled from weight. |
| Control | Python + SQL | Application guard and SQL diagnostic. |
## Consultant Rules
### BR-PT-FEE-001 - Freight value from shipment fee
Source: `BR-PT-003`
#### Consultant Rule
The freight value printed on invoice documents comes from the maritime freight
fee on the shipment, not from a direct invoice field.
#### Developer Notes
- Find the physical lot from the invoice.
- Find its `shipment_in`.
- Search the `fee.fee` with `product.name = 'Maritime freight'`.
- Use `fee.get_amount()`.
### BR-PT-FEE-002 - Effective lots for fees
Source: `BR-PT-021`
#### Consultant Rule
A fee follows the virtual lot until a physical lot is linked. As soon as a
physical lot is linked, physical lots become the calculation basis for the fee.
#### Developer Notes
- Do not remove the virtual lot link: it remains the fallback.
- Effective lots are:
- physical lots if at least one physical lot is linked;
- otherwise virtual lots.
- The same selection applies to fee PnL.
- Synchronization points:
- fee creation;
- `fee.lots` link;
- `quantity_theorical` change;
- weighing;
- physical lot removal.
### BR-PT-FEE-003 - Fee quantity
#### Consultant Rule
The quantity of a fee follows the quantity life cycle of its linked lots. It
must represent the sum of those lots in the authorized contractual quantity
state.
#### Short Rule
```text
fee.quantity = sum(applicable quantity of effective fee.lots)
```
#### Quantity State Selection
| Case | Target state |
| --- | --- |
| empty `fee.qt_state` | `weight basis` of the fee carrier contract |
| filled `fee.qt_state` | `fee.qt_state`, unless later than the `weight basis` |
| `fee.qt_state` later than `weight basis` | `weight basis` |
| target state missing on lot | closest previous state by `lot.qt.type.sequence` |
| no target state available | current lot weight, only when no `weight basis` applies |
#### Weight Basis Source
| Fee | `weight basis` |
| --- | --- |
| Purchase fee | `fee.line.purchase.wb.qt_type` |
| Sale fee | `fee.sale_line.sale.wb.qt_type` |
| Shipment fee | purchase weight basis of the lot when present |
| Shipment fee without purchase | sale weight basis of the lot when present |
#### Net / Gross
- If `fee.weight_type = net`:
- read `lot.qt.hist.quantity`.
- If `fee.weight_type = brut`:
- read `lot.qt.hist.gross_quantity`.
#### Per Packing
- `mode = ppack` is excluded from the weight rule.
- `fee.quantity` represents a packing quantity.
- This quantity may be decoupled from net or gross weight.
#### Lump Sum
- `mode = lumpsum` also follows the quantity rule.
- The amount remains fixed.
- The quantity gives a more accurate per-ton price.
### BR-PT-FEE-004 - `% rate` fees from financing delta
Source: historical `BR-PT-016` and `2026-04-30` notes
#### Consultant Rule
Percentage financial fees are calculated with the financing delta from the
`BL date` estimated line, not with the current date.
#### Developer Notes
- Formula: `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`.
- Delta source: `Estimated date` line with `trigger = bldate`.
- If no `bldate` line exists, do not calculate a `% rate` amount.
## Developer Section
### Key Fields
- Fee: `fee.fee`
- Fee lots: `fee.lots`
- Fee quantity: `fee.fee.quantity`
- Mode: `fee.fee.mode`
- Optional quantity state: `fee.fee.qt_state`
- Net / gross: `fee.fee.weight_type`
- Lot quantity state: `lot.qt.hist.quantity_type`
- Net quantity: `lot.qt.hist.quantity`
- Gross quantity: `lot.qt.hist.gross_quantity`
- State order: `lot.qt.type.sequence`
- Purchase / sale weight basis: `purchase.weight.basis.qt_type`
### Calculation Functions
- `Fee._get_effective_fee_lots()`:
- selects physical lots, then virtual lots.
- `Fee._target_qt_type_for_lot()`:
- chooses `fee.qt_state` or the `weight basis`;
- caps the state at the `weight basis`.
- `Fee._select_lot_qt_type()`:
- takes the exact state when it exists;
- otherwise takes the closest previous state by `sequence`.
- `Fee._get_lot_fee_quantity()`:
- reads net or gross depending on `weight_type`.
- `Fee.sync_quantity_from_lots()`:
- resynchronizes `fee.quantity`.
### Python Guards
- Central check:
- `fee.fee.assert_quantity_consistency()`
- `fee.fee.assert_quantities_consistency()`
- Trigger points:
- fee creation;
- fee modification;
- `fee.lots` creation / modification / deletion;
- weighing through linked fee resynchronization.
- Exception:
- `mode = ppack` is not controlled as weight.
### SQL Diagnostic
- Script:
- [sql/fee_quantity_consistency_checks.sql](sql/fee_quantity_consistency_checks.sql)
- Usage:
- audit test databases;
- audit historical data;
- qualify data before correction.
- The script returns non-`ppack` fees where `fee.quantity` does not match the
sum of effective lots according to the applicable quantity state.

View File

@@ -1,55 +1,185 @@
# Fees
Statut: `migration partielle`
Langue : `fr`
Page miroir : [fees.en.md](fees.en.md)
Statut : `migration partielle`
Voir aussi la page technique historique: `../fees.md`.
Voir aussi la page technique historique : `../fees.md`.
## BR-PT-FEE-001 - Freight value depuis fee shipment
> **Résumé opérationnel**
> Un fee porte un montant, mais sa quantité doit rester alignée avec les lots
> qui le composent. Le mode `Per packing` est la seule exception : il suit une
> quantité de colisage, pas le poids du lot.
Source: `BR-PT-003`
| Repère | Sujet | Règle courte |
| --- | --- | --- |
| Quantité | `fee.quantity` | Somme des lots liés dans `fee.lots`. |
| État | `fee.qt_state` vide | Utilise le `weight basis` du contrat. |
| État | `fee.qt_state` renseigné | Utilise cet état, plafonné au `weight basis`. |
| Fallback | état absent sur le lot | Prend l'état antérieur le plus proche par `sequence`. |
| Net / brut | `fee.weight_type` | `net` lit `quantity`, `brut` lit `gross_quantity`. |
| Packing | `ppack` | Quantité de packing, décorrélée du poids. |
| Contrôle | Python + SQL | Check applicatif et diagnostic SQL. |
### Regle consultant
## Règles consultant
La valeur de fret affichee sur les documents facture vient du fee maritime du
### BR-PT-FEE-001 - Freight value depuis fee shipment
Source : `BR-PT-003`
#### Règle consultant
La valeur de fret affichée sur les documents facture vient du fee maritime du
shipment, pas d'un champ direct de la facture.
### Notes developpeur
#### Notes développeur
- Retrouver le lot physique depuis la facture.
- Retrouver son `shipment_in`.
- Chercher le `fee.fee` avec `product.name = 'Maritime freight'`.
- Utiliser `fee.get_amount()`.
## BR-PT-FEE-002 - Les fees lies aux lots privilegient les physiques
### BR-PT-FEE-002 - Lots effectifs des fees
Source: `BR-PT-021`
Source : `BR-PT-021`
### Regle consultant
#### Règle consultant
Un fee suit le lot virtuel tant qu'aucun lot physique n'est lie. Des qu'un lot
physique est lie, les lots physiques deviennent la base de calcul du fee.
Un fee suit le lot virtuel tant qu'aucun lot physique n'est lié. Dès qu'un lot
physique est lié, les lots physiques deviennent la base de calcul du fee.
### Notes developpeur
#### Notes développeur
- Ne pas supprimer le lien virtuel: il reste le fallback.
- Quantite `ppack`: somme de `lot.lot_qt` des physiques.
- Modes quantitatifs: quantites courantes converties des physiques.
- La meme selection s'applique au PnL fee.
- Points de synchronisation: creation fee, lien `fee.lots`, changement de
`quantity_theorical`, weighing, suppression de physique.
- Ne pas supprimer le lien virtuel : il reste le fallback.
- Les lots effectifs sont :
- les physiques si au moins un physique est lié ;
- sinon les virtuels.
- La même sélection s'applique au PnL fee.
- Points de synchronisation :
- création fee ;
- lien `fee.lots` ;
- changement de `quantity_theorical` ;
- weighing ;
- suppression de physique.
## BR-PT-FEE-003 - Fees `% rate` via delta de financement
### BR-PT-FEE-003 - Quantité du fee
Source: `BR-PT-016` historique et notes `2026-04-30`
#### Règle consultant
### Regle consultant
La quantité d'un fee suit la vie de la quantité des lots qui lui sont liés. Elle
doit représenter la somme des quantités de ces lots dans l'état contractuel
autorisé.
#### Règle courte
```text
fee.quantity = somme(quantité applicable des lots effectifs fee.lots)
```
#### Sélection de l'état de quantité
| Cas | État cible |
| --- | --- |
| `fee.qt_state` vide | `weight basis` du contrat porteur du fee |
| `fee.qt_state` renseigné | `fee.qt_state`, sauf s'il est postérieur au `weight basis` |
| `fee.qt_state` postérieur au `weight basis` | `weight basis` |
| état cible absent du lot | état antérieur le plus proche par `lot.qt.type.sequence` |
| aucun état cible disponible | poids courant du lot, seulement si aucun `weight basis` ne s'applique |
#### Source du `weight basis`
| Fee | `weight basis` |
| --- | --- |
| Fee achat | `fee.line.purchase.wb.qt_type` |
| Fee vente | `fee.sale_line.sale.wb.qt_type` |
| Fee shipment | weight basis achat du lot si présent |
| Fee shipment sans achat | weight basis vente du lot si présent |
#### Net / brut
- Si `fee.weight_type = net` :
- lire `lot.qt.hist.quantity`.
- Si `fee.weight_type = brut` :
- lire `lot.qt.hist.gross_quantity`.
#### Per packing
- `mode = ppack` est exclu de la règle poids.
- `fee.quantity` représente une quantité de packing.
- Cette quantité peut être décorrélée du poids net ou brut.
#### Lump sum
- `mode = lumpsum` suit aussi la règle de quantité.
- Le montant reste forfaitaire.
- La quantité permet de calculer un prix par tonne plus précis.
### BR-PT-FEE-004 - Fees `% rate` via delta de financement
Source : `BR-PT-016` historique et notes `2026-04-30`
#### Règle consultant
Les frais financiers en pourcentage se calculent avec le delta de financement
de la ligne d'estimation `BL date`, pas avec la date du jour.
### Notes developpeur
#### Notes développeur
- Formule: `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`.
- Source du delta: ligne `Estimated date` avec `trigger = bldate`.
- Formule : `amount = unit_price * quantity * (price / 100) * fin_int_delta / 360`.
- Source du delta : ligne `Estimated date` avec `trigger = bldate`.
- Si aucune ligne `bldate` n'existe, ne pas calculer de montant `% rate`.
## Section développeur
### Champs clés
- Fee : `fee.fee`
- Lots du fee : `fee.lots`
- Quantité du fee : `fee.fee.quantity`
- Mode : `fee.fee.mode`
- État de quantité optionnel : `fee.fee.qt_state`
- Net / brut : `fee.fee.weight_type`
- État de quantité lot : `lot.qt.hist.quantity_type`
- Quantité nette : `lot.qt.hist.quantity`
- Quantité brute : `lot.qt.hist.gross_quantity`
- Ordre des états : `lot.qt.type.sequence`
- Weight basis achat / vente : `purchase.weight.basis.qt_type`
### Fonctions de calcul
- `Fee._get_effective_fee_lots()` :
- sélectionne physiques puis virtuels.
- `Fee._target_qt_type_for_lot()` :
- choisit `fee.qt_state` ou le `weight basis` ;
- plafonne l'état au `weight basis`.
- `Fee._select_lot_qt_type()` :
- prend l'état exact s'il existe ;
- sinon prend l'état antérieur le plus proche par `sequence`.
- `Fee._get_lot_fee_quantity()` :
- lit net ou brut selon `weight_type`.
- `Fee.sync_quantity_from_lots()` :
- resynchronise `fee.quantity`.
### Garde-fous Python
- Check central :
- `fee.fee.assert_quantity_consistency()`
- `fee.fee.assert_quantities_consistency()`
- Points de déclenchement :
- création de fee ;
- modification de fee ;
- création / modification / suppression de `fee.lots` ;
- weighing via resynchronisation des fees liés.
- Exception :
- `mode = ppack` n'est pas contrôlé comme un poids.
### Diagnostic SQL
- Script :
- [sql/fee_quantity_consistency_checks.sql](sql/fee_quantity_consistency_checks.sql)
- Usage :
- audit des bases de test ;
- audit des données historiques ;
- qualification avant correction.
- Le script remonte les fees non `ppack` dont `fee.quantity` ne correspond pas
à la somme des lots effectifs selon l'état de quantité applicable.

View File

@@ -45,3 +45,44 @@ Main columns:
Cross-category UoM rows are reported as manual-review diagnostics because the
Python code may pass explicit conversion factors that cannot be inferred safely
from SQL alone.
## fee_quantity_consistency_checks.sql
Checks the fee quantity invariant documented in `fees.md` and `fees.en.md`.
The script ignores `mode = 'ppack'` because Per packing follows a packing
quantity, not the lot net/gross weight.
For every other fee, it recomputes the expected quantity from the effective
`fee.lots`:
- physical lots if at least one physical lot is linked;
- otherwise virtual lots;
- `fee.qt_state` when filled, capped by the contract `weight basis`;
- otherwise the contract `weight basis`;
- closest previous `lot.qt.hist` state by `lot.qt.type.sequence` when the exact
state is missing;
- `lot.qt.hist.quantity` for net fees and `lot.qt.hist.gross_quantity` for gross
fees.
Run it on a restored test database:
```sql
\i modules/purchase_trade/docs/business/sql/fee_quantity_consistency_checks.sql
```
The script returns rows only when it finds a potential issue.
Main columns:
- `check_name`: failed invariant or manual-review diagnostic.
- `fee_id`: `fee.fee` id.
- `purchase_id` / `purchase_number`: related purchase contract when available.
- `sale_id` / `sale_number`: related sale contract when available.
- `observed_value`: current `fee.quantity`.
- `expected_value`: recomputed quantity from effective lots.
- `diff`: observed minus expected.
- `detail`: human-readable explanation.
Rows reported as `fee_quantity_uom_manual_review` need manual analysis because
SQL cannot safely infer cross-category conversion factors.